Combination Sum
서로 다른 양의 정수로 이루어진 candidates 목록과 양의 정수 target이 주어집니다. 각 후보를 원하는 만큼 사용할 수 있을 때, 값의 합이 정확히 target이 되는 모든 조합을 찾으세요. 두 조합에 같은 값이 같은 횟수만큼 포함되어 있으면 같은 조합으로 간주하므로, [2, 3, 3]과 [3, 2, 3]은 한 번만 셉니다.
각 조합은 값을 오름차순으로 정렬해 반환하고, 조합들은 사전순으로 반환하세요. 두 조합을 왼쪽부터 값별로 비교하여 처음으로 다른 위치에서 더 작은 값이 있는 조합을 먼저 둡니다.
함수
- candidatesinteger-array
- 사용할 수 있는 서로 다른 값으로, 순서에 상관없이 원하는 만큼 각각 사용할 수 있습니다
- targetinteger
- 모든 조합의 합계가 정확히 도달해야 합니다
- 반환값integer-2d-array
- target의 합이 되는 모든 조합을 오름차순으로 나열하고, 사전순으로 정렬합니다
제약 조건
1 ≤ candidates.length ≤ 502 ≤ candidates[i] ≤ 5002 ≤ target ≤ 500candidates의 모든 값은 서로 다르며, 특정한 순서는 없습니다.- 최소 하나의 조합은
target에 도달하며, 최대 150개의 조합이 도달합니다.
예제
- 입력
- candidates = [6, 2, 3]target = 8
- 출력
- [[2, 2, 2, 2], [2, 3, 3], [2, 6]]
- 설명
- 2 네 개를 곱하면 8이고, 2 + 3 + 3과 2 + 6도 마찬가지입니다. 세 가지 모두 2로 시작하므로, 두 번째 값이 순서를 정합니다. 2, 그다음 3, 그다음 6입니다. 2가 없으면 3과 6만 남고, 이 둘을 어떻게 조합하든 3의 배수가 되는데, 8은 3의 배수가 아닙니다.
- 입력
- candidates = [5, 3, 4]target = 11
- 출력
- [[3, 3, 5], [3, 4, 4]]
- 설명
- 3 + 3 + 5와 3 + 4 + 4는 모두 11이 됩니다. 첫 번째 값은 같고, 두 번째 값에서는 3이 4보다 작으므로
[3, 3, 5]가 먼저 옵니다. 4와 5만 섞어서는 11을 만들 수 없습니다.
- 입력
- candidates = [4, 9]target = 9
- 출력
- [[9]]
- 설명
- 9 자체가 조합입니다. 4는 9를 지나가는 도중 4, 8, 12만 만들고, 4 + 9는 이미 13이므로
[9]만 정답입니다.
제출 시 숨은 테스트 +12개
후속 질문
이제 각 후보는 최대 한 번만 사용할 수 있고, candidates에는 중복된 값이 들어 있을 수 있습니다. 조합이 두 번 이상 나타나지 않도록 검색을 어떻게 바꾸면 될까요?
힌트
하나씩 열어 보세요. 열 때마다 조금 더 알려 줍니다.
[2, 3, 3]과[3, 2, 3]은 같은 조합입니다. 각 조합을 항상 값을 오름차순으로 정렬하여 만든다면, 각 조합을 만들 수 있는 방법은 몇 가지일까요?후보를 정렬하고 한 번에 하나의 값씩 조합을 늘려 나가세요.
nums[i]를 추가한 후에는 다음 값으로nums[i]를 다시 선택하거나 그보다 뒤에 있는 값을 선택할 수 있지만, 앞에 있는 값은 선택할 수 없습니다.backtrack(start, remaining)을 작성하세요.remaining이 0이면 현재 값의 복사본을 저장하세요. 그렇지 않으면start부터 반복하세요. 값을 추가하고, 같은 인덱스와 더 작은 나머지 값으로 재귀 호출한 다음, 값을 제거하세요.remaining보다 큰 첫 번째 값에서 반복을 종료하세요.
풀이
각 답은 후보들의 멀티셋이며, 함정은 같은 멀티셋을 두 번 이상 만드는 것입니다. 2를 고른 다음 3을 고르고 다시 3을 고르는 경우와 3을 고른 다음 2를 고르고 다시 3을 고르는 경우는 같은 조합에 도달합니다. 이 문제를 해결하는 방법은 각 조합을 오름차순으로 구성해 조합을 만드는 방법이 정확히 하나만 있도록 하고, 후보를 정렬해 다음 값이 남은 값보다 커지는 순간 분기를 멈추는 것입니다. 이렇게 오름차순으로 탐색하면 마지막에 정렬하지 않아도 사전식 순서로 조합을 얻을 수 있습니다.
모든 후보의 모든 개수를 시도해 보세요
정답이지만 가장 큰 테스트에서는 끝나지 않음
핵심 아이디어
조합은 각 후보를 몇 개씩 사용하는지로 완전히 설명할 수 있습니다. [6, 2, 3]에서 목표값이 8일 때, 답 [2, 3, 3]은 2 하나와 3 두 개를 사용하고 6은 사용하지 않습니다. 따라서 모든 답을 찾는 한 가지 방법은 각 후보의 가능한 개수를 모두 시도하고, 합계가 정확히 target인 선택만 남기는 것입니다. 후보 c는 최대 target / c번 들어갈 수 있으므로, 개수는 0부터 그 상한까지입니다.
후보를 정렬한 뒤 후보마다 한 단계씩 있는 결정 트리를 떠올려 보세요. i번째 단계에서는 i번째 값의 복사본을 몇 개 선택할지 결정하고, 맨 아래의 각 리프는 개수를 완전히 정한 하나의 선택입니다. 각 중복집합은 개수 목록이 정확히 하나이므로, 같은 조합이 두 번 발견되지 않습니다. 가장 큰 개수부터 시도하면 요구된 순서도 얻을 수 있습니다. 두 답이 어떤 값의 개수에서 처음 달라질 때, 그 작은 값을 더 많이 포함한 답에는 다른 답이 이미 더 큰 값을 포함하는 위치에도 그 작은 값이 남아 있으므로, 그 답이 먼저 옵니다.
문제는 트리의 크기입니다. 리프의 개수는 모든 후보에 대해 target / c + 1을 곱한 값입니다. 정렬된 [2, 3, 6]에서 목표값이 8인 경우, 답은 3개지만 리프는 5 × 3 × 2 = 30개입니다. target / 2보다 큰 후보는 최대 한 번만 들어갈 수 있는데도 리프 수를 두 배로 늘리므로, 그런 후보만 40개 있어도 리프는 2^40개, 즉 약 10^12개가 됩니다. 큰 테스트는 이런 식으로 만들어져 있어 이 방법으로는 끝까지 처리할 수 없습니다.
알고리즘
- 후보를 정렬하고 각 값마다 하나씩 개수를 저장하는 배열을 만듭니다.
- 값의 인덱스가
i인 항목의 개수를 결정하는choose(i, total)을 작성합니다. k가target / nums[i]부터 0까지 감소하도록 반복하면서 개수를k로 설정하고choose(i + 1, total + k × nums[i])을 호출합니다.- 모든 값의 개수가 정해지면
total이target과 같은 경우 조합을 보관하고, 각 값을 해당 개수만큼 출력합니다. choose(0, 0)을 호출합니다. 보관된 조합은 이미 사전순으로 정렬되어 있습니다.
def combinationSum(candidates, target):
nums = sorted(candidates)
counts = [0] * len(nums)
result = []
def choose(i, total):
if i == len(nums):
if total == target:
combo = []
for value, k in zip(nums, counts):
combo.extend([value] * k)
result.append(combo)
return
# Most copies first, so the combinations come out in lexicographic order.
for k in range(target // nums[i], -1, -1):
counts[i] = k
choose(i + 1, total + k * nums[i])
counts[i] = 0
choose(0, 0)
return result오름차순으로 백트래킹하고 가지치기하기
핵심 아이디어
각 조합은 값을 하나씩, 직접 적을 때처럼 오름차순으로 만듭니다. 시작 인덱스가 이 순서를 유지합니다. nums[i]를 넣은 다음에는 후보 값을 반복해서 사용할 수 있으므로 다음 값으로 다시 nums[i]를 선택할 수도 있고, 그보다 뒤의 값을 선택할 수도 있지만, 앞선 값은 선택할 수 없습니다. 따라서 인덱스 i를 넣은 호출은 i부터만 반복합니다. 각 조합에는 오름차순이 정확히 하나뿐이므로 트리에서도 경로가 정확히 하나이며, [3, 2, 3]과 같은 중복 조합은 만들어지지 않습니다.
정렬된 [2, 3, 6]과 target 8에 대한 전체 트리는 다음과 같습니다. 루트에는 8이 남아 있고 2, 3, 6을 시도합니다. 2 아래에는 6이 남습니다. 2, 2 아래에는 4가 남고, 2, 2, 2 아래에는 2가 남습니다. 여기에 2를 한 번 더 넣으면 정답 [2, 2, 2, 2]이 됩니다. 2, 2, 3 아래에는 1이 남으므로 더 진행할 수 없습니다. 2, 3 아래에는 3이 남아 있으며 3과 6만 시도할 수 있고, 3을 선택하면 [2, 3, 3]이 됩니다. 2, 6 아래에는 남은 값이 없습니다. 즉 [2, 6]입니다. 3 아래에서는 3과 6만 시도할 수 있으며, 3, 3 아래에는 2가 남지만 어느 값으로도 채울 수 없습니다. 6 아래에는 2가 남으며 6만 시도할 수 있습니다. 총 12번의 호출로, 첫 번째 방법에서의 30개 리프 노드와 비교됩니다.
정렬하면 막다른 경로에서 일찍 멈출 수 있습니다. nums[i]가 남은 값보다 크면 그 뒤의 모든 값도 더 크므로, 나머지를 확인하는 대신 break로 루프를 빠져나옵니다. 위 트리에서 1이 남은 2, 2, 3 노드는 3을 확인하고 들어맞지 않는다는 것을 알게 되므로 6은 확인하지 않습니다. 탐색은 합이 여전히 target 이하인 접두 경로만 방문합니다. 그래서 첫 번째 방법을 막히게 하는 큰 테스트도 여기서는 몇천 번의 호출만으로 처리됩니다.
출력 순서도 같은 순회에서 나옵니다. 각 단계에서 루프는 더 작은 값부터 시도하며 모든 조합은 오름차순으로 작성됩니다. 두 답은 경로가 갈라지는 단계에서 처음 달라지며, 그 단계에서 더 작은 값을 선택한 경로를 먼저 탐색하므로 답은 사전식 순서로 나옵니다. 값이 양수이고 두 조합의 합이 같으므로 한 조합이 다른 조합의 접두사가 될 수는 없습니다.
알고리즘
- 후보를 오름차순으로 정렬합니다.
- 하나의 목록
path를 공유하는backtrack(start, remaining)을 작성합니다.remaining이 0이면path의 복사본을 저장합니다. - 그렇지 않으면
i를start부터 끝까지 반복합니다.nums[i] > remaining이면 중단합니다. 뒤에 오는 모든 값이 더 크기 때문입니다. nums[i]를 추가하고, 값이 반복될 수 있도록i + 1이 아니라i를 사용해backtrack(i, remaining-nums[i])을 호출한 다음, 해당 값을 제거합니다.backtrack(0, target)을 호출하고, 이미 사전식 순서로 정렬된 저장된 조합을 반환합니다.
def combinationSum(candidates, target):
nums = sorted(candidates)
result = []
path = []
def backtrack(start, remaining):
if remaining == 0:
result.append(path[:])
return
for i in range(start, len(nums)):
if nums[i] > remaining:
break # sorted, so every later value is too big as well
path.append(nums[i])
backtrack(i, remaining - nums[i]) # i, not i + 1: nums[i] may repeat
path.pop()
backtrack(0, target)
return result
함정과 경계 사례
대부분의 오답은 산술이 아니라 탐색 순서에서 비롯됩니다.
- 현재 인덱스부터가 아니라 각 단계에서 모든 후보를 반복하면
[2, 3, 3],[3, 2, 3],[3, 3, 2]가 세 가지 답으로 만들어집니다. 각 답을 정렬하고 나중에 중복을 제거하면 올바른 목록을 얻을 수 있지만, 기하급수적으로 더 많은 작업을 하게 됩니다. i대신i + 1로 재귀 호출하면 각 값이 한 번만 나타날 수 있으므로[2, 2, 2, 2]가 누락됩니다.- 복사본 대신
path자체를 저장하면 저장된 모든 답이 같은 목록이 되고, 백트래킹이 끝날 때쯤에는 그 목록이 비워져 있습니다. - 정렬하지 않은 후보에
break를 사용하는 경우입니다.[6, 2, 3]에서 남은 값이 2라면, 반복문은 6에서 멈춰 2를 시도하지 않습니다. - 정렬되지 않은 입력 순서가 제시하는 순서대로 조합을 반환하는 경우입니다. 예상 목록은 사전식 순서이며, 정렬된 탐색을 사용하면 추가 정렬 없이 이 순서를 얻을 수 있습니다.
- Lua와 R에서는 배열 인덱스가 1부터 시작하므로 첫 호출은 인덱스 1에서 시작하고 반복문은 배열의 길이까지 실행됩니다.
자주 묻는 질문4
Combination Sum의 시간 복잡도는 얼마인가요?
백트래킹 검색은 지수 시간입니다. 후보가 n개이고, 목표값이 t이며 가장 작은 후보가 m일 때, 조합에는 최대 t/m개의 값이 들어가고 각 단계에는 최대 n개의 선택지가 있으므로 작업량은 O(n^(t/m))으로 제한됩니다. 정렬된 후보를 기준으로 가지치기를 하면 실제 호출 횟수는 이보다 훨씬 적습니다. 검색은 합이 여전히 t 이하인 접두사만 방문하기 때문입니다. 추가 공간은 현재 경로와 호출 스택에 O(t/m)이 필요하며, 여기에 출력 공간이 더해집니다.
Combination Sum에서 왜 i + 1이 아니라 i를 사용해 재귀 호출하나요?
i로 재귀 호출하면 다음 값이 다시 같은 후보가 될 수 있으므로, 값이 두 번 이상 사용될 수 있습니다. i + 1로 재귀 호출하면 해당 후보를 지나가므로, 각 후보를 최대 한 번만 사용하는 변형 문제로 바뀝니다. 규칙의 나머지 절반도 그만큼 중요합니다. i보다 앞선 인덱스로 절대 돌아가지 않으면 모든 조합이 오름차순으로 유지되고 중복이 방지됩니다.
집합 없이 중복 조합을 어떻게 피할 수 있나요?
고정된 순서로 오름차순으로 모든 조합을 생성합니다. 시작 인덱스가 이를 강제합니다. nums[i]를 배치한 후 검색은 nums[i]와 그 이후의 값만 살펴봅니다. 그러면 모든 조합은 검색 트리에서 정확히 하나의 경로만 가지므로 한 번만 생성되며, 집합이나 마지막 중복 제거가 필요하지 않습니다.
Combination Sum을 동적 프로그래밍으로 풀 수 있나요?
네. 0부터 target까지 각 합계에 도달하는 조합 목록을 저장하고, 한 번에 후보 하나씩 추가해 각 목록의 값이 오름차순을 유지하게 합니다. 거스름돈을 만드는 방법을 세는 것과 같은 방식입니다. 막다른 길을 두 번 탐색하지는 않지만, 모든 합계에 대해 모든 부분 조합을 저장하므로 백트래킹보다 훨씬 많은 메모리가 필요하고, 최종 목록을 정렬해야 할 수도 있습니다. 출력 자체가 지수적으로 커질 수 있으므로 일반적으로는 백트래킹을 사용합니다.
비슷한 문제
같은 아이디어를 쓰는 문제입니다. 두세 개를 풀면 패턴이 몸에 익습니다.
Python
def combinationSum(candidates, target):
# 여기에 코드를 작성하세요케이스 1
케이스 2
케이스 3
입력
candidates = [6, 2, 3] target = 8
기대값
[[2, 2, 2, 2], [2, 3, 3], [2, 6]]