읽기 전에 알아두면 좋아요
아래 개념이 익숙하면 본문을 바로 읽으세요. 낯선 개념만 링크에서 복습하면 됩니다.
- 슬라이딩 윈도우: 고정 길이 합과 양수 배열의 가변 구간슬라이딩 윈도우는 연속 구간에 새 값을 넣고 이전 값을 빼며 계산을 갱신합니다. 양수 배열에서 합이 커질수록 왼쪽을 줄일 수 있는 이유를 확인합니다.
개념과 예제는 실행하지 않아도 읽을 수 있습니다. 내 컴퓨터·터미널에서 직접 실행하는 과정은 선택 실습입니다.
과정 위치와 선택 복습 안내
과정 위치: 구간과 탐색 · 문제
과정 위치는 이 글이 속한 묶음입니다. 이 단계의 모든 개념을 먼저 읽을 필요는 없습니다. 위에 연결한 개념 중 낯선 내용만 복습하세요. 문제의 조건·상태·갱신 순서는 익숙한 언어로 설명하면 됩니다.
JavaScript로 문법·실행 복습하기 (선택)
개념 글의 코드는 JavaScript 예시입니다. 문제 화면에서는 JavaScript·Python·Java·C++·C# 중 익숙한 언어의 함수를 작성하세요. 본문의 Node.js 입출력 예시는 로컬 실행을 위한 별도 안내입니다.
합이 M인 연속 부분수열 문제는 어떤 값들로 이루어진 배열에서 무엇의 개수를 구하나요?
양의 정수 배열에서 합이 목표값인 연속 구간의 개수를 구하는 문제입니다. 배열·구간·합계의 기본을 준비하고, 아래 입력·반환 계약을 확인해 직접 풀어보세요.
적용 조건·범위: 이 문제에서 가장 중요한 조건은 원소가 모두 양수라는 점입니다. 오른쪽에 원소를 더하면 합은 반드시 커지고, 왼쪽 원소를 빼면 합은 반드시 작아집니다. 이 단조성 덕분에 이미 지나온 시작점을 다시 확인하지 않아도 됩니다.
본문 근거: MDN Map 문서
문제 접근 미리보기
연속 부분수열의 합이 목표값 M과 정확히 같은 경우를 셉니다. 먼저 입력에 양수만 있는지 확인하고, 작은 예제를 손으로 세어 보세요. 아래에서 완전탐색과 투 포인터의 차이, 값이 0이나 음수일 때 달라지는 조건을 비교합니다.
문제와 양수 조건

