A detailed explanation of the most complete permutation and combination algorithm in history and a summary of routines are done in one article

Project github address: bitcarmanlee easy-algorithm-interview-and-practice
often have students private messages or leave messages to ask related questions, V number bitcarmanlee. The classmates of star on github, within the scope of my ability and time, I will try my best to help you answer related questions and make progress together.

1. Permutation and combination problem

Permutation and combination is a classic algorithm problem, and the related content has been learned in middle school. Before talking about the algorithm implementation, let's briefly review the related definitions of permutation and combination.
Permutation, the English name is Permutation, or P for short. Suppose there is an array {1, 2, 3, 4, 5}, we need to sort all the elements in the array, then at the first position, we can choose any one of the five numbers, there are 5 choices. In the second position, you can choose any one of the remaining four numbers. There are 4 choices in total. In the third position, you can choose any one of the remaining three numbers. There are 3 choices. In the fourth position, you can choose any one of the remaining two numbers. There are 2 choices. The last position, because there is only one number left, there is no choice, there is only one choice. Then the total number of permutations in the array is 5 ∗ 4 ∗ 3 ∗ 2 ∗ 1 = 120 5*4*3*2*1=12054321=. 1 2 0 species.
If the elements of the array are not repeated and the number of elements is N, according to the above derivation, it is easy to get that the number of all permutations of the array isN! N!N !, That is,P (N) = N! P(N) = N!P(N)=N!

Many times we don’t do all permutations. For example, if there are 5 elements, we only need to take 3 for sorting. According to the previous analysis, it is easy to know that the number of permutations is 5 ∗ 4 ∗ 3 = 60 5*4*3=60543=. 6 0 species, the latter2 1 2 * 1 *21 Both cases were discarded. Therefore, choosing k elements from N elements for permutation, the formula is also easy to write:P (N, k) = N! (N − k)! P(N, k) = \frac{N!}{( Nk)!}P(N,k)=(Nk)!N!

Combination, the English name is Combination, or C for short. Assuming the same array {1, 2, 3, 4, 5}, we need to select any 3 elements from the array, so how many ways are there?
According to the previous derivation, we can know that if 3 elements are selected from 5 elements, the arrangement is P (5, 3) = 5! (5 − 3)! = 60 P(5, 3) = \ frac{5!}{(5-3)!} = 60P(5,3)=(53)!5!=6 0 species. But when combining, it is not sensitive to the order. For example, we choose 1, 2, 3 and 1, 3, 2. Although there are two arrangements, it is the same situation in the combination. The total arrangement of 3 elements totals3! = 6 3! = 63!=6 types, so the combined formula isC (N, K) = N! (N − k)! K! C(N,K) =\frac{N!}{(Nk)!k!}C(N,K)=(Nk)!k!N!

There is also the binomial theorem:
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. All subsets

First, let's look at the situation of finding all the subsets: assuming that the array now has three distinct elements {1, 2, 3}, find all the subsets of the array.
According to the binomial theorem, it is not difficult to find that the number of all subsets of the array is 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

Aside from anything else, go to the code first, and then analyze the specific ideas later.

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, ","));
        }
    }
}

The output of the above code is


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

There are exactly 8 cases, and looking at the output results, they are in line with our expectations.

The above solution is a classic backtracking solution. Analyze the specific ideas:
First, think about how we can make up the three subsets {1}, {1,2}, {1,2,3}? The traversal starts from index=0. At this time, element 1 is added to the inner and inner is added to the result, so that the subset {1} is added to the result. Next, call the subset method recursively, just change the index to 0+1=1. At this time, the inner adds 2 to {1,2}, and the inner is added to the result at the same time, so that {1,2} is Subset join results. By analogy, the next recursive call will add {1,2,3} to the result.

Mainly to analyze how to get {1,3}
from the subset of {1,2,3} : After obtaining the subset of {1,2,3}, at this time, we recursively call subset(inner, 3), which is not satisfied The condition of i<nums.length in the for loop, the call ends. At this time, return to the stacking scene when start=2, first execute inner.remove(inner.size()-1); this sentence will delete the last element 3 of inner at this time, and inner is {1, 2 }. Then, it will return to the stacking scene when start=1. At this time, the last element 2 in the inner will be deleted, and at this time, only the last element 1 of the inner will be left. When the initial start=0, the call to subset(1) in the for loop has all ended, and the execution of subset(2) in the for loop is started, element 3 will be added, and inner becomes {1,3}. By analogy, all the subsets will eventually be obtained.

The above analysis process is actually the process in which the functions in the code keep pushing the stack and then backtracking the call. It is recommended that students can actually debug and take a look at the code running process, and they will have a deeper understanding.

3. Select k combinations from n elements

The second part is to find all the subsets. If we limit the number of subset elements, that is, select k element combinations from n elements, which is the common C (n, k) C(n, k)C(n,k ) Question.

The solution idea is basically the same as above, first look at the code.

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, ","));
        }
    }
}

The result is:

1,2
1,3
2,3

The difference from seeking all subsets is that only when the number of elements in inner is k, inner is added to result. At the same time, after adding, delete the last element first, and then you can directly continue to end this cycle.

4. Full arrangement of n elements

According to our previous analysis, there are n non-repeated elements, and the total permutation is n! N!n ! Kind. Assuming the array {1, 2, 3}, there are 6 cases in total permutation.

Still code first

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, ","));
        }
    }
}

Let's also analyze the idea of ​​the code:
1. If the size of the inner meets the condition, add it to the result and return.
2. Start the cycle from the first element:
2.1 If the element is in the inner, it means that the element has been visited, and this cycle continues.
2.2 If the element is not in inner, add inner.
2.3 Call the permuation method recursively.
2.4 After this permuation method is called, delete the last element in inner.

Is the thinking fairly clear? Similarly, if it looks a little dizzy, I suggest you go to debug in the IDE and observe the entire process of function recursive calling.

The function of inner.contains(nums[i]) is to determine whether the element has been visited. In practice, another more common way of writing is to use a visit array to record the visit of the element. Below we use visit Let's show how to write an array.

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, ","));
        }
    }
}

Use the visit array to mark whether the element has been visited. Compared with the previous version, there are two more steps:
1. The element is visited, and the position in the visit array is set to true;
2. When recursively backtracking, the position in the visit array is set to true. Is set to false.

5.n total permutations with repeated elements

In the full permutation example above, there are no duplicate elements in the array. What should I do if there are duplicate elements in an array, such as the array, {1, 1, 2}, which requires the entire arrangement of the array?
Not much to say, let's get the code first.

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, ","));
        }
    }
}

Focus on the differences from the above:
1. Sort the array first to ensure order.
2.

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

This condition can be understood as follows:
after the first arrangement 1,1,2 is recorded, a subsequent arrangement of 1,1,2 will be generated. The second arrangement of 1,1,2 is that the second 1 is accessed first, and the first 1 is accessed again. At this time, the visit flag of the first 1 is false, so in this case, this cycle can also directly continue without adding to the result!

6. Routine summary

After solving the above cases one by one, let's summarize the routines of permutation and combination problems.

Look at the arrangement problem first:

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

For different situations, there are only two points that need to be confirmed: end conditions and how to choose.
The above process is essentially a standard backtracking method.

Look at the combination problem again

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

Combination routines are essentially the use of retrospective methods. The difference from the arrangement is that the end condition of the combined problem does not need to be written out, just wait for the loop result.

Guess you like

Origin blog.csdn.net/bitcarmanlee/article/details/114500993