元のタイトル
https://leetcode.cn/problems/combinations/
一連の考え
バックトラッキング アルゴリズムは、実際には一種の深さ優先アルゴリズムです。つまり、深さ優先アルゴリズム (通常は再帰を使用) を使用している場合、ステップの完了後に前のステップに戻る必要がある場合があり、これをバックトラッキングと呼びます。
さて、バックトラックは dfs の一種であるため、以前に dfs の手順についても説明しましたが、最も重要な手順は操作です。まず第一に、この質問の目的は組み合わせを尋ねることです。次に、k=2 と仮定すると、数字から 2 回選択してすべての結果を記録するということですか? いわゆる 1 つの操作は、実際には数字を選択することであり、組み合わせ [1, 2] と[2, 1] の組み合わせは同じ (1 つと見なされる) ため、[1, 2] を選択した場合、最初に [2] を選択した場合、2 回目に [1] を選択することはできません。単一の操作が何であるかをすでに知っているので、再帰する必要があるだけではありません。(後戻り等については後述)。
k は 2 に等しいため、分析プロセスは上の図のようになります。つまり、各小さなセットを 2 回選択する必要があります。最初の操作では、任意の数を選択できます [1 2 3 4]. 2 番目の操作では、[1 2] と [2 1] の状況を防ぐために、以前に選択したものを削除する必要があります; 選択したものを削除するには前回 (k=1)、つまりセットが [1 1] にならないようにするためです。
さて、上記の内容は先ほど書いた深さ最適化アルゴリズム(再帰)と同じです(実際、バックトラッキングアルゴリズムは一般的に再帰に基づいて実装されています)。
では、なぜバックトラッキングを使用するのでしょうか。
ここで質問を考えてみましょう。まず、サブセットを保存したいので、最初にすべてのサブセットを保存する大きなコレクションを定義し、次に [1, 2] などの選択を保存するサブセットを定義します。それでは、上記の再帰プロセスについて考えてみましょう。ここで、バックトラッキングの本質を理解するために上の図を組み合わせる必要があります。
- 最初は、サブセットは空です。【】
- 初めて再帰関数を呼び出して 1 を選択すると、このときの一時サブセットは [1] になります。
- 次に、再帰関数を再度呼び出して (つまり、2 番目の番号を選択します)、2 を選択すると、このときのセットは [1 2] になります。
- さて、この時点で 2 回選択したため (k=2 であるため、選択は完了しています)、サブセットを大きなセットに入れます。
- 大きなセットにはサブセット [1 2] があり、この時点で関数が実行され、最後の時間、つまり選択する {2 3 4} に戻ります。
- この時点でバックトラックしなければ、一時的なサブセットは [1 2] のままでしょうか? 答えはイエスです。
- ここに来た多くの初心者は、一時的なサブセットをクリアできますか?と考えるでしょう。実際には、一時的なサブセットの前に 1 を既に選択しており、2 番目のレイヤーにロールバックするため、完全に不要です.この時点では、1 は意味があるため、2 をロールバックするだけで済みます (つまり、 、操作をロールバックするには、通常、1回再帰するときに1回ロールバックする必要があります)。
- [1 2] を [1] に戻すと、{ 2 3 4 } から選択できます。これは、2 が既に選択されているためです。つまり、次のステップは 3 を選択することであり、サブセットは [ 1 3 になります。 ]。
- 上記の処理は実際には深さ優先のアルゴリズム(再帰)ですが、再帰のたびに【ロールバック】、つまり【バックトラッキング】が必要です。
- バックトラッキングもコードの中で非常に現実的で、再帰の前に要素を追加し、再帰の後に最後の要素を削除するだけです。操作は難しくありません。主なことは、考え方を理解することです。
コード
class Solution {
List<List<Integer>> all = new ArrayList<>();
List<Integer> part = new ArrayList<>();
public List<List<Integer>> combine(int n, int k) {
tCombine(n,k,0,new ArrayList());
return all;
}
public void tCombine(int n,int k,int index,List<Integer> part) {
if(k == 0){
all.add(new ArrayList(part));
}
if(index == n){
return;
}
for(int i = index;i < n;i++){
part.add(i+1);
tCombine(n,k-1,i+1,part);
part.remove(part.size()-1);
}
}
}
補充する
この質問に対して【バックトラック】と言いましたが、この質問はさらに最適化、つまり【プルーニング】によって再帰回数を減らすことができます。それについては後で話します。