N개의 자연수로 이루어진 배열에서 합이 정확히 M인 연속 부분수열의 개수를 구합니다.
제약
1 ≤ N ≤ 100,000
1 ≤ M ≤ 100,000,000
각 원소는 1 이상 1,000 이하인 자연수
예시
N = 5, M = 5
배열 = 1 3 1 2 3
정답 구간
[1, 3, 1]
[2, 3]
출력
2
이 문제에서 가장 중요한 조건은 원소가 모두 양수라는 점입니다. 오른쪽에 원소를 더하면 합은 반드시 커지고, 왼쪽 원소를 빼면 합은 반드시 작아집니다. 이 단조성 덕분에 이미 지나온 시작점을 다시 확인하지 않아도 됩니다.
연속 부분수열은 원소를 건너뛰지 않습니다. 시작 인덱스와 끝 인덱스를 고르면 그 사이 원소를 모두 포함한 하나의 구간이 됩니다. 부분집합이나 일반 부분수열 문제와 구분해야 합니다.
직접 풀어보기
먼저 선택한 언어의 입력·반환 계약에 맞춰 작성해 보세요. 힌트 받기와 정답과 비교하기는 코드를 실행하지 않는 AI 분석입니다. 해답·해설은 아래에서 필요할 때 펼칠 수 있습니다.
AI 힌트
코드를 실행한 채점 결과가 아닌 AI의 코드 분석입니다.
해답·해설 보기
function countPositiveSubarraysSum(target, numbers) {
let count = 0, left = 0, sum = 0;
for (const value of numbers) {
sum += value;
while (sum > target) sum -= numbers[left++];
if (sum === target) count++;
}
return count;
}
countPositiveSubarraysSum(target, numbers)에 target(정수), numbers(정수 배열)를 전달하고 정수를 반환합니다. 아래 문제의 유효 입력 조건을 따릅니다. 표준 입력과 출력 코드는 작성하지 않습니다.
def countPositiveSubarraysSum(target, numbers):
count = left = total = 0
for value in numbers:
total += value
while total > target:
total -= numbers[left]
left += 1
if total == target:
count += 1
return count
countPositiveSubarraysSum(target, numbers)에 target(정수), numbers(정수 배열)를 전달하고 정수를 반환합니다. 아래 문제의 유효 입력 조건을 따릅니다. 표준 입력과 출력 코드는 작성하지 않습니다.
import java.util.*;
class Solution {
public int countPositiveSubarraysSum(int target, int[] numbers) {
int count = 0, left = 0, sum = 0;
for (int value : numbers) {
sum += value;
while (sum > target) sum -= numbers[left++];
if (sum == target) count++;
}
return count;
}
}
Solution.countPositiveSubarraysSum(int target, int[] numbers)에 target(정수), numbers(정수 배열)를 전달하고 정수를 반환합니다. 아래 문제의 유효 입력 조건을 따릅니다. 표준 입력과 출력 코드는 작성하지 않습니다.
#include <algorithm>
#include <string>
#include <vector>
#include <map>
#include <stack>
#include <stdexcept>
int countPositiveSubarraysSum(int target, const std::vector < int > & numbers) {
int count = 0, left = 0, sum = 0;
for (int value : numbers) {
sum += value;
while (sum > target) sum -= numbers[left++];
if (sum == target) count++;
}
return count;
}
countPositiveSubarraysSum(target, numbers)에 target(정수), numbers(정수 배열)를 전달하고 정수를 반환합니다. 아래 문제의 유효 입력 조건을 따릅니다. 표준 입력과 출력 코드는 작성하지 않습니다.
using System;
using System.Collections.Generic;
public class Solution {
public int Solve(int target, int[] numbers) {
int count = 0, left = 0, sum = 0;
foreach(int value in numbers) {
sum += value;
while (sum > target) sum -= numbers[left++];
if (sum == target) count++;
}
return count;
}
}
Solution.Solve(int target, int[] numbers)에 target(정수), numbers(정수 배열)를 전달하고 정수를 반환합니다. 아래 문제의 유효 입력 조건을 따릅니다. 표준 입력과 출력 코드는 작성하지 않습니다.
O(N²) 완전탐색부터 확인하기
function countByBruteForce(target, numbers) {
let count = 0;
for (let start = 0; start < numbers.length; start++) {
let sum = 0;
for (let end = start; end < numbers.length; end++) {
sum += numbers[end];
if (sum === target) count++;
if (sum > target) break; // 양수 배열에서만 가능한 조기 종료
}
}
return count;
}
완전탐색은 모든 인덱스를 시작점으로 잡고 오른쪽 끝을 하나씩 늘립니다. 구현이 직관적이고 작은 입력의 정답을 검증하는 기준 함수로 유용하지만, 최악에는 N개의 시작점마다 N개에 가까운 원소를 확인하므로 O(N²)입니다. N이 100,000이면 제출 풀이로 사용하기 어렵습니다.
처음 합을 0으로 두고 현재 원소부터 더하면 [M]처럼 단일 원소 하나가 답인 경우도 빠뜨리지 않습니다. 합이 M을 넘은 뒤 조기 종료하는 규칙은 뒤에 음수가 없다는 조건에서만 안전합니다.
O(N) 투 포인터 정확합 풀이

