RiseOJ는 solved.ac와 제휴 관계가 없습니다. 티어 아이콘 © solved.ac. solved.ac
← 실버 III

누적 합

구간 합을 O(1)로 — 전처리의 힘.

선수 지식: 배열
1강 전처리로 구간 합을 O(1)에 공식

누적 합이란?

누적 합(prefix sum) 은 배열의 "앞에서부터의 합"을 미리 계산해 두어, 임의
구간의 합을 \(O(1)\)에 구하는 전처리 기법입니다.

구간 합을 매번 반복문으로 더하면 한 번에 \(O(N)\), 질의가 \(Q\)개면 \(O(NQ)\)입니다.
누적 합을 쓰면 전처리 \(O(N)\) 후 각 질의 \(O(1)\), 전체 \(O(N + Q)\)로 줄어듭니다.


1. 핵심 아이디어

prefix[i]를 "처음부터 \(i\)번째까지의 합"으로 정의하면, 구간 \([l, r]\)의 합은
두 누적 합의 차로 구해집니다.

$$ \text{sum}(l, r) = \text{prefix}[r] - \text{prefix}[l-1] $$

a       = [3, 1, 4, 1, 5]
prefix  = [3, 4, 8, 9, 14]   (앞에서부터 누적)
sum(1,3) = prefix[3] - prefix[0] = 9 - 3 = 6   (a[1]+a[2]+a[3] = 1+4+1)

큰 합에서 앞부분을 빼면 가운데 구간만 남는다 — 이 한 줄이 전부입니다.


2. 왜 빠른가

전처리는 배열을 한 번 훑으며 누적하므로 \(O(N)\). 이후 어떤 구간 합이든 뺄셈
한 번, 즉 \(O(1)\)입니다. 질의가 많을수록 이득이 커집니다.

방식 전처리 질의 1회 질의 \(Q\)
매번 더하기 없음 \(O(N)\) \(O(NQ)\)
누적 합 \(O(N)\) \(O(1)\) \(O(N + Q)\)

3. 1-인덱스가 편하다

누적 합은 \(\text{prefix}[l-1]\)을 자주 쓰기 때문에 \(l = 0\)에서 음수 인덱스가
생깁니다. prefix[0] = 0으로 두고 1-인덱스로 다루면 경계 처리가 깔끔해집니다.

$$ \text{prefix}[i] = \text{prefix}[i-1] + a[i], \quad \text{sum}(l, r) = \text{prefix}[r] - \text{prefix}[l-1] $$


4. 2차원으로 확장

격자에서 직사각형 영역의 합도 같은 원리로 \(O(1)\)에 구합니다. 포함-배제
핵심입니다.

$$ \text{sum} = P[r_2][c_2] - P[r_1-1][c_2] - P[r_2][c_1-1] + P[r_1-1][c_1-1] $$

겹쳐 뺀 왼쪽 위 모서리를 다시 더해 주는 것이 포인트입니다.


5. 차이 배열: 누적 합의 쌍대

반대로 "여러 구간에 일괄로 값을 더하고, 마지막에 한 번 결과를 본다"
차이 배열(difference array) 을 씁니다. 구간 \([l, r]\)\(v\)를 더할 때
diff[l] += v; diff[r+1] -= v;만 하고, 마지막에 diff의 누적 합을 취하면 됩니다.

각 구간 갱신이 \(O(1)\), 마지막 복원이 \(O(N)\)입니다. 누적 합과 차이 배열은
서로 역연산 관계입니다.


정리

누적 합은 "구간 합 = 두 누적 합의 차"라는 한 줄짜리 아이디어로 구간 질의를
\(O(1)\)에 답합니다. 1-인덱스로 짜면 깔끔하고, 2차원·차이 배열로 자연스럽게
확장됩니다. 다음 강의에서 구현과 응용을 다룹니다.

2강 1D·2D 누적 합과 차이 배열 구현 공식

