JavaScript 투 포인터: 합이 M인 연속 부분수열 개수

2026.05.07·수정 2026.09.13·약 12분·작성: 해비·블로그 소개

핵심 답부터 보기

모든 원소가 양수라면 오른쪽 포인터로 합을 늘리고, 합이 M보다 클 때만 왼쪽 포인터를 옮깁니다. 조정이 끝난 뒤 합이 정확히 M일 때 한 번 세면 전체를 O(N)에 구할 수 있습니다. M=5, [1, 3, 1, 2, 3]의 정답은 [1, 3, 1]과 [2, 3], 총 2개입니다. 0이나 음수가 들어오면 이 단조성이 깨지므로 누적합 빈도 Map으로 전환해야 합니다.

문제와 양수 조건

코딩테스트 JS 투 포인터 풀이: 연속부분수열 목표 합 찾기 전반부 흐름 정리
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

이 문제에서 가장 중요한 조건은 원소가 모두 양수라는 점입니다. 오른쪽에 원소를 더하면 합은 반드시 커지고, 왼쪽 원소를 빼면 합은 반드시 작아집니다. 이 단조성 덕분에 이미 지나온 시작점을 다시 확인하지 않아도 됩니다.

연속 부분수열은 원소를 건너뛰지 않습니다. 시작 인덱스와 끝 인덱스를 고르면 그 사이 원소를 모두 포함한 하나의 구간이 됩니다. 부분집합이나 일반 부분수열 문제와 구분해야 합니다.

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) 투 포인터 정확합 풀이

코딩테스트 JS 투 포인터 풀이: 연속부분수열 목표 합 찾기 후반부 흐름 정리
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개 사례를 완전탐색 결과와 대조했습니다.

풀이 전에 확인할 순서

  1. 입력값과 출력값을 한 문장으로 다시 적습니다.
  2. 반복할 대상과 비교·저장할 값을 정합니다.
  3. 필요한 자료구조와 시간복잡도를 예상합니다.
  4. 코드를 보기 전에 손으로 작은 예제를 계산합니다.
힌트: JavaScript 투 포인터: 합이 M인 연속 부분수열 개수

정답 코드를 바로 따라 쓰기보다, 본문에서 값이 갱신되는 조건과 반복 범위를 먼저 찾으세요. 반복 한 번마다 반드시 유지되어야 하는 값이 무엇인지 적으면 풀이의 중심 변수를 고르기 쉽습니다.

테스트 확인

  • 가능한 가장 작은 입력
  • 같은 값이나 문자가 반복되는 입력
  • 정답이 처음 또는 마지막 위치에서 결정되는 입력
  • 입력 제한에 가까운 경우의 실행 시간

확인 결과: 본문의 예제뿐 아니라 위 경계 사례에서도 예상값과 실제 출력이 같아야 풀이가 완료됩니다.

이 글이 도움이 되었나요?

조회 중

코딩테스트 JavaScript 학습 순서

필수 49개 · 전체 49개

읽음 기록 관리

