今日は、頻度が高く、混乱しやすい3つのアルゴリズムについて説明します。これらは、サブセット、順列、および組み合わせです。これらの問題は、バックトラッキングアルゴリズムで解決できます。
1.サブセットの
問題は非常に単純です。重複する数値を含まない配列を入力し、アルゴリズムにこれらの数値のすべてのサブセットを出力するように依頼します。
vector <vector < int >>サブセット(vector < int >&nums);
たとえば、nums = [1,2,3]と入力すると、アルゴリズムは空のセットとそれ自体を含む8つのサブセットを出力するはずです。順序は異なる場合があります。
[[]、[1]、[2]、[3]、[1,3]、[2,3]、[1,2]、[1,2,3]]
最初の解決策は、数学的帰納法の考え方を使用することです。小さな副問題の結果がわかったとしたら、現在の問題の結果をどのように導き出すことができますか?
具体的には、[1,2,3]のサブセットについてお聞きしますが、[1,2]のサブセットがわかっている場合、[1,2,3]のサブセットを導出できますか?最初に[1,2]のサブセットを書き出して、見てみましょう。
[[]、[1]、[2]、[1,2]]
あなたはそのようなルールを見つけるでしょう:
サブセット([1,2,3])-サブセット([1,2])
= [3]、[1,3]、[2,3]、[1,2,3]
そして、この結果は、sebset([1,2])の結果の各セットに3を追加します。
つまり、A =サブセット([1,2])の場合、次のようになります。
サブセット([1,2,3])
= A + [A [i] .add(3)for i = 1..len(A)]
これは典型的な再帰構造です。[1,2,3]のサブセットには[1,2]を追加でき、[1,2]のサブセットには[1]を追加できます。ベースケースは明らかにつまり、入力セットが空のセットの場合、出力サブセットも空のセットになります。
コードに変換すると、簡単に理解できます。
vector <vector < int >>サブセット(vector < int >&nums){ // 基本の場合、空のセットを返す if(nums.empty())return {{}}; // 最後の要素を int n から取り出す = nums.back(); nums.pop_back(); // 先行する要素のすべてのサブセットを再帰的に計算する vector <vector < int >> res = サブセット( nums ); int size = res.size(); for(int = I 0、I <サイズ; I ++ ){ // 次に、以前の結果かけて添加は 、(RES [I])res.push_back res.back()一back(N-);. } リターンres; }
この問題の時間の複雑さの計算は比較的簡単です。再帰的アルゴリズムの時間の複雑さを計算するために使用した方法は、再帰の深さを見つけ、それを各再帰の反復回数で乗算することです。この問題では、再帰の深さは明らかにNですが、各再帰forループの反復回数はresの長さに依存し、固定されていないことがわかりました。
ちょうど今の考えによれば、resの長さは再帰ごとに2倍になるはずなので、反復の総数は2 ^ Nになるはずです。または、そのような手間をかけずに、サイズNのセットのサブセットをいくつ考えますか?2 ^ N右なので、少なくとも2 ^ N要素を解像度に追加します。
それでは、アルゴリズムの時間の複雑さはO(2 ^ N)ですか?それでも正しくない場合、2 ^ N個のサブセットがpush_backによってresに追加されるため、push_back操作の効率を考慮する必要があります。
vector <vector < int >> res = ... for(int i = 0 ; i <size; i ++ ){ res.push_back(res [i]); // O(N) res.back()。push_back(n); // O(1) }
res [i]も配列であるため、push_backはres [i]のコピーをコピーして配列の末尾に追加するため、1つの操作の時間はO(N)になります。
要約すると、合計時間の複雑さはO(N * 2 ^ N)であり、非常に時間がかかります。
スペースが複雑になるため、返された結果を格納するために使用されるスペースを計算しない場合は、O(N)の再帰的なスタックスペースのみが必要です。resに必要なスペースが計算される場合、それはO(N * 2 ^ N)である必要があります。
2番目の一般的な方法は、バックトラッキングアルゴリズムです。古いテキストのバックトラッキングアルゴリズムは、バックトラッキングアルゴリズムのテンプレートを詳細に説明しています。
=結果[] DEFバックトラック(経路選択リスト): IF 終了条件が満たされる: result.add(パス) のリターン のための選択で選択リスト: 選択する バックトラック(パス選択リスト)を 選択解除
バックトラッキングアルゴリズムのテンプレートを変更するだけです。
vector <vector < int >> res; vector <vector < int >>サブセット(vector < int >&nums){ // 通過したパスを記録 vector < int > track; backtrack(nums、0 、track); return res ; } ボイドバックトラック(ベクトル< INT > NUMSと、int型の開始、ベクトル< INT >&トラック){ res.push_back(トラック); // 注増分iが開始してから のために(INT iは=開始; I <nums.size (); i ++ ){ // 選択する track.push_back(nums [i]); // バックトラック backtrack(nums、i + 1 、track); // 選択を解除し ます track.pop_back(); } }
ご覧のとおり、resへの更新はプレオーダートラバーサルです。つまり、resはツリー内のすべてのノードです。
2.
2つの数値n、kを組み合わせると、アルゴリズムは[1..n]にk個の数値のすべての組み合わせを出力します。
ベクトル<ベクトル<整数>>結合(整数n、整数k);たとえば
、入力n = 4、k = 2、次の結果を出力、順序は関係ありませんが、繰り返しを含めることはできません(組み合わせの定義に従って、[1,2]および[ 2,1]も繰り返されます):
[
[1,2]、
[1,3]、
[1,4]、
[2,3]、
[2,4]、
[3,4]
]
これは典型的なバックトラッキングアルゴリズムです。Kはツリーの高さを制限し、nはツリーの幅を制限します。前に説明したバックトラッキングアルゴリズムのテンプレートフレームワークを適用するだけです。
vector <vector < int >> res; vector <vector < int >> bind(int n、int k){ if(k <= 0 || n <= 0)return res; vector < int > track; backtrack(n、k、1 、track); 解像度を返す; } void backtrack(int n、int k、int start、vector < int >&track){ // 到達达树的底部 if(k ==track.size()){ res.push_back(トラック); リターン; } // 音符iが開始増分から のために(INT I =開始; I <= N; I ++ ){ // 選択されない track.push_back(i)を; バックトラック(N、K、I + 1 、トラック); // 選択解除 track.pop_back(); } }
バックトラック機能は計算サブセットに似ています。違いは、解像度を更新する場所がツリーの最下部であることです。
3.順列
重複する数値を含まない配列numsを入力し、これらの数値のすべての順列を返します。
vector <vector <int >> permute(vector <int>&nums);
たとえば、入力配列[1,2,3]の場合、出力結果は次のようになります。順序は関係ありません。繰り返しはありません。
[
[1,2,3]、
[
1,3,2]、
[2,1,3]、
[2,3,1]、
[ 3,1,2]、[3,2,1]
]
バックトラッキングアルゴリズムの詳細な説明では、この問題を使用してバックトラッキングテンプレートを説明します。この問題もここにリストされています。これは、「配置」と「組み合わせ」という2つのバックトラッキングアルゴリズムのコードを比較するためのものです。
まず、トレースバックツリーを描画して確認します。
Javaコードを使用してソリューションを記述しました。
List <List <Integer >> res = new LinkedList <> (); / * メイン関数、繰り返しのない数値のグループを入力し、その完全な配置を返す* / List <List <Integer >> permute(int [] nums ) { // 「パス」を記録する LinkedList <Integer> track = new LinkedList <> (); backtrack(nums、track); return res; } void backtrack(int [] nums、LinkedList <Integer> track){ // トリガーの終わり条件付き if(track.size()== nums.length){ res.add(new LinkedList(track)); return ; } for(のInt I = 0 ; I <nums.length; I ++ ){ //は法的オプション除外 IF (track.contains(NUMS [I])) 続行; // 選択する track.add(NUMS [I])を、 // 次のレベルのディシジョンツリー バックトラックに入ります(nums 、track); // 選択を解除します track.removeLast(); } }
バックトラッキングテンプレートは変更されていませんが、順列問題と組み合わせ問題によって描かれたツリーによれば、順列問題のツリーはより対称的であり、ツリーが組み合わせ問題に近いほど、正しいノードが少なくなります。
コードで明らかになったのは、配置の問題で、containsメソッドを使用して、トラックで毎回選択された数値が除外され、組み合わせの問題が開始パラメーターで渡され、開始インデックスの前の数値が除外されることです。
上記は、順列、組み合わせ、サブセットの3つの問題の解決策です。
サブセット問題は、小さな問題の結果が既知であり、元の問題の結果を導き出す方法を前提として、数学的帰納法のアイデアを使用できます。startパラメータを使用して、選択した数値を除外するバックトラッキングアルゴリズムを使用することもできます。
組み合わせ問題はバックトラッキングのアイデアを使用しており、結果はツリー構造として表すことができます。バックトラッキングアルゴリズムテンプレートを適用するだけで済みます。重要な点は、開始点を使用して、選択された数値を除外することです。
配置の問題は遡及的な考え方であり、アルゴリズムテンプレートを適用するためのツリー構造として表現することもできます。重要な点は、containsメソッドを使用して、選択した数値を除外することです。前の記事では詳細な分析を行っています。主に、組み合わせの問題との比較を示します。
これら3つの問題については、再帰ツリーの構造を観察することにより、コードの意味を理解できます。。