누적 합을 코드로

1차원, 2차원 누적 합과 차이 배열을 구현하고, 흔한 실수를 정리합니다.


1. 1차원 누적 합 (1-인덱스)

int a[100001], prefix[100001];        // a는 1-인덱스로 입력
for (int i = 1; i <= n; i++)
    prefix[i] = prefix[i - 1] + a[i]; // 누적

// 구간 [l, r] 합
auto sum = [&](int l, int r){ return prefix[r] - prefix[l - 1]; };
cout << sum(2, 5) << '\n';
a = [0] + nums                        # 1-인덱스로 맞춤 (앞에 0 추가)
prefix = [0] * (n + 1)
for i in range(1, n + 1):
    prefix[i] = prefix[i - 1] + a[i]

def rng(l, r):                        # 구간 [l, r] 합
    return prefix[r] - prefix[l - 1]

prefix[0] = 0과 1-인덱스 덕분에 l = 1이어도 prefix[0]이 안전하게 0입니다.


2. 2차원 누적 합

int P[1001][1001];                    // P[i][j] = (1,1)~(i,j) 직사각형 합
for (int i = 1; i <= n; i++)
    for (int j = 1; j <= m; j++)
        P[i][j] = a[i][j]
                + P[i - 1][j] + P[i][j - 1] - P[i - 1][j - 1];