function countPositiveSubarraysSum(target, numbers) {
let count = 0;
let left = 0;
let sum = 0;
for (let right = 0; right < numbers.length; right++) {
sum += numbers[right];
while (sum > target) {
sum -= numbers[left++];
}
if (sum === target) count++;
}
return count;
}
console.log(countPositiveSubarraysSum(5, [1, 3, 1, 2, 3])); // 2
오른쪽 포인터는 새 원소를 창에 넣습니다. 합이 목표보다 커지면 왼쪽 원소를 하나씩 빼서 다시 M 이하로 만듭니다. 조정이 끝난 시점의 합이 M이면 현재 right에서 끝나는 정답 구간을 하나 찾은 것입니다.
모든 원소가 양수이므로 같은 right에 대해 합이 M인 시작점은 최대 하나입니다. 왼쪽을 더 옮기면 합은 반드시 작아지고, 덜 옮기면 M보다 큰 상태이기 때문입니다. 그래서 sum === target일 때 한 번만 증가시키면 됩니다.
M=5 예제 이동 추적표
| right | 추가한 값 | M 초과 조정 | 조정 후 left·sum | 누적 정답 |
|---|---|---|---|---|
| 0 | 1 | 없음 | left=0, sum=1 | 0 |
| 1 | 3 | 없음 | left=0, sum=4 | 0 |
| 2 | 1 | 없음 | left=0, sum=5 | 1: [1, 3, 1] |
| 3 | 2 | 1, 3을 차례로 제거 | left=2, sum=3 | 1 |
| 4 | 3 | 1 제거 | left=3, sum=5 | 2: [2, 3] |
이 추적에서 정답은 두 구간뿐입니다. 예시 출력은 2여야 하며, 10은 정확합 문제의 결과가 아닙니다.
왜 정확하고 O(N)인가
정확성
right가 이동할 때 합은 커집니다. 합이 M보다 큰 동안 left를 옮기면 합은 엄격하게 감소합니다. 반복이 끝나면 현재 합은 M 이하이고, 양수 조건 때문에 방금 지나친 어떤 left도 M인 구간이 될 수 없습니다. 현재 합이 M이면 그 구간 하나가 정답이고, M보다 작으면 현재 right에서 끝나는 정답 구간은 없습니다.
시간과 공간
right는 0부터 N-1까지 한 번만 이동합니다. left도 뒤로 가지 않고 최대 N번 이동합니다. 각 원소는 창에 한 번 들어오고 최대 한 번 빠지므로 전체 연산은 N에 비례해 O(N)입니다. 별도 자료구조를 만들지 않으므로 추가 공간은 O(1)입니다.
경계 테스트로 구현 검증하기
풀이 직접 실행
본문 풀이에 예제 입력을 넣고, 입력을 바꿔 계산 결과를 비교합니다.
예제 크게 보기 (새 탭)| M | 배열 | 기대값 | 확인 목적 |
|---|---|---|---|
| 5 | [1, 3, 1, 2, 3] | 2 | 본문 오류 회귀 테스트 |
| 6 | [1, 2, 1, 3, 1, 1, 1, 2] | 3 | 여러 길이의 정답 구간 |
| 3 | [1, 1, 1, 1] | 2 | 겹치는 구간 |
| 10 | [1, 2, 3] | 0 | 정답 없음 |
| 5 | [5] | 1 | 단일 원소 |
const assert = require("node:assert/strict");
const positiveCases = [
{ target: 5, numbers: [1, 3, 1, 2, 3], expected: 2 },
{ target: 6, numbers: [1, 2, 1, 3, 1, 1, 1, 2], expected: 3 },
{ target: 3, numbers: [1, 1, 1, 1], expected: 2 },
{ target: 10, numbers: [1, 2, 3], expected: 0 },
{ target: 5, numbers: [5], expected: 1 },
];
for (const testCase of positiveCases) {
assert.equal(
countPositiveSubarraysSum(testCase.target, testCase.numbers),
testCase.expected,
);
}
검증일은 2026년 7월 19일이며 Node.js v24.15.0에서 위 양수 케이스 5개를 실행했습니다. 완전탐색 함수의 결과와도 모두 대조했습니다.
0·음수가 있으면 누적합 빈도 Map
0이나 음수가 들어오면 창을 늘렸을 때 합이 항상 커지지 않습니다. 또한 0 때문에 같은 right에서 합이 M인 시작점이 여러 개일 수도 있습니다. 이때 투 포인터 한 번 카운트 방식은 정답을 놓칠 수 있으므로 누적합의 등장 횟수를 저장합니다.
function countSubarraysSum(target, numbers) {
let count = 0;
let prefix = 0;
const frequency = new Map([[0, 1]]);
for (const value of numbers) {
prefix += value;
count += frequency.get(prefix - target) ?? 0;
frequency.set(prefix, (frequency.get(prefix) ?? 0) + 1);
}
return count;
}
console.log(countSubarraysSum(1, [1, -1, 1])); // 3
console.log(countSubarraysSum(0, [0, 0])); // 3
현재 누적합이 prefix라면 이전 누적합이 prefix – target인 위치마다 그 사이 구간의 합이 target입니다. 같은 누적합이 여러 번 나올 수 있으므로 존재 여부가 아니라 빈도를 더합니다. 시작점이 0인 구간도 세기 위해 Map을 [[0, 1]]로 초기화합니다.
이 방법은 0과 음수를 포함해도 평균 O(N)에 동작하며, 서로 다른 누적합을 저장하므로 공간은 O(N)입니다. JavaScript Map API는 MDN Map 문서에서 확인할 수 있습니다.
const generalCases = [
{ target: 1, numbers: [1, -1, 1], expected: 3 },
{ target: 0, numbers: [0, 0], expected: 3 },
{ target: 3, numbers: [1, 2, 3, -3, 3], expected: 5 },
];
for (const testCase of generalCases) {
assert.equal(
countSubarraysSum(testCase.target, testCase.numbers),
testCase.expected,
);
}
합이 정확히 M과 M 이하인 문제는 다르다
양수 배열에서 합이 M 이하인 모든 구간을 세는 문제는 합을 M 이하로 줄인 뒤, 현재 right에서 끝나는 가능한 시작점 수인 right - left + 1을 누적할 수 있습니다. 그러나 이 수에는 합이 M보다 작은 구간도 들어 있습니다.
따라서 합이 정확히 M인 문제에는 그 개수 공식을 사용하지 않습니다. 이 글의 양수 풀이처럼 sum === target인 창만 한 번 세거나, 0·음수까지 허용되면 누적합 Map을 사용해야 합니다.
FAQ
Q. while 조건은 sum > M인가요, sum >= M인가요?
이 글의 정확합 구현은 sum > M인 동안만 줄인 뒤 sum === M을 검사합니다. 합이 M인 창을 검사하기 전에 제거하지 않도록 실행 순서를 함께 봐야 합니다.
Q. 양수 배열에서 같은 right로 끝나는 정답이 왜 하나뿐인가요?
왼쪽 원소를 하나 뺄 때마다 합이 반드시 작아집니다. 따라서 특정 right에서 합이 M인 시작점이 하나 나오면 그보다 오른쪽 시작점의 합은 모두 M보다 작습니다.
Q. 배열에 0만 있어도 투 포인터를 그대로 쓰면 안 되나요?
0을 빼도 합이 변하지 않아 같은 right에 여러 정답 시작점이 생길 수 있습니다. 정확한 개수를 세려면 누적합 빈도 Map이 안전합니다.
Q. 구간 자체도 반환하려면 어떻게 하나요?
양수 풀이에서 합이 target일 때 [left, right]를 저장하면 됩니다. 다만 문제에서 개수만 요구하면 구간을 저장하지 않아야 추가 공간 O(1)을 유지할 수 있습니다.
함께 풀면 좋은 문제
- 정렬된 두 배열을 합치는 투 포인터 문제: 포인터가 뒤로 가지 않는 공통 원리를 비교합니다.
- 고정 길이 최대 매출 슬라이딩 윈도우: 창의 길이가 고정된 유형과 현재 가변 길이 유형을 구분합니다.
검증 기록: 2026-07-19, Node.js v24.15.0. 양수 5개와 0·음수 포함 4개 사례를 완전탐색 결과와 대조했습니다.
이 문제를 푼 뒤 확인하기
단일 원소가 M인 경우와 양수가 아닌 값이 섞인 경우를 구분해 투 포인터의 전제를 설명해 보세요.
본문의 예제와 이 반례를 직접 실행해 예상 출력과 비교하고, 결과가 다르면 입력 조건과 반복 범위를 다시 확인하세요.
풀이 함수를 독립 실행해 확인합니다
선수 지식은 배열·조건문·반복문과 위에서 설명한 자료구조입니다. O(N) 투 포인터 정확합 풀이의 완성 함수 블록 하나를 solution.cjs로 저장하고, 그 아래에 다음 호출을 붙입니다. 앞의 비교용 풀이와 같은 이름의 함수를 함께 붙이지 않습니다. 터미널에서 node solution.cjs로 실행합니다.
console.log(JSON.stringify(countPositiveSubarraysSum(...[5, [5]])));
이 변형 호출의 JSON 출력은 1입니다. 기존 함수 블록의 console.log가 있으면 그 출력 다음 줄에서 확인합니다.
이 글이 도움이 되었나요?
이어서 공부하기
체크포인트: 해설을 가리고 다시 풀고, 경계 입력을 하나 바꾸어 예상 결과와 실제 결과를 비교하세요. 포인터를 움직여도 답을 버리지 않는 이유와 양수·0·음수의 차이를 설명하세요.
다음 추천 · 문제: 코딩테스트 해시 풀이: 모든 아나그램 찾기
권장 순서는 학습 가이드와 같습니다. 다른 순서는 접힌 참고 목록에서 확인할 수 있습니다. 읽음 표시는 숙달이나 채점 결과가 아닙니다.
원래 과정 목록 순서 (선택)
읽음 기록
읽음 — / 전체 58개 · 필수 58개
새 글 받아보기
RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.