歴史上最も完全な順列と組み合わせアルゴリズムの詳細な説明とルーチンの要約は、1つの記事で行われます。

プロジェクトのgithubアドレス:bitcarmanlee easy-algorithm-interview-and-practiceには、
多くの場合、学生にプライベートメッセージを送信したり、関連する質問をするためのメッセージを残したりします。V番号bitcarmanlee。githubのスターのクラスメートは、私の能力と時間の範囲内で、あなたが関連する質問に答え、一緒に進歩するのを助けるために最善を尽くします。

1.順列と組み合わせの問題

順列と組み合わせは古典的なアルゴリズムの問​​題であり、関連するコンテンツは中学校で学習されています。アルゴリズムの実装について説明する前に、順列と組み合わせの関連する定義を簡単に確認しましょう。
順列、英語名は順列、または略してPです。配列{1、2、3、4、5}があるとすると、配列内のすべての要素を並べ替える必要があります。最初の位置で、5つの数値のいずれかを選択できます。5つの選択肢があります。2番目の位置では、残りの4つの数字のいずれかを選択できます。合計4つの選択肢があります。3番目の位置では、残りの3つの数字のいずれかを選択できます。3つの選択肢があります。4番目の位置では、残りの2つの数字のいずれかを選択できます。2つの選択肢があります。最後の位置は、数字が1つしか残っていないため、選択肢がなく、選択肢が1つだけです。この場合、配列内の順列の総数は5 ∗ 4 ∗ 3 ∗ 2 ∗ 1 = 120 5 * 4 * 3 * 2 * 1 = 120です。54321=1 2 0種。
配列の要素が繰り返されず、要素の数がNの場合、上記の導出によれば、配列のすべての順列の数はN!N!であることが簡単にわかります。N !、つまり、P(N)= N!P(N)= N!P N =N

多くの場合、すべての順列を実行するわけではありません。たとえば、要素が5つある場合、並べ替えに必要なのは3つだけです。前の分析によると、順列の数が5 ∗ 4 ∗ 3であることが簡単にわかります。= 60 5 * 4 * 3 = 60543=.6 0種、後者2 1 2 * 1 *21両方のケースが破棄されました。したがって、順列のためにN個の要素からk個の要素を選択すると、式も簡単に記述できます。P(N、k)= N!(N − k)!P(N、k)= \ frac {N!} {(Nk )!}P N k =N k N

組み合わせ、英語名は組み合わせ、または略してCです。同じ配列{1、2、3、4、5}を想定すると、配列から任意の3つの要素を選択する必要があるので、いくつの方法がありますか?
前の導出によれば、5つの要素から3つの要素を選択すると、配置はP(5、3)= 5!(5 − 3)!= 60 P(5、3)= \ frac {5であることがわかります。 !} {(5-3)!} = 60P 5 3 =5 3 5 =6 0種。ただし、組み合わせる場合は順序に影響されません。たとえば、1、2、3、1、3、2を選択します。2つの配置がありますが、組み合わせでも同じ状況です。3つの要素の合計配置は合計3!= 6 3!= 63 =6種類なので、結合式はC(N、K)= N!(N − k)!K!C(N、K)= \ frac {N!} {(Nk)!k!}です。C N K =N k k N

二項定理もあります:
C(n、0)+ C(n、1)+ C(n、2)+ ...... + C(n、n)= 2 n C(n、0) + C(n、1)+ C(n、2)+ \ cdots + C(n、n)= 2 ^ nC n 0 +C n 1 +C n 2 ++C n n =2n

2.すべてのサブセット

まず、すべてのサブセットを見つける状況を見てみましょう。配列に3つの異なる要素{1、2、3}があると仮定して、配列のすべてのサブセットを見つけます。
二項定理によれば、配列のすべてのサブセットの数がC(3、0)+ C(3、1)+ C(3、2)+ C(3、3)=であることを見つけるのは難しくありません。2 3 = 8 C(3、0)+ C(3、1)+ C(3、2)+ C(3、3)= 2 ^ 3 = 8C 3 0 +C 3 1 +C 3 2 +C 3 3 =23=8

他のことは別として、最初にコードに移動し、後で特定のアイデアを分析します。

import org.apache.commons.lang3.StringUtils;

import java.util.ArrayList;

public class SubSet {

    public static int[] nums = {1, 2, 3};
    public static ArrayList<ArrayList<Integer>> result = new ArrayList<>();

    public static void subset(ArrayList<Integer> inner, int start) {
        for(int i=start; i<nums.length; i++) {
            inner.add(nums[i]);
            result.add(new ArrayList<>(inner));
            subset(inner, i+1);
            inner.remove(inner.size()-1);
        }
    }

    public static void main(String[] args) {
        ArrayList<Integer> inner = new ArrayList<>();
        result.add(inner);
        subset(inner, 0);
        for(ArrayList<Integer> each: result) {
            System.out.println(StringUtils.join(each, ","));
        }
    }
}

