1. 누적합(Prefix Sum)이란?
- 누적합은 배열의 각 위치까지의 원소들의 합을 미리 계산해두는 기법입니다.
- 특정 구간의 합을 빠르게 계산해야 할 때 유용하게 사용됩니다.
- 구간의 합이 아니라 구간의 업데이트를 빠르게 수행해야 할 때는 차분 배열을 사용합니다.
- 차분 배열
- 구간 업데이트를 O(1) 시간에 수행할 수 있는 장점이 있습니다.
- DifferenceArray.md 참고
1.1 기본 개념
- 누적합 배열의 각 원소는 원본 배열의 처음부터 해당 위치 직전까지의 모든 원소의 합을 저장합니다.
- 구 간 [L, R]의 합을 O(1) 시간에 계산 가능합니다.
- 메모리를 조금 더 사용하는 대신 반복적인 구간 합 계산을 최적화합니다.
1.2 누적합 배열의 특별한 설계
- 누적합 배열(prefix_sum)은 다음과 같은 특징을 가집니다:
- prefix_sum[i]는 원본 배열의 0번째부터 (i-1)번째까지의 합을 저장
- prefix_sum[0]은 항상 0으로 초기화
- prefix_sum의 길이는 원본 배열보다 1만큼 더 김
- 이렇게 설계하는 이유:
- 구간 합 계산이 더 직관적이고 일관됨
- 경계 조건(0번째 인덱스, 마지막 인덱스) 처리가 간단
- 실수할 가능성이 줄어듦
# 기존 방식(포함하지 않는 방식)
sum_0_to_2 = prefix_sum[3] - prefix_sum[0] # 10 - 0 = 10
# 포함하는 방식
sum_0_to_2 = prefix_sum[2] # 특별 처리 필요
- 만약 prefix_sum[i]는 원본 배열의 0번째부터 i번째까지의 합을 저장한다면
- 0번째 인덱스부터 시작하는 구간 합에 대해서 특별한 처리가 필요합니다.
2. 1차원 배열의 누적합
2.1 구현 방법
누적합 배열 생성
def create_prefix_sum(arr):
n = len(arr)
prefix_sum = [0] * (n + 1) # 길이가 n+1인 배열 생성
for i in range(n):
prefix_sum[i + 1] = prefix_sum[i] + arr[i]
return prefix_sum
prefix_sum[i + 1]
는arr[0]
부터arr[i]
까지의 합을 저장합니다.- 따라서
prefix_sum[i]
와arr[i]
를 더하면arr[0]
부터arr[i]
까지의 합이 됩니다.
구간 합 계산
def get_range_sum(prefix_sum, left, right):
return prefix_sum[right + 1] - prefix_sum[left]
prefix_sum[right + 1]
에서prefix_sum[left]
를 빼면 구간 [left, right]의 합이 됩니다.
2.2 상세 예제와 설명
예제 데이터
원본 배열: [1, 3, 6, 2, 5, 4]
누적합 배열: [0, 1, 4, 10, 12, 17, 21]
각 prefix_sum 값의 의미:
- prefix_sum[0] = 0 (아무 원소도 포함하지 않음)
- prefix_sum[1] = 1 (arr[0]까지의 합)
- prefix_sum[2] = 4 (arr[0:2]까지의 합: 1 + 3)
- prefix_sum[3] = 10 (arr[0:3]까지의 합: 1 + 3 + 6)
- prefix_sum[4] = 12 (arr[0:4]까지의 합: 1 + 3 + 6 + 2)
구간 합 계산 예시:
# 인덱스 1부터 3까지의 구간 합
sum_1_3 = prefix_sum[4] - prefix_sum[1] # 12 - 1 = 11
# 실제로 arr[1] + arr[2] + arr[3] = 3 + 6 + 2 = 11
# 인덱스 0부터 2까지의 구간 합
sum_0_2 = prefix_sum[3] - prefix_sum[0] # 10 - 0 = 10
# 실제로 arr[0] + arr[1] + arr[2] = 1 + 3 + 6 = 10
3. 2차원 배열의 누적합
- 이번엔 2차원 배열의 누적합을 구하는 방법에 대해 알아봅니다.
- 2차원 배열의 각 위치까지의 원소들의 합을 미리 계산해두는 방법입니다.
- 2차원 배열의 특정 구간의 합을 빠르게 계산할 수 있습니다.
- 1차원 배열의 누적합과 비슷하게 구현할 수 있습니다.
- 2차원 배열의 누적합을 구하면 2차원 구간 합을 O(1) 시간에 계산할 수 있습니다.
3.1 구현 방법
2차원 누적합 배열 생성
def create_2d_prefix_sum(matrix):
if not matrix or not matrix[0]:
return []
rows, cols = len(matrix), len(matrix[0])
# (rows+1) x (cols+1) 크기의 배열 생성
prefix_sum = [[0] * (cols + 1) for _ in range(rows + 1)]
for i in range(rows):
for j in range(cols):
prefix_sum[i + 1][j + 1] = (
prefix_sum[i][j + 1] + # 위쪽
prefix_sum[i + 1][j] - # 왼쪽
prefix_sum[i][j] + # 중복 제거
matrix[i][j] # 현재 값
)
return prefix_sum
2차원 구간 합 계산
def get_2d_range_sum(prefix_sum, x1, y1, x2, y2):
return (
prefix_sum[x2 + 1][y2 + 1] -
prefix_sum[x1][y2 + 1] -
prefix_sum[x2 + 1][y1] +
prefix_sum[x1][y1]
)
3.2 상세 예제와 설명
예제 데이터
원본 행렬:
1 2 3
4 5 6
7 8 9
누적합 행렬:
0 0 0 0
0 1 3 6
0 5 12 21
0 12 27 45
각 행과 열의 첫 번째 값이 0인 이유:
- 2차원에서도 1차원과 같은 설계 원칙 적용
- 경계 조건 처리 단순화
- 구간 합 계산 공식의 일관성 유지
4. 시간 복잡도와 공간 복잡도
4.1 1차원 누적합
- 시간 복잡도:
- 누적합 배열 생성: O(N)
- 구간 합 계산: O(1)
- 공간 복잡도: O(N)
4.2 2차원 누적합
- 시간 복잡도:
- 누적합 배열 생성: O(N×M)
- 구간 합 계산: O(1)
- 공간 복잡도: O(N×M)
5. 누적합의 장단점
5.1 장점
- 반복적인 구간 합 계산이 필요할 때 효율적
- 구간 합 계산이 O(1)로 매우 빠름
- 코드가 간결하고 실수할 가능성이 적음
- 2차원까지 확장 가능
5.2 단점
- 추가 메모리 필요
- 배열이 자주 변경되는 경우 누적합 배열 재계산 필요
- 초기 전처리 시간 필요
6. 실전 활용 팁
6.1 적용하기 좋은 경우
- 구간 합을 여러 번 계산해야 할 때
- 배열이 자주 변경되지 않을 때
- 메모리 제한이 충분할 때
6.2 주의사항
- 메모리 제한 확인
- 배열 크기가 커질 때의 공간 복잡도 고려
- 0번 인덱스 처리 주의
7. 마치며
- 누적합은 단순하지만 강력한 최적화 기법입니다.
- 특히 구간 합을 자주 계산해야 하는 상황에서 매우 유용하며, 1차원뿐만 아니라 2차원으로도 확장하여 다양한 문제를 해결할 수 있습니다.