// (r1,c1)~(r2,c2) 직사각형 합
int rect = P[r2][c2] - P[r1 - 1][c2] - P[r2][c1 - 1] + P[r1 - 1][c1 - 1];
P = [[0] * (m + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
    for j in range(1, m + 1):
        P[i][j] = a[i][j] + P[i-1][j] + P[i][j-1] - P[i-1][j-1]

def rect(r1, c1, r2, c2):
    return P[r2][c2] - P[r1-1][c2] - P[r2][c1-1] + P[r1-1][c1-1]

겹쳐서 두 번 뺀 P[r1-1][c1-1]을 다시 더하는 포함-배제가 핵심입니다.


3. 차이 배열: 구간 일괄 갱신

"구간 \([l, r]\)\(v\)를 더하기"를 여러 번 한 뒤 최종 배열을 구합니다.

long long diff[100002] = {0};
// 각 갱신은 O(1)
for ( 갱신 (l, r, v)) { diff[l] += v; diff[r + 1] -= v; }
// 마지막에 누적 합으로 복원
for (int i = 1; i <= n; i++) diff[i] += diff[i - 1];
// 이제 diff[i] = i번 원소의 최종 값
diff = [0] * (n + 2)
for l, r, v in updates:
    diff[l] += v
    diff[r + 1] -= v
for i in range(1, n + 1):
    diff[i] += diff[i - 1]

각 구간 갱신이 \(O(1)\), 모든 갱신 후 복원이 \(O(N)\)입니다.


4. 응용: 출석/존재 카운팅

"어떤 값이 \([l, r]\) 범위에 몇 개 있나"는, 값의 등장 횟수 배열에 누적 합을 씌워
풉니다. 좌표 압축과 결합하면 큰 값에도 적용됩니다.


5. 흔한 실수

  • 인덱스 경계prefix[l - 1]에서 l = 0이면 음수 인덱스. 1-인덱스 + prefix[0]=0로 해결.
  • 자료형 오버플로 — 합은 금방 커집니다. long long을 쓰세요.
  • 차이 배열의 r + 1 — 배열 크기를 n + 2로 잡지 않으면 범위 초과.
  • 2D 포함-배제 부호 실수 — 빼고 빼고 더하는 부호를 헷갈리지 마세요.

6. 패턴 알아보기

  • "구간 합을 여러 번 물어본다" → 1D/2D 누적 합.
  • "여러 구간에 더한 뒤 결과를 본다" → 차이 배열.
  • "직사각형 영역의 합" → 2D 누적 합.
  • \(O(NQ)\)로 보이는 구간 질의 → 전처리로 \(O(N + Q)\).
3강 실전 가이드 — 구간 질문은 누적 합부터 공식

실전에서 누적 합 문제 알아보기

"구간"이라는 단어가 보이면 세그먼트 트리부터 떠올리기 쉽지만, Silver에서는
변경 없는 구간 질의 = 누적 합이 정답인 경우가 대부분입니다.


1. 출제 신호

  • "구간 \([l, r]\)의 합을 \(Q\)번 질의" + 원소 변경이 없음 — 누적 합의
    교과서 신호입니다. \(N, Q \le 10^5\)면 매번 더하기(\(O(NQ)\))는 시간 초과.
  • "합이 \(K\)인 연속 부분 수열의 개수" — 누적 합 + 해시맵
    (\(S_r - S_{l-1} = K\)를 변형).
  • 2차원 격자에서 직사각형 영역의 합 — 2D 누적 합.
  • "구간에 일괄로 더하는" 갱신이 많고 결과는 마지막에 한 번 — 차이 배열.
  • 평균, 개수("구간 안의 짝수 개수")도 합으로 환원되면 같은 기법입니다.

2. 풀이 결정 절차

  1. 갱신이 있는가? — 없으면 누적 합, "구간 더하기 후 최종 상태"면 차이
    배열, 갱신과 질의가 섞이면(Gold 이후) 트리 자료구조.
  2. 차원을 확인합니다 — 1D인가 2D인가.
  3. 인덱스 규약을 정합니다 — \(S_0 = 0\), \(S_i = a_1 + \cdots + a_i\)로 두면
    구간 합은 \(S_r - S_{l-1}\)이고 \(l=1\)도 자연스럽게 처리됩니다.
  4. 질의 공식을 종이에 적고 작은 예제로 손 검산한 뒤 코딩합니다.

3. 자주 하는 실수

  • 0-인덱스로 시작해 \(l-1\)이 음수. 1-인덱스 + \(S_0 = 0\) 규약이 경계
    분기를 없애 줍니다.
long long S[N + 1];                 // S[0] = 0
for (int i = 1; i <= n; i++)
    S[i] = S[i - 1] + a[i];         // a도 1-인덱스
// 질의 [l, r]:
long long sum = S[r] - S[l - 1];    // l = 1이어도 안전
  • 합의 오버플로. \(10^5\)개 × \(10^9\)면 합이 \(10^{14}\)int를 넘습니다.
    누적 배열은 처음부터 long long(파이썬은 무관)으로 선언하세요.
  • 2D 포함-배제 부호 실수. 직사각형 합은
    \(S_{r2,c2} - S_{r1-1,c2} - S_{r2,c1-1} + S_{r1-1,c1-1}\) — 마지막 항은
    더하기입니다. 전처리 점화식에서도 같은 항을 빼야 합니다.
  • 차이 배열의 \(r+1\) 위치. 구간 \([l, r]\)\(+x\)d[l] += x; d[r+1] -= x;
    입니다. 배열 크기를 \(N+2\)로 잡지 않으면 r = N에서 범위를 벗어납니다.
  • 빠른 입력 누락. \(N + Q\)가 수십만이면 scanf/sys.stdin.readline 없이
    입력만으로 시간이 끝나는 경우가 있습니다.

4. 연습 방법

이 페이지 오른쪽의 추천 문제는 쉬운 것부터 어려운 것 순입니다. 1D 구간 합
→ 2D → 차이 배열·합=K 변형 순서로 단계가 올라가도록 구성되어 있습니다.

문제를 읽으면 코딩 전에 질의 공식을 수식 한 줄(\(S_r - S_{l-1}\) 꼴)로 먼저
적으세요. 공식이 적히지 않으면 아직 모델링이 덜 된 것입니다. 3문제 이상
풀어 클리어하면 레이팅의 CLASS 보너스에 반영됩니다.