上記のコードの出力は次のとおりです。


1
1,2
1,2,3
1,3
2
2,3
3

正確に8つのケースがあり、出力結果を見ると、それらは私たちの期待に沿っています。

上記のソリューションは、古典的なバックトラッキングソリューションです。具体的なアイデアを分析します。
まず、3つのサブセット{1}、{1,2}、{1,2,3}をどのように構成できるかを考えますか?トラバーサルはindex = 0から始まります。このとき、要素1が内部に追加され、内部が結果に追加されるため、サブセット{1}が結果に追加されます。次に、サブセットメソッドを再帰的に呼び出し、インデックスを0 + 1 = 1に変更します。このとき、内部は{1,2}に2を追加し、同時に内部は結果に追加されるため、{ 1,2}はサブセット結合の結果です。類推すると、次の再帰呼び出しは結果に{1,2,3}を追加します。

主に
{1,2,3}のサブセットから{1,3}を取得する方法を分析するために:{1,2,3}のサブセットを取得した、この時点で、subset(inner、3)を再帰的に呼び出します。これが満たされていないforループのi <nums.lengthの条件で、呼び出しは終了します。このとき、start = 2のときにスタッキングシーンに戻り、最初にinner.remove(inner.size()-1)を実行します。この文はこの時点でinnerの最後の要素3を削除し、innerは{1、2 }。その後、start = 1のときにスタッキングシーンに戻ります。このとき、インナーの最後のエレメント2が削除され、この時点でインナーの最後のエレメント1のみが残ります。最初のstart = 0の場合、forループでのsubset(1)の呼び出しはすべて終了し、forループでのsubset(2)の実行が開始され、要素3が追加され、innerは{1,3になります。 }。類推により、すべてのサブセットが最終的に取得されます。

上記の分析プロセスは、実際には、コード内の関数がスタックをプッシュし続け、呼び出しをバックトラックするプロセスです。学生が実際にデバッグしてコード実行プロセスを確認できるようにすることをお勧めします。そうすれば、学生はより深く理解できるようになります。

3.n個の要素からk個の組み合わせを選択します

2番目の部分は、すべてのサブセットを見つけることです。サブセット要素の数を制限する場合、つまり、共通のC(n、k)C(n、k)であるn個の要素からk個の要素の組み合わせを選択します。C n k 質問。

ソリューションのアイデアは基本的に上記と同じです。最初にコードを見てください。

import org.apache.commons.lang3.StringUtils;

import java.util.ArrayList;

public class SelectK {

    public static int[] nums = {1, 2, 3};
    public static ArrayList<ArrayList<Integer>> result = new ArrayList<>();

    public static void select(ArrayList<Integer> inner, int start, int k) {
        for(int i=start; i<nums.length; i++) {
            inner.add(nums[i]);
            if (inner.size() == k) {
                result.add(new ArrayList<>(inner));
                inner.remove(inner.size()-1);
                continue;
            }
            select(inner, i+1, k);
            inner.remove(inner.size()-1);
        }
    }

    public static void main(String[] args) {
        ArrayList<Integer> inner = new ArrayList<>();
        int k = 2;
        select(inner, 0, k);
        for(ArrayList<Integer> each: result) {
            System.out.println(StringUtils.join(each, ","));
        }
    }
}

結果は次のとおりです。

1,2
1,3
2,3

すべてのサブセットを検索することとの違いは、innerの要素数がkの場合にのみ、innerが結果に追加されることです。同時に、追加後、最初に最後の要素を削除してから、このサイクルを直接終了し続けることができます。

4.n個の要素の完全な配置

以前の分析によると、繰り返されない要素はn個あり、順列の合計はn!N!です。n 種類。配列{1、2、3}を想定すると、合計順列には6つのケースがあります。

それでも最初にコードを書く

import org.apache.commons.lang3.StringUtils;

import java.util.ArrayList;

public class PermutationN {

    public static int[] nums = {1, 2, 3};
    public static ArrayList<ArrayList<Integer>> result = new ArrayList<>();

    public static void permuation(ArrayList<Integer> inner) {
        if (inner.size() == nums.length) {
            result.add(new ArrayList<>(inner));
            return;
        }

        for(int i=0; i<nums.length; i++) {
            if (inner.contains(nums[i])) {
                continue;
            }
            inner.add(nums[i]);
            permuation(inner);
            inner.remove(inner.size()-1);
        }
    }

    public static void main(String[] args) {
        ArrayList<Integer> inner = new ArrayList<>();
        permuation(inner);
        for(ArrayList<Integer> each: result) {
            System.out.println(StringUtils.join(each, ","));
        }
    }
}

コードのアイデアも分析しましょう:
1。インナーのサイズが条件を満たしている場合は、それを結果に追加して返します。
2.最初の要素からサイクルを開始します
。2.1要素が内部にある場合は、要素が訪問されたことを意味し、このサイクルが続行されます。
2.2要素が内部にない場合は、内部を追加します。
2.3permuationメソッドを再帰的に呼び出します。
2.4このpermuationメソッドが呼び出された後、innerの最後の要素を削除します。

