본문으로 건너뛰기
홈
기술
기술 전체
프로그래밍68
컴퓨터 과학63
AI48
웹 개발36
인프라33
데이터31
소프트웨어 공학18
소개
← 목록으로컴퓨터 과학 › 알고리즘 › 이론

14. 구간합

목차

여러 번 묻는 구간의 합을 매번 처음부터 더하면 한 질문에 최악 O(N) 시간이 든다. 누적합(prefix sum)은 앞에서부터의 합을 한 번 저장해 두고 두 누적값의 차이로 구간을 구한다. 전처리에 O(N) 시간·공간을 쓰고 이후 각 질문을 O(1)에 답한다. 배열의 값이 바뀌지 않고 질의가 많을 때 특히 유리하다.

합 배열 S의 정의

  • A[0]부터 A[i]까지의 합

S[i] = A[0] + A[1] + A[2] + … + A[i-1] + A[i];

합 배열은 기존의 배열을 전 처리한 배열로써 이런 식으로 미리 구해 놓으면 기존 배열의 일정 범위의 합을 구하는 시간 복잡도가 O(n) 에서 O(1)로 감소

인덱스012345
배열A1513107312
합배열 S152838454860

합 배열 S를 만드는 공식

S[i] = S[i-1] + A[i] (i >= 1, 0 기반 배열이라면 S[0] = A[0])

구간 합을 구하는 공식

S[j] - S[i-1] (i부터 j까지 포함, i=0이면 앞부분의 합을 0으로 취급)

왜 빼는가? S[j]는 A[0]부터 A[j]까지이고 S[i-1]은 A[0]부터 A[i-1]까지다. 둘의 차에는 앞부분이 사라지고 A[i]...A[j]만 남는다. 위 표에서 인덱스 2부터 4까지는 10+7+3=20이고, S[4]-S[1]=48-28=20이다. i=0에서 S[-1]을 읽으면 오류가 나므로 코드에서는 앞에 0을 둔 1 기반 누적합 배열을 쓴다.

  • 구간 합을 예제
import java.util.Scanner;
public class 구간합구하기4_11659 {
    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);
        int N = scan.nextInt();
        int M = scan.nextInt();
        long[] S = new long[N + 1]; // S[0] = 0
        for (int i = 1; i <= N; i++) {
            S[i] = S[i-1] + scan.nextInt(); //합배열 S를 만드는 공식
        }

        StringBuilder answer = new StringBuilder();
        for (int seq = 0; seq < M; seq++) {
            int i = scan.nextInt();
            int j = scan.nextInt();
            if (i < 1 || j > N || i > j) {
                throw new IllegalArgumentException("구간은 1 <= i <= j <= N 이어야 합니다");
            }
            long sum = S[j] - S[i-1];
            answer.append(sum).append('\n');
        }
        System.out.print(answer);
    }
}

2차원으로 확장: 사각형을 네 조각으로 생각하기

2차원 구간 합 배열 D[x][y]

D[x][y] = 원본 배열의 (0,0)부터 (x,y)까지 사각형 영역 안있는 수의 합

       **숫자배열A[i][j]**                                                        
1234
11234
22345
33456
44567

완성된 구간합 배열 D[i][j]

1234
113610
2381524
36152742
410244264
  • 가로 구간 합 배열

D[1][j] = D[1][j-1] + A[1][j]

  • 세로 구간 합 배열

D[i][1] = D[i-1][1] + A[i][1]

D[i][j]의 값을 채우는 구간 합 공식

D[i][j] = D[i-1][j] + D[i][j-1] - D[i-1][j-1] + A[i][j]

윗쪽 직사각형 D[i-1][j]와 왼쪽 직사각형 D[i][j-1]을 더하면 왼쪽 위 D[i-1][j-1] 영역이 두 번 들어간다. 한 번 빼고 현재 칸 A[i][j]를 더해야 한다. 기존 식은 A[i-1][j] 같은 원본 배열 값을 더해 누적 영역을 만들지 못했다. 첫 행·열의 경계를 간단히 처리하려면 D[0][j]와 D[i][0]을 0으로 둔다.

질문 사각형이 (r1,c1)부터 (r2,c2)까지라면 양 끝을 포함한 합은 다음과 같다.

D[r2][c2] - D[r1-1][c2] - D[r2][c1-1] + D[r1-1][c1-1]

전체 큰 직사각형에서 위쪽과 왼쪽을 빼면 왼쪽 위 교집합을 두 번 뺀 셈이므로 한 번 되돌려 더한다. 예를 들어 위 표의 원본에서 (2,2)부터 (3,4)까지는 3+4+5+4+5+6=27이고, D[3][4]-D[1][4]-D[3][1]+D[1][1]=42-10-6+1=27이다.

public class PrefixSum {
    public static void main(String[] args) {
        int[][] a = {{1,2,3,4}, {2,3,4,5}, {3,4,5,6}, {4,5,6,7}};
        int rows = a.length;
        int cols = a[0].length;
        long[][] sum = new long[rows + 1][cols + 1];

        for (int i = 1; i <= rows; i++) {
            for (int j = 1; j <= cols; j++) {
                sum[i][j] = sum[i-1][j] + sum[i][j-1] - sum[i-1][j-1] + a[i-1][j-1];
            }
        }

        int r1 = 2, c1 = 2;
        int r2 = 3, c2 = 4;

        long result = sum[r2][c2] - sum[r1-1][c2] - sum[r2][c1-1] + sum[r1-1][c1-1];
        System.out.println(result); // 27
    }
}

rows와 cols를 따로 두어 직사각형 배열도 처리한다. sum[i][j]는 원본의 첫 i행·첫 j열의 합이므로 원본 인덱스는 a[i-1][j-1]이다. 바깥 반복문을 한 행씩 채울 때 필요한 위쪽 sum[i-1][j], 왼쪽 sum[i][j-1], 대각선 sum[i-1][j-1]은 이미 계산되어 있다. 전처리는 O(RC), 한 사각형 질의는 O(1), 저장 공간은 O(RC)다.

값이 바뀌는 배열에는?

누적합은 정적 데이터에 맞는다. 한 원소를 수정하면 그 뒤의 누적합들이 달라져 1차원에서는 최악 O(N), 2차원에서는 최악 O(RC)를 다시 계산해야 한다. 값 수정과 합 질의가 둘 다 자주 일어나면 펜윅 트리나 세그먼트 트리처럼 갱신과 질의를 나눠 처리하는 구조를 고려한다. 합이 int 범위를 넘을 수 있어 예제는 long으로 누적한다. 입력이 빈 행렬이거나 좌표가 범위를 벗어나면 조회 전에 별도 검증이 필요하다.

같은 카테고리의 글