【Turn】 Retrospecting the problem of permutation, combination and subset of thought groups

Today I will talk about three algorithms that have a high frequency and are easy to confuse. They are subset, permutation, and combination. These problems can be solved with backtracking algorithm.

1. The subset
problem is very simple. Enter an array that does not contain duplicate numbers, and ask the algorithm to output all subsets of these numbers.

vector<vector<int>> subsets(vector<int>& nums);


For example, input nums = [1,2,3], your algorithm should output 8 subsets, including the empty set and itself, the order can be different:

[ [],[1],[2],[3],[1,3],[2,3],[1,2],[1,2,3] ]

The first solution is to use the idea of ​​mathematical induction: Suppose I now know the results of a smaller sub-problem, how can I derive the results of the current problem?

Specifically, let me ask you for the subset of [1,2,3]. If you know the subset of [1,2], can you derive the subset of [1,2,3]? First write out the subset of [1,2] and take a look:

[ [],[1],[2],[1,2] ]

You will find such a rule:

subset([1,2,3]) - subset([1,2])

= [3],[1,3],[2,3],[1,2,3]

And this result is to add 3 to each set in the result of sebset ([1,2]).

In other words, if A = subset ([1,2]), then:

subset([1,2,3])

= A + [A[i].add(3) for i = 1..len(A)]

This is a typical recursive structure. The subset of [1,2,3] can be appended by [1,2], the subset of [1,2] can be appended by [1], the base case is obviously That is, when the input set is an empty set, the output subset is also an empty set.

Translated into code, it is easy to understand:

vector <vector < int >> subsets (vector < int > & nums) {
 // base case, return an empty set 
if (nums.empty ()) return {{}};
 // take the last element out 
int n = nums.back (); 
nums.pop_back (); 
// Recursively calculate all subsets of the preceding elements 
vector <vector < int >> res = subsets (nums); 

int size = res.size ();
 for ( int i = 0 ; i <size; i ++ ) {
 // Then add 
res.push_back (res [i]); 
res.back (). push_back (n); 
} 
return res;
}

 

The calculation of the time complexity of this problem is relatively easy. The method we used to calculate the time complexity of the recursive algorithm is to find the depth of the recursion and then multiply it by the number of iterations in each recursion. For this problem, the recursion depth is obviously N, but we found that the number of iterations of each recursive for loop depends on the length of res and is not fixed.

According to the idea just now, the length of res should double every recursion, so the total number of iterations should be 2 ^ N. Or without such hassle, how many subsets of a set of size N do you think? 2 ^ N right, so at least add 2 ^ N elements to res.

So is the time complexity of the algorithm O (2 ^ N)? Still not right, 2 ^ N subsets are added by push_back to res, so the efficiency of push_back operation should be considered:

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)
}

Because res [i] is also an array, push_back copies a copy of res [i] and adds it to the end of the array, so the time for one operation is O (N).

In summary, the total time complexity is O (N * 2 ^ N), which is quite time-consuming.

For space complexity, if you do not calculate the space used to store the returned result, only O (N) recursive stack space is required. If the space required for res is calculated, it should be O (N * 2 ^ N).

The second general method is the backtracking algorithm. Old text backtracking algorithm explained in detail the template of the backtracking algorithm:

result = [] 
def backtrack (path, selection list): 
if the end condition is met: 
result.add (path) 
return 
for selection in selection list: 
make selection 
backtrack (path, selection list) to 
cancel selection


Just modify the template of the backtracking algorithm:

vector <vector < int >> res; 

vector <vector < int >> subsets (vector < int > & nums) {
 // Record the path traversed 
vector < int > track; 
backtrack (nums, 0 , track);
 return res ; 
} 

void backtrack (vector < int > & nums, int start, vector < int > & track) { 
res.push_back (track); 
// Note that i increments from start 
for ( int i = start; i <nums.size (); i ++ ) {
 // make a choice
track.push_back (nums [i]);
 // backtracking 
backtrack (nums, i + 1 , track);
 // deselect selection 
track.pop_back (); 
} 
}

 

