バックトラックで 1 つずつしか 使えない 数から 足すと 目標に なる組を 全部集める
同じ
海外で定番の問題リスト 4 本には入っていません
問題
財布にcandidates と、target をtarget に
- 額面の
組が 同じ物は 1 回だけ 入れます。 たとえば 額面 1 の 券が 2 枚ある とき、 どちらの 1 を 使っても [1, 4]は 1 つの組です - 組の
中の 額面は、 小さい 順に並べます - 組どうしの
順は 辞書の 順です。 前の 額面から 比べ、 小さい方を 先にします - 組が 1 つも
無ければ、 空の 並び []を返します candidatesの長さは 1 以上 30 以下、 額面は どれも 1 以上 50 以下の 整数です。 targetは 1 以上 30 以下の整数です
入力と出力の例
例 1
並べ替えると 1、
例 2
6 の[6, 6] の 1 つだけです。
例 3
どの
面接での聞かれ方
面接では、
- 「重複を
含む数の 配列と 目標の 値が あります。 各数を 1 回だけ 使って、 和が 目標に なる 組み合わせを、 重複なく 全部返してください」 - 同じ数を
何回でも 使える 版 (組み合わせの 和) の あとに、 続けて 出される ことが 多い 問題です。 「前の 問題と、 どこを 変えましたか?」と 聞かれます - 「重なりを
集合で 消さずに、 作る 前に 防げますか?」と 聞かれます
同じ数を
ヒント
まだ開いていません
付箋は 4 枚。下の段ほど答えに近づきます
ヒント 2
ヒント 3
ヒント 4答えに近い
アルゴリズムのチートシートの「再帰と木」に、
関わる学び
再帰は、
まず 考える手順
段 1
全部の
段 2
まずi + 1 からです (何回でもi からでした)。
段 3
重なりは、[1, 1, 3] は
段 4
並べ替えてあるので、
解答
Python
Python
def combination_sum_once(
candidates, target
):
a = sorted(candidates)
n = len(a)
out = []
path = []
def dfs(s, left):
if left == 0:
out.append(path[:])
return
# この段で最後に選んだ額面
prev = None
for i in range(s, n):
x = a[i]
if x > left:
break
if x == prev:
continue
prev = x
path.append(x)
dfs(i + 1, left - x)
path.pop()
dfs(0, target)
return outJavaScript
JavaScript
function combinationSumOnce(
candidates, target
) {
const a = [...candidates];
a.sort((p, q) => p - q);
const out = [];
dfs(a, 0, target, [], out);
return out;
}
function dfs(
a, s, left, path, out
) {
if (left === 0) {
out.push([...path]);
return;
}
// この段で最後に選んだ額面
let prev = null;
const n = a.length;
for (let i = s; i < n; i++) {
const x = a[i];
if (x > left) {
break;
}
if (x === prev) {
continue;
}
prev = x;
path.push(x);
dfs(a, i + 1, left - x,
path, out);
path.pop();
}
}Go
Go
import "sort"
func combinationSumOnce(
candidates []int, target int,
) [][]int {
a := []int{}
a = append(a, candidates...)
sort.Ints(a)
n := len(a)
out := [][]int{}
path := []int{}
var dfs func(s, left int)
dfs = func(s, left int) {
if left == 0 {
p := []int{}
p = append(p, path...)
out = append(out, p)
return
}
// この段で最後に選んだ額面
prev := 0
for i := s; i < n; i++ {
x := a[i]
if x > left {
break
}
if x == prev {
continue
}
prev = x
path = append(path, x)
dfs(i+1, left-x)
path = path[:len(path)-1]
}
}
dfs(0, target)
return out
}Go の prev のpath は 1 本を
計算量
- 時間は O(2^n × n) です。
それぞれの 券を 使うか 使わないかで 2^n 通りまでの 枝が あり、 組を 写すのに n まで かかります。 実際には、 残りの 額を 超えた所で 枝を 切るので、 ずっと 少なく済みます - 使う
記憶は、 答えを 除くと O(n) です。 再帰の 深さと、 ここまでの 組の 長さが n までです
よく ある間違い
- 次の
位置を iから続ける。 同じ券を何回でも 使える ことに なり、 例 2 で [6, 6]だけでなく、6 の 券が 1 枚でも 12 が 作れてしまいます。 1 回ずつなので i + 1からです - 並べ替えずに、
同じ 額面を飛ばす。 同じ 額面が 離れていると 見逃し、 例 1 で [1, 4]が 2 回入ります - 「最後に選んだ額面」を
段を またいで持つ。 深い段で 2 枚目の 1 を 選べなくなり、 [1, 1, 3]が作れません。 段 (再帰の 1 回) ごとに 用意します - 大きすぎる
額面で continueする。 並べ替えてあるので、そこから 先は 全部大きすぎます。 breakで抜けると 速くなります
解答を動かした版 Python 3.14.7Node.js 24.22.1Go 1.24.2確認日 2026年11月8日
面接での 話し方の例
書き始める
「まず
並べ替えます。 次に 使う数を、いまの 位置より 後ろから 選ぶ再帰にし、 選んだら 残りの 額を 減らして、 次の 位置から 続けます。 1 回ずつしか 使えないので i + 1 からです。 同じ段で 最後に 選んだ値と 同じ値は 飛ばして、 同じ組を 2 回作らないようにします。 残りの 額より 大きい値が 来たら 抜けます。」
書き終えたら、
「例 1 を
並べ替えると 1、 1、 2、 3、 4 です。 頭の 段で 1 を 選ぶと 残りは 4 です。 次の 段で 2 枚目の 1 を 選び残り 3、 その次で 2 は 残り 1 で 届かず、 3 で 0 に なって [1, 1, 3] です。 戻って、 次の 段で 4 を 選ぶと [1, 4] です。 頭の 段の 2 枚目の 1 は 飛ばし、 2 を 選ぶと 3 で [2, 3] です。」