전체 과정 목차 (49개)
  1. 필수 길잡이 · 코딩테스트 JS 자료구조 로드맵: 배열, 해시, 스택, 투 포인터 순서
  2. 필수 학습 · 세 수 중 최솟값 JavaScript 조건문 풀이 정리
  3. 필수 학습 · 삼각형 판별하기 JavaScript 풀이
  4. 필수 학습 · 연필 개수 JavaScript 풀이
  5. 필수 학습 · 1부터 N까지 합 출력하기 JavaScript 풀이
  6. 필수 학습 · 최솟값 구하기 JavaScript 풀이|배열 순회와 비교 갱신 원리
  7. 필수 학습 · 홀수 JavaScript 풀이: 조건 판별과 결과 처리 정리
  8. 필수 학습 · 10부제 JavaScript 풀이: 끝자리 비교로 위반 차량 수 세기
  9. 필수 학습 · A를 #으로 JavaScript 풀이: 문자열 순회와 치환
  10. 필수 학습 · 문자 찾기 JavaScript 풀이: 문자열 순회로 개수 세기
  11. 필수 학습 · 대문자 찾기 JavaScript 풀이
  12. 필수 학습 · 대문자로 통일 JavaScript 풀이
  13. 필수 학습 · 대소문자 변환 JavaScript 풀이
  14. 필수 학습 · 일곱 난쟁이 JavaScript 풀이: 두 명을 제외하는 완전탐색
  15. 필수 학습 · 코딩테스트 JS Map 풀이: 학급 회장 득표수 세기
  16. 필수 학습 · 코딩테스트 JS 스택 풀이: 올바른 괄호 검증하기
  17. 필수 학습 · 코딩테스트 JS 스택 풀이: 괄호문자 제거하기
  18. 필수 학습 · 코딩테스트 JS 스택 풀이: 크레인 인형뽑기 처리법
  19. 필수 학습 · 코딩테스트 JS 스택 풀이: 후위식 연산 계산하기
  20. 필수 학습 · 코딩테스트 JS 스택 풀이: 쇠막대기 레이저 절단 개수 세기
  21. 필수 학습 · 코딩테스트 JS 투 포인터 풀이: 두 정렬 배열 합치기
  22. 필수 학습 · 코딩테스트 JS 투 포인터 풀이: 공통 원소 추출하기
  23. 필수 학습 · 코딩테스트 JS 슬라이딩 윈도우 풀이: 최대 매출 구간 합 계산하기
  24. 필수 학습 · JavaScript 투 포인터: 합이 M인 연속 부분수열 개수 현재 글
  25. 필수 학습 · 코딩테스트 JS 해시 풀이: 모든 아나그램 찾기
  26. 필수 학습 · 가장 긴 문자열 JavaScript 풀이
  27. 필수 학습 · 가운데 문자 출력 JavaScript 풀이
  28. 필수 학습 · 중복문자제거 JavaScript 풀이
  29. 필수 학습 · 코딩테스트 JS 고급: 최소 힙으로 다익스트라 최단 경로 구하기
  30. 필수 학습 · 코딩테스트 JS Union-Find: 연결 성분 수와 크기 구하기
  31. 필수 학습 · 코딩테스트 JS Trie: 접두사에 맞는 단어 수 세기
  32. 필수 학습 · 코딩테스트 JS Fenwick Tree: 값 갱신과 구간 합 처리
  33. 필수 학습 · 코딩테스트 JS 세그먼트 트리: 단일 대입과 구간 합
  34. 필수 학습 · 코딩테스트 JS LRU 캐시: 지도 타일 재사용 기록
  35. 필수 학습 · 코딩테스트 JS AVL 트리: 기준 이상 최솟값 찾기
  36. 필수 학습 · 코딩테스트 JS 큐: 상담 창구 대기열 명령 처리
  37. 필수 학습 · 코딩테스트 JS 연결 리스트: 재생 대기 목록 관리
  38. 필수 학습 · JavaScript 원형 덱 연습: 최근 기록 창과 되돌리기
  39. 필수 학습 · JavaScript 해시 테이블 연습: 정규화 문자열 빈도와 등장 순서
  40. 필수 학습 · JavaScript 트리 순회 연습: 깊이별 노드 묶기
  41. 필수 학습 · JavaScript BST 연습: 닫힌 구간의 중복 키 보고서
  42. 필수 학습 · JavaScript 최소 힙 연습: 동률 순서를 지키는 작업 스케줄러
  43. 필수 학습 · JavaScript 그래프 연습: 연결 구역 크기를 작은 순서로 출력하기
  44. 필수 학습 · 중복단어제거 JavaScript 풀이
  45. 필수 학습 · TypeScript 이진 탐색 연습: 숫자 카드 존재 여부 확인
  46. 필수 학습 · 큰 수 출력하기 JavaScript 풀이
  47. 필수 학습 · 보이는 학생 JavaScript 풀이
  48. 필수 학습 · 가위바위보 JavaScript 풀이
  49. 필수 학습 · 점수계산 JavaScript 풀이

새 글 받아보기

RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.

RSS 피드 구독하기

댓글 남기기