考え方はかなり明確ですか?同様に、少しめまいがするように見える場合は、IDEでデバッグして、関数の再帰呼び出しのプロセス全体を観察することをお勧めします。

inner.contains(nums [i])の関数は、要素が訪問されたかどうかを判別することです。実際には、もう1つのより一般的な記述方法は、訪問配列を使用して要素の訪問を記録することです。以下では、visitLet'sを使用します。配列の書き方を示します。

import org.apache.commons.lang3.StringUtils;

import java.util.ArrayList;

public class PermutationN {

    public static int[] nums = {1, 2, 3};
    public static ArrayList<ArrayList<Integer>> result = new ArrayList<>();
    public static boolean[] visit = new boolean[nums.length];

    public static void permuation(ArrayList<Integer> inner, boolean[] visit) {
        if (inner.size() == nums.length) {
            result.add(new ArrayList<>(inner));
            return;
        }
        for(int i=0; i<nums.length; i++) {
            if (visit[i]) {
                continue;
            }
            visit[i] = true;
            inner.add(nums[i]);
            permuation(inner, visit);
            inner.remove(inner.size()-1);
            visit[i] = false;
        }
    }


    public static void main(String[] args) {
        ArrayList<Integer> inner = new ArrayList<>();
        permuation(inner, visit);
        for(ArrayList<Integer> each: result) {
            System.out.println(StringUtils.join(each, ","));
        }
    }
}

訪問配列を使用して、要素が訪問されたかどうかをマークします。以前のバージョンと比較して、さらに2つのステップがあります
。1。要素が訪問され、訪問配列内の位置がtrueに設定されます
。2。再帰的にバックトラックする場合、訪問配列内の位置はtrueに設定されています。falseに設定されています。

5.n繰り返される要素を持つ合計順列

上記の完全な順列の例では、配列に重複する要素はありません。配列{1、1、2}など、配列全体の配置が必要な重複要素が配列にある場合はどうすればよいですか?
言うまでもありませんが、最初にコードを取得しましょう。

import org.apache.commons.lang3.StringUtils;

import java.util.ArrayList;
import java.util.Arrays;

public class PermutationDuplicate {

    public static int[] nums = {1, 2, 1};
    public static ArrayList<ArrayList<Integer>> result = new ArrayList<>();
    public static boolean[] visit = new boolean[nums.length];

    public static void permuation(ArrayList<Integer> inner, boolean[] visit) {
        if (inner.size() == nums.length) {
            result.add(new ArrayList<>(inner));
            return;
        }

        for(int i=0; i<nums.length; i++) {
            if (visit[i]) {
                continue;
            }
            if (i > 0 && nums[i] == nums[i-1] && !visit[i-1]) {
                continue;
            }

            inner.add(nums[i]);
            visit[i] = true;
            permuation(inner, visit);
            inner.remove(inner.size()-1);
            visit[i] = false;
        }
    }

    public static void main(String[] args) {
        Arrays.sort(nums);
        ArrayList<Integer> inner = new ArrayList<>();
        permuation(inner, visit);
        for(ArrayList<Integer> each: result) {
            System.out.println(StringUtils.join(each, ","));
        }
    }
}

上記との違いに注目してください
。1。最初に配列を並べ替えて、順序を確認します。
2.2。

            if (i > 0 && nums[i] == nums[i-1] && !visit[i-1]) {
                continue;
            }

この条件は次のように理解できます。
最初のアレンジメント1,1,2が記録された後、後続のアレンジメント1,1,2が生成されます。1,1,2の2番目の配置は、2番目の1が最初にアクセスされ、最初の1が再びアクセスされることです。このとき、最初の1の訪問フラグは偽であるため、この場合、このサイクルは結果に追加せずに直接続行することもできます。

6.ルーチンの要約

上記のケースを1つずつ解決した後、順列と組み合わせの問題のルーチンを要約しましょう。

最初に配置の問題を見てください。

result = []
def permutation(路径, 选择列表):
    if 满足结束条件:
        result.add(路径)
        return
    for 选择 in 选择列表:
        做选择
        permutation(路径, 选择列表)
        撤销选择

さまざまな状況で、確認する必要があるのは、終了条件と選択方法の2つだけです。
上記のプロセスは、基本的に標準のバックトラッキング方法です。

組み合わせの問題をもう一度見てください

result = []
def permutation(路径, 选择列表):
    for 选择 in 选择列表:
        做选择
        permutation(路径, 选择列表)
        撤销选择

組み合わせルーチンは、本質的に遡及的方法の使用です。配置との違いは、結合された問題の終了条件を書き出す必要がなく、ループの結果を待つだけであるということです。

おすすめ

転載: blog.csdn.net/bitcarmanlee/article/details/114500993