다음과 같은 문제를 생각해 보자.
n개의 서로 다른 정수가 주어질 때, 여기서 서로 다른 두 수를 골라서 합이 x가 되게 만드는 경우의 수를 구하시오.
이 문제의 경우 입력되는 정수의 범위가 크지 않아서 크기가 106인 배열을 선언해서 각 수가 등장했는지 체크하는 식의 방법으로 풀 수 있다. 하지만 입력되는 정수의 범위가 1018 정도까지 커졌다고 가정해 보자. 일단 set 등을 사용해서 각 수마다 x에서 그 수를 뺀 값이 set에 존재하는지 확인하는 방식으로 해결이 가능하다. 하지만 set은 속도가 그렇게 빠르지 않고, 각 수마다 set에서 값을 찾는 시간복잡도가 O(lgn)이다. 이번 글에서는 이 방법을 대체할 수 있는 투 포인터(Two Pointers)에 대해 다룬다.
서로 다른 두 수를 고를 때 수들의 순서가 어떻게 되어 있는지는 중요하지 않다. 이렇게 수들의 순서가 중요하지 않은 경우 일단 정렬을 하자. 그러면 풀이가 보이는 경우가 많다. 이런 문제도 그런 경우 중 하나인데, 두 수를 고를 수열이 다음과 같고 쌍의 개수를 구해야 하는 합이 22라고 하자.
[4,7,12,6,15,3,19,17,10]
이걸 정렬하면
[3,4,6,7,10,12,15,17,19]
이렇게 된다. 이제 투 포인터를 어떻게 적용하는지 알아보자. 먼저 이 수열에서 두 수를 골라서 합이 22가 되는 경우의 수는 3이다. 여기서 가능한 경우를 나열해 보면
[3,4,6,7,10,12,15,17,19]
[3,4,6,7,10,12,15,17,19]
[3,4,6,7,10,12,15,17,19]
이렇게 된다. 합이 일정하기 때문에 고른 수 중 작은 수가 커지면 큰 수는 작아지며, 수열이 오름차순으로 정렬되어 있기 때문에 가능한 경우 중 아무거나 둘을 비교했을 때 작은 수가 상대적으로 뒤에 나올 경우 큰 수는 상대적으로 앞에 나오는 것을 알 수 있다. 즉 어떤 가능한 쌍 (ai,aj)를 찾았고(여기서 ai<aj라고 가정한다. 따라서 i<j이다.) 그 다음으로 (ai+1,ak)가 가능한 쌍이 되는 k가 존재하는지 확인할 경우 j보다 큰 값들은 k의 후보가 될 수 없다. 따라서 i를 증가시킬 때 j를 계속 감소시키면서 가능한 쌍이 있는지 확인하면 된다. 이렇게 i와 j라는 두 인덱스(포인터)를 움직이면서 답을 구하기 때문에 이 방법을 투 포인터라고 부른다. 그러면 포인터 i와 j를 어떤 순서로 움직여야 할지를 알아야 하는데, 이를 위에 나온 예시의 설명을 통해 알아보기로 한다.
먼저 포인터를 수열의 양쪽 끝에 둔다. 편의상 첫 번째 수에 있는 포인터를 A, 마지막 수에 있는 포인터를 B라고 하자.
[3,4,6,7,10,12,15,17,19]
두 포인터가 가리키는 수의 합을 확인한다. 이 경우 처음부터 합이 22와 일치하므로 가능한 경우의 수에 1을 더한다. 다음으로 포인터를 움직이는데, 합이 22보다 작다면 A를 다음 위치로 옮기고, 합이 22보다 크다면 B를 이전 위치로 옮긴다. 지금처럼 합이 22인 경우는 둘 중 어느 쪽을 선택해도 된다. 여기서는 A를 다음 위치로 옮겼다고 하자.
[3,4,6,7,10,12,15,17,19],ans=1
포인터가 가리키는 수들의 합이 22보다 크므로 이번에는 B를 이전 위치로 옮긴다.
[3,4,6,7,10,12,15,17,19],ans=1
포인터가 가리키는 수들의 합이 22보다 작으므로 A를 다음 위치로 옮긴다.
[3,4,6,7,10,12,15,17,19],ans=1
이 과정을 반복한다.
[3,4,6,7,10,12,15,17,19],ans=1
[3,4,6,7,10,12,15,17,19],ans=2
[3,4,6,7,10,12,15,17,19],ans=2
[3,4,6,7,10,12,15,17,19],ans=3
[3,4,6,7,10,12,15,17,19],ans=3
두 포인터가 같은 위치에 왔을 경우 모든 경우를 다 확인한 것이므로 여기서 탐색을 종료하면 된다. 포인터를 움직이는 횟수가 n을 넘지 않으므로 시간복잡도는 Θ(n)이 된다. 계속 탐색을 하면 가능한 경우를 또 찾을 수도 있지만, 그 경우들은 이미 앞에서 다 찾았고 단지 (ai,aj)에서 (aj,ai)로만 바뀐 것이므로 순서가 중요하지 않은 경우 의미가 없다.
이제 이런 방식으로 정렬된 수열에서 투 포인터로 조건에 맞는 쌍의 개수를 선형 시간에 구할 수 있다. 구현은 다음과 같다.
int n, a[100005]; // a 배열의 원소는 오름차순으로 정렬되어 있다고 가정한다.
int f(int x)
{
int s = 0;
for(int i = 1, j = n; i < j;)
{
if(a[i] + a[j] == x)s++;
if(a[i] + a[j] <= x)i++;
else j--;
}
}
[연습문제]
양쪽 배열에 포인터를 하나씩 에서 가장 작은 값부터 차례로 출력한다. 다만 (N+M)개의 정수를 다 입력받고 전체를 정렬하는 풀이도 충분히 빠르게 통과한다고 하니 참고하자.
양 끝에서부터 포인터를 가운데로 움직이면서 합이 0에 가장 가까운 조합을 찾는다. 합이 양수일 때는 양수 쪽에 있는 포인터를, 합이 음수일 때는 음수 쪽에 있는 포인터를 움직이면 된다.
BOJ 1806. Subsequence (Gold IV)
'Algorithm > K. Others' 카테고리의 다른 글
227. Case Work (0) | 2025.04.15 |
---|---|
226. Offline Query (1) | 2025.04.09 |
213. IMOS Method (4) | 2025.01.18 |
209. Coordinate Compression (0) | 2024.08.20 |
208. Others Intro (2) | 2024.08.12 |