"배열에서 이웃한 두 윈도우는 원소를 거의 다 공유해. 그런데 매번 처음부터 다시 계산하면 그 겹침을 그냥 버리는 거지. 슬라이딩 윈도우는 그걸 지켜내. O(n²)가 O(n)이 되는 건 딱 그 차이에서 나와."
낭비하는 방식과 고치는 방식
연속된 k개 원소의 최대 합을 구한다고 해 보자. 단순하게 가면 시작 위치마다 k개를 새로 더하게 돼. 윈도우가 n개고 하나에 덧셈이 k번이니 O(n·k)고, k가 n을 따라 커지면 O(n²)이지. 그런데 이웃한 윈도우 둘을 나란히 놓고 보면 양 끝 원소 하나씩만 다르고 나머지는 전부 겹쳐. 무차별 대입은 그 겹치는 가운데를 매번 다시 더하고 있는 거야.
슬라이딩 윈도우는 여기를 고쳐. 첫 윈도우의 합만 한 번 제대로 구해 놓고, 그다음부터는 새로 들어온 원소를 더하고 빠져나간 원소를 빼면서 한 칸씩 밀어. 한 칸 미는 데 O(1)이니 전체가 O(n)으로 끝나지. 겹치는 부분은 손도 안 대고 양 끝만 손보는 거야. 분할 상환 분석이나 누적 합과 뿌리가 같은 발상이고. 이미 해놓은 일을 다시 하지 말 것.
윈도우는 두 종류야
- 고정 크기 윈도우 — 폭이 늘
k로 같고 그냥 밀기만 해. "모든 k칸 구간의 최댓값·최솟값·평균" 같은 문제에 써. - 가변 크기 윈도우 — 조건을 맞추려고 양 끝이 따로따로 움직여. "중복 없는 가장 긴 부분 문자열"에 잘 맞고, "합이 목표값 이상인 가장 짧은 부분 배열"에 쓰려면 조건이 하나 붙어. 원소가 모두 0 이상이어야 해. 그래야 윈도우를 넓힐 때 합이 한 방향으로만 움직이거든. 이 전제가 성립하면 양 끝이 앞으로만 가니까 전체가 O(n)이야.
슬라이딩 윈도우는 이웃한 범위끼리 겹치는 부분을 재사용해. 들어오는 원소는 더하고 나가는 원소는 빼고, 포인터는 둘 다 앞으로만 움직여. 그래서 O(n²)처럼 보이던 훑기가 O(n)이 돼.
가변 윈도우가 왜 그래도 O(n)일까
가변 윈도우는 겉모양이 중첩 반복문이야. 바깥에 포인터가 하나 있고 안쪽에 줄이는 반복문이 있으니 O(n²)이겠거니 하게 되지. 핵심은 왼쪽 포인터가 절대 뒤로 가지 않는다는 거야. 전체를 통틀어 오른쪽 포인터가 n번 전진하고 왼쪽 포인터도 많아야 n번 전진하니까, 이동 횟수를 다 합쳐도 2n을 안 넘어. 중첩된 모양을 볼 게 아니라 포인터가 실제로 움직인 총 횟수를 세야 O(n)이 보여. 복잡도 트랙에서 배운 분할 상환 추론을 반복문 모양에 그대로 적용한 거야.
피파의 고백
가변 크기 윈도우는 처음에 정말 안 받아들여졌어. 안쪽 while 때문에 무조건 O(n²)이라고 확신하고 아빠랑 그걸로 옥신각신했지. 아빠가 왼쪽 포인터에 카운터를 하나 달고 직접 돌려보라고 하더라. 배열 전체를 도는 동안 왼쪽 포인터는 한 단계에 n번이 아니라 통틀어 n번 움직였어. 카운터가 선형에 머무는 걸 눈으로 보고서야 납득이 됐어. 그 뒤로는 반복문이 제곱처럼 보일 때마다 직감을 믿기 전에 실제 이동 횟수부터 세.