部分和問題¶
重複しない要素の場合¶
Question
正整数配列 nums と目標の正整数 target が与えられたとき、要素の和が target に等しくなるすべての組合せを見つけてください。配列に重複要素はなく、各要素は複数回選択できます。これらの組合せをリスト形式で返してください。リストに重複する組合せを含めてはなりません。
例えば、入力集合 \(\{3, 4, 5\}\) と目標整数 \(9\) に対する解は \(\{3, 3, 3\}, \{4, 5\}\) です。次の 2 点に注意してください。
- 入力集合内の要素は何度でも繰り返し選択できます。
- 部分集合では要素の順序を区別しません。例えば \(\{4, 5\}\) と \(\{5, 4\}\) は同じ部分集合です。
全順列の解法を参考にする¶
全順列問題と同様に、部分集合の生成過程を一連の選択結果として捉え、選択の過程で「要素の和」を逐次更新できます。そして要素の和が target に等しくなった時点で、その部分集合を結果リストに記録します。
ただし全順列問題と異なるのは、**この問題では集合内の要素を無制限に選択できる**点です。そのため、要素がすでに選択されたかどうかを記録する selected ブール配列は不要です。全順列のコードに少し修正を加えると、まず次の解法コードが得られます。
上のコードに配列 \([3, 4, 5]\) と目標値 \(9\) を入力すると、出力は \([3, 3, 3], [4, 5], [5, 4]\) となります。和が \(9\) となる部分集合はすべて見つかっていますが、重複する部分集合 \([4, 5]\) と \([5, 4]\) が含まれています。
これは、探索過程では選択順を区別する一方で、部分集合では選択順を区別しないためです。次の図のように、先に \(4\) を選んでから \(5\) を選ぶ場合と、先に \(5\) を選んでから \(4\) を選ぶ場合は別の分岐ですが、対応する部分集合は同じです。
重複する部分集合を取り除くために、**直接的な方法として結果リストの重複を除去する**ことが考えられます。しかし、この方法は効率が低く、その理由は次の 2 点です。
- 配列要素が多い場合、特に
targetが大きい場合には、探索過程で大量の重複部分集合が生成されます。 - 部分集合(配列)同士の違いを比較するのは非常に時間がかかり、まず配列をソートし、その後に各要素を比較する必要があります。
重複部分集合の枝刈り¶
**探索過程で枝刈りを行って重複を除去する**ことを考えます。次の図を観察すると、重複部分集合は配列要素を異なる順序で選択したときに生じます。例えば次のような状況です。
- 1 回目と 2 回目でそれぞれ \(3\) と \(4\) を選ぶと、これら 2 要素を含むすべての部分集合、すなわち \([3, 4, \dots]\) が生成されます。
- その後、1 回目で \(4\) を選んだ場合、**2 回目では \(3\) をスキップすべき**です。というのも、この選択で生成される部分集合 \([4, 3, \dots]\) は、手順
1.で生成された部分集合と完全に重複するからです。
探索過程では、各階層の選択は左から右へ順に試されるため、右側にある分岐ほど多く枝刈りされます。
- 最初の 2 回で \(3\) と \(5\) を選ぶと、部分集合 \([3, 5, \dots]\) が生成されます。
- 最初の 2 回で \(4\) と \(5\) を選ぶと、部分集合 \([4, 5, \dots]\) が生成されます。
- もし 1 回目で \(5\) を選ぶなら、**2 回目では \(3\) と \(4\) をスキップすべき**です。なぜなら、部分集合 \([5, 3, \dots]\) と \([5, 4, \dots]\) は、手順
1.と手順2.で述べた部分集合と完全に重複するからです。
まとめると、入力配列 \([x_1, x_2, \dots, x_n]\) が与えられ、探索過程における選択列を \([x_{i_1}, x_{i_2}, \dots, x_{i_m}]\) とすると、この選択列は \(i_1 \leq i_2 \leq \dots \leq i_m\) を満たす必要があります。この条件を満たさない選択列は重複を生むため、枝刈りすべきです。
コード実装¶
この枝刈りを実現するために、走査の開始位置を示す変数 start を初期化します。**選択 \(x_{i}\) を行った後、次のラウンドはインデックス \(i\) から走査を開始する**ように設定します。これにより、選択列が \(i_1 \leq i_2 \leq \dots \leq i_m\) を満たし、部分集合の一意性が保証されます。
これに加えて、コードには次の 2 つの最適化も施しています。
- 探索を始める前に、まず配列
numsをソートします。すべての選択肢を走査するとき、**部分集合の和がtargetを超えたら直ちにループを終了**します。後続の要素はさらに大きいため、その和も必ずtargetを超えるからです。 - 要素和を保持する変数
totalは省略し、**targetから減算することで要素和を管理**します。targetが \(0\) になったときに解を記録します。
次の図は、配列 \([3, 4, 5]\) と目標値 \(9\) を上のコードに入力したときの、全体のバックトラッキング過程を示しています。
重複要素を考慮する場合¶
Question
正整数配列 nums と目標の正整数 target が与えられたとき、要素の和が target に等しくなるすべての組合せを見つけてください。与えられた配列には重複要素が含まれる可能性があり、各要素は 1 回しか選択できません。これらの組合せをリスト形式で返してください。リストに重複する組合せを含めてはなりません。
前問と比べると、この問題の入力配列には重複要素が含まれる可能性があります。そのため、新たな問題が生じます。例えば、配列 \([4, \hat{4}, 5]\) と目標値 \(9\) が与えられると、既存コードの出力は \([4, 5], [\hat{4}, 5]\) となり、重複部分集合が現れます。
この重複が生じる原因は、同じ値の要素があるラウンドで複数回選ばれてしまうことにあります。次の図では、1 回目には 3 つの選択肢があり、そのうち 2 つはどちらも \(4\) です。これにより 2 本の重複した探索分岐が生じ、重複部分集合が出力されます。同様に、2 回目の 2 つの \(4\) も重複部分集合を生みます。
等しい要素の枝刈り¶
この問題を解決するには、各ラウンドで等しい要素が 1 回しか選ばれないように制限する必要があります。実装方法は巧妙です。配列はすでにソートされているため、等しい要素は必ず隣り合っています。したがって、あるラウンドの選択で現在の要素が左隣の要素と等しいなら、それはすでに選ばれたことを意味するので、その要素を直接スキップします。
同時に、**この問題では各配列要素を 1 回しか選択できない**という制約もあります。幸い、この制約も変数 start を使って満たせます。すなわち、選択 \(x_{i}\) を行った後、次のラウンドはインデックス \(i + 1\) から後ろへ走査するよう設定します。これにより、重複部分集合を除去できるだけでなく、同じ要素を繰り返し選ぶことも防げます。
コード実装¶
次の図は、配列 \([4, 4, 5]\) と目標値 \(9\) に対するバックトラッキング過程を示しており、全部で 4 種類の枝刈り操作が含まれています。図とコードコメントを対応させながら、探索全体の流れと、各枝刈り操作がどのように機能するかを理解してください。




