N개의 수로 된 수열 A[1], A[2], …, A[N] 이 있다. 이 수열의 i번째 수부터 j번째 수까지의 합 A[i] + A[i+1] + … + A[j-1] + A[j]가 M이 되는 경우의 수를 구하는 프로그램을 작성하시오.
첫째 줄에 N(1 ≤ N ≤ 10,000), M(1 ≤ M ≤ 300,000,000)이 주어진다. 다음 줄에는 A[1], A[2], …, A[N]이 공백으로 분리되어 주어진다. 각각의 A[x]는 30,000을 넘지 않는 자연수이다.
첫째 줄에 경우의 수를 출력한다.
4 2
1 1 1 1
3
10 5
1 2 3 4 2 5 3 1 1 2
3
✌투 포인터로 해결할 수 있는 문제입니다.
수열의 숫자들을 가리키는 포인터 변수 두 개를 가지고, 두 변수를 이동시키면서 변수 사이의 숫자들의 합을 확인하는 식으로 문제를 해결하였습니다.
- 숫자들의 합이 M인 경우
정답의 개수를 하나 증가한 뒤, 왼쪽의 포인터를 한 칸 이동해줍니다.- 숫자들의 합이 M보다 큰 경우
합이 더 커 포함되는 숫자의 수를 줄여야하므로, 왼쪽의 포인터를 한 칸 이동해줍니다.- 숫자들의 합이 M보다 작은 경우
합이 더 작아 더 많은 숫자를 포함시켜야 하므로, 오른쪽의 포인터를 한 칸 이동해줍니다.
import sys
input = sys.stdin.readline
N, M = map(int, input().split())
A = list(map(int, input().split()))
left, right = 0, 1 # 왼쪽과 오른쪽을 가리키는 포인터
ans = 0
while left <= right <= N:
total = sum(A[left:right]) # left 포함 right 미포함 사이 합 계산
if total == M: # 합이 M인 경우 왼쪽 포인터 한 칸 이동
ans += 1
left += 1
elif total < M: # 합이 M보다 작은 경우 오른쪽 포인터 한 칸 이동
right += 1
else: # 합이 M보다 큰 경우 왼쪽 포인터 한 칸 이동
left += 1
print(ans)
left와 right의 두 포인터를 이동하면서, 두 포인터 사이의 합들을 계속해서 계산해서 계산한 합이 M이 되는지를 확인하는 코드입니다.
while 문 내에서 A 배열의 slicing과 sum 계산이 반복되기 때문에 시간이 오래 걸립니다.
따라서 매번 구간합을 새로 계산하지 않고, left와 right의 이동에 따라 구간에 새로 더해지거나 빠지는 숫자들만을 더하고 빼준다면, 의 시간복잡도로 빠르게 문제를 해결할 수 있습니다.
import sys
input = sys.stdin.readline
N, M = map(int, input().split())
A = list(map(int, input().split())) + [0]
ans = left = 0
right = 1
sum_nums = A[0] # 시작 구간 합 : sum(A[0:1])
while left <= right <= N:
if sum_nums > M: # 합이 M보다 큰 경우, left 값을 빼주고 left 한 칸 이동
sum_nums -= A[left]
left += 1
elif sum_nums < M: # 합이 M보다 작은 경우, right 값을 더해주고 right 한 칸 이동
sum_nums += A[right]
right += 1
else: # 합이 M인 경우, left 값 빼주고 left 한 칸 이동
sum_nums -= A[left]
left += 1
ans += 1
print(ans)
매번 구간합을 새로 계산하지 않고, 구간의 변화에 따라 숫자들만 더하고 빼주면서 합을 구해가는 방식의 코드입니다.
코드1에 비하여 시간이 대폭 줄어든 것을 확인할 수 있습니다.
A 리스트의 마지막에 [0]을 더해준 이유는, 반복문 내에서 right가 N보다 커지는 경우를 확인해야하는 경우가 있는데, 그 조건을 매번 확인하지 않고 index overflow가 나지 않도록 더미값을 더해준 것입니다.
백준 미로 탐색 문제에서 리스트를 0으로 감싸준 것과 같은 맥락입니다.