As you can see, the update to res is a preorder traversal, that is, res is all the nodes in the tree:

 

 

 

2. Combine
two numbers n, k, and the algorithm outputs all combinations of k numbers in [1..n].

vector <vector <int >> combine (int n, int k); For
example, input n = 4, k = 2, output the following results, the order does not matter, but can not contain repetition (according to the definition of combination, [1,2] and [ 2,1] is also repeated):

[
[1,2],
[1,3],
[1,4],
[2,3],
[2,4],
[3,4]
]

This is a typical backtracking algorithm. K limits the height of the tree, and n limits the width of the tree. Just apply the template framework of the backtracking algorithm we talked about before:

 

 

 

vector<vector<int>>res;

vector<vector<int>> combine(int n, int k) {
if (k <= 0 || n <= 0) return res;
vector<int> track;
backtrack(n, k, 1, track);
return res;
}

void backtrack(int n, int k, int start, vector<int>& track) {
// 到达树的底部
if (k ==track.size ()) { 
res.push_back (track); 
return ; 
} 
// Note that i increments from start 
for ( int i = start; i <= n; i ++ ) {
 // make choices 
track.push_back (i) ; 
BackTrack (n-, K, I + . 1 , Track);
 // deselection 
track.pop_back (); 
} 
}

 

The backtrack function is similar to the calculation subset, the difference is that the place to update res is the bottom of the tree.

3. Permutation
Enter an array nums that does not contain duplicate numbers, and return all permutations of these numbers.

vector <vector <int >> permute (vector <int> & nums);
For example, input array [1,2,3], the output result should be as follows, the order does not matter, there can be no repetition:

[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]

In the detailed explanation of the backtracking algorithm, this problem is used to explain the backtracking template. This problem is also listed here, which is to compare the codes of the two backtracking algorithms "permutation" and "combination".

First draw a traceback tree to take a look:

 

 

 

We used Java code to write the solution:

List <List <Integer >> res = new LinkedList <> (); 

/ * Main function, input a group of non-repeating numbers, and return their full arrangement * / 
List <List <Integer >> permute ( int [] nums) {
 // Record the "path" 
LinkedList <Integer> track = new LinkedList <> (); 
backtrack (nums, track); 
return res; 
} 

void backtrack ( int [] nums, LinkedList <Integer> track) {
 // End of trigger Conditional 
if (track.size () == nums.length) { 
res.add ( new LinkedList (track));
 return ; 
} 

for( int i = 0 ; i <nums.length; i ++ ) {
 // Exclude illegal choices 
if (track.contains (nums [i]))
 continue ;
 // Make choices 
track.add (nums [i]);
 // Enter the next level of decision tree 
backtrack (nums, track);
 // Deselect 
track.removeLast (); 
} 
}

 

The backtracking template has not changed, but according to the tree drawn by the permutation problem and the combination problem, the tree of the permutation problem is more symmetrical, and the closer the tree to the combination problem is, the fewer the right nodes.

The manifestation in the code is that the arrangement problem uses the contains method to exclude the numbers that have been selected in the track each time; and the combination problem passes in a start parameter to exclude the number before the start index.

The above is the solution to the three problems of permutation, combination and subset. Summarize:

The subset problem can use the idea of ​​mathematical induction, assuming that the results of a smaller problem are known, and how to derive the results of the original problem. You can also use a backtracking algorithm, using the start parameter to exclude selected numbers.

The combination problem uses the backtracking idea, and the result can be expressed as a tree structure. We only need to apply the backtracking algorithm template. The key point is to use a start to exclude the numbers that have been selected.

The arrangement problem is a retrospective idea, and it can also be expressed as a tree structure to apply the algorithm template. The key point is to use the contains method to exclude the selected numbers. The previous article has a detailed analysis. Here, it is mainly compared with the combination problem.

For these three problems, you can understand the meaning of the code by observing the structure of the recursive tree. .

Guess you like

Origin www.cnblogs.com/lau1997/p/12728355.html