투 포인터 알고리즘은 일차원 배열상에서 두개의 포인터를 움직여 문제를 해결하는 방식을 말한다.  

일반적으로 두 개의 포인터는 각각 배열의 항목 중 하나를 가리키거나 연속구간의 시작과 끝을 나타내는 형태로 사용된다.

 

1. 연속구간의 합에 관한 문제

 합이 특정한 수가 되도록 하는 모든 연속 부분구간을 구하는 문제는 투 포인터 알고리즘이 사용되는 대표적 예시이다.  절차는 다음과 같다.

 

주어진 수열 seq 의 길이가 n 일 때, seq의 연속 부분수열 중 합이 m 이 되는 것의 갯수 answer 를 구한다면

① 주어진 수열의 시작과 끝을 나타낼 s, e(두 개의 포인터)와 구간의 합을 나타낼 t를 모두 0으로 초기화

② t에 seq[e] 를 더한다.

③ s가 e보다 커지거나 t가 m보다 작거나 같아질 때 까지 시작구간을 뒤로 이동하고 그만큼 합계를 감산

④ 연속구간의 길이가 1이 될때까지도 t가 m보다 크다면 e가 구간의 끝인 상태에선 더이상 t가 m이 될

    가능성이 없기 때문에 e += 1

⑤ ④에서 s<=e 인채로 t가 m보다 작거나 같아졌을 경우, t == m이라면 answer += 1

⑥ ②~⑤를 e가 n-1 이하일 동안 반복

⑦ 반복문이 종료된 후 answer 가 seq의 연속 부분수열 중 합이 m이 되는 것의 갯수가 된다.

 

아래는 이를 구현하여 문제(https://www.acmicpc.net/problem/2003)를 해결한 코드이다. 

import sys

input = sys.stdin.readline


def sol2003():
    n, m = map(int, input().split())
    seq = list(map(int, input().split()))
    
    # 1
    s, t = 0, 0
    res = 0
    
    # 6
    for e in range(n):
    	# 2
        t += seq[e]
        
        # 3
        while t > m and s <= e:
            t -= seq[s]
            s += 1
           
        # 4
        if s > e:
            continue
            
        # 5
        if t == m:
            res += 1
            
    # 7
    return res


if __name__ == '__main__':
	print(sol2003())

 

2. 두 수의 합

 정렬된 수열의 양끝에서 두 개의 포인터를 좁혀가면서 두 수의 합이 특정 값과 같은 경우의 수를 찾거나 

특정 값에 가장 가까운 쌍을 찾는 등의 문제 또한 투포인터를 사용하여 해결 가능하다.  합의 특정한 수가 되는 경우의 수를 구하는 것은 매우 간단하고 투포인터를 사용하는 것 보다 쉬운 해결책도 있기 때문에 여기서는 특정 수에 가장 가까운 쌍을 찾는 문제만을 다룬다.

 

길이가 N인 수열 seq가 주어졌을 때, 이 중 두 수를 합한 값이 M에 가장 가까운 순서쌍을 구한다면

① seq를 오름차순 정렬

② 시작과 끝 포인터 s, e를 각각 0, N-1 로 초기화

③ 답이 될 두 수 r1, r2를 각각 0으로, M과의 차의 최솟값 d를 무한으로 초기화 

④ seq[s] + seq[e]와 M의 차가 d보다 작은 경우 r1, r2를 seq[s], seq[e]로, d를 abs(M-(seq[s]+seq[e]))로 갱신

⑤ seq[s] + seq[e] 가 M보다 작다면 s += 1, 크다면 e -= 1, M과 같다면 더이상 탐색이 필요없기 때문에 반복 종료

⑥ ④, ⑤ 과정을 s <= e인 동안 반복한다.

 

아래는 이를 구현하여 문제(https://www.acmicpc.net/problem/2470)를 해결한 코드이다.

import sys

input = sys.stdin.read


def sol2470():
    n, *seq = map(int, input().split())
    # 1
    seq.sort()
    
    # 2
    s, e = 0, n-1
    
    # 3
    r1, r2, d = 0, 0, float('inf')
    
    # 6
    while s < e:
        t = seq[s]+seq[e]
        # 4
        if abs(t) < d:
            r1, r2, d = seq[s], seq[e], abs(t)
            
        # 5
        if t == 0:
            break
        elif t < 0:
            s += 1
        else:
            e -= 1
    return '%d %d' % (r1, r2)


if __name__ == '__main__':
    print(sol2470())

 

3. Meet in the Middle

 문제의 풀이법이 지수시간을 가질 때( ex) 2^N ) N이 조금만 커져도 해결에 엄청난 시간이 소모된다. 

Meet in the Middle은 이러한 문제를 절반으로 나누어 각각 해결하는것으로 시간복잡도를 줄이는 방식이다. 

 

N(1<=N<=40)개의 정수로 이루어진 수열 seq가 주어졌을 때 seq의 부분수열 중 크기가 양수이고 합이 S가 되는것의 수를 구할 경우, 단순히 모든 조합을 따진다면 2^40이라는 엄청난 시간이 필요하다. 이를 Meet in the middle 알고리즘을 활용하여 해결해본다.

① 주어진 수열을 반으로 나눈다

② 나누어진 각 수열에 대해 모든 부분 수열의 합을 구하여 dictionary에 카운트한다.(ls, rs)

③ 한쪽의 부분수열의 합이 T가 되는 경우의 수에 대해 다른 한쪽의 부분수열의 합이 S-T 가 되는 경우의 수를 곱하여 더해나간다. 

④ 만약 S가 0이라면 크기가 0인 부분수열(공집합)이 답에 포함되기 때문에 답에서 1을 빼준다.

 

아래는 이를 구현하여 부분수열의 합 문제(https://www.acmicpc.net/problem/1208)를 해결한 코드이다.

import sys
from itertools import combinations

input = sys.stdin.read


def sol1208():
    n, s, *seq = map(int, input().split())
    mid = n // 2
    # 1
    left, right = seq[:mid], seq[mid:]
    
    # 2
    ls, rs = {}, {}
    for c in [map(sum, combinations(left, i)) for i in range(mid + 1)]:
        for t in c:
            ls[t] = ls.get(t, 0) + 1
    for c in [map(sum, combinations(right, i)) for i in range(n - mid + 1)]:
        for t in c:
            rs[t] = rs.get(t, 0) + 1

    # 3
    res = 0
    for t in ls.keys():
        res += ls[t] * rs.get(s - t, 0)
        
    # 4
    if s == 0:
        res -= 1
        
    return res


if __name__ == '__main__':
    print(sol1208())

Meet in the middle 알고리즘은 큰 문제를 작은문제로 나누어 해결하는 부분에서 분할정복과 비슷한 느낌이지만 문제를 재귀적으로 쪼개나가며 해결하는 방식은 아니라는점에서 차이가 있다.  

+ Recent posts