The idea of backtracking is simple: go forward from one way, go in if you can, go back if you can't, and try another way. Of course, this is too abstract. The idea used in the algorithm is to transform it into trees and graphs. The simple description of the idea of backtracking method is that the solution space of the problem is transformed into the structural representation of graph or tree, and then traversed by depth first search strategy. In the process of traversal, all feasible solutions or optimal solutions are recorded and found.
Like dynamic planning, there is also a three board Axe:
1. Determination of DFS function parameters
2. Determine recursive termination conditions
3. Determine single layer search logic
Backtracking algorithm is suitable for problems composed of multiple steps, and each step has multiple options, all of which are similar to a tree structure.
To sum up, there is another template for backtracking, which is implemented with pseudo code:
function dfs(parameter list){ if(Termination conditions){ Storage results return } for(Select: Elements in this layer collection){ Processing node dfs(parameter list(route)) Backtracking, undo processing results } }
Next, use a few sutras to realize the following according to the trilogy:
1. The path with a certain value in the binary tree
Enter the following node and an integer of a binary tree, and print out all paths in which the sum of node values in the binary tree is the input integer. Path is defined as a path from the root node of the tree down to the leaf node.
Three board axe analysis: first, define a result array results to receive all paths. The parameters of the dfs function accept the root node root and a path (usually represented by a string). Let's look at the termination condition of dfs. Obviously, when you reach the leaf node, you can stop traversal and push it into the results. The next step is to determine the single-layer search logic. The first and second steps also pave the way for the third step. When the root is not empty, the path performs string splicing, and then determines whether it is a leaf node. If not, dfs depth gives priority to the left and right nodes. And pass the new path down. Upper Code:
var pathSum = function(root, target) { let results=[] function dfs(root,path,sum){ if(!root){ return } sum+=root.val path+=root.val if(!root.left && !root.right){ if(sum==target){ results.push(path) return } }else{ path+='->' dfs(root.left,path,sum) dfs(root.right,path,sum) } } dfs(root,'',0) return results.map(item=>{ return item.split('->') }) };
Wait, didn't you say a good backtracking? Just a dfs process without pruning? The above solution is actually a general solution to all path problems of binary tree, because all paths can be found. You can use an array instead of a string to implement a pruning process.
2. Combination sum
Give an array of positive integers without duplicate elements candidates And a positive integer target , find candidates All can make the sum of numbers as the target number target Unique combination of. candidates The numbers in can be selected repeatedly without limit. If the number of at least one selected number is different, the two combinations are unique.
Three board axe analysis: dfs (path, sum, idx) is selected as the parameter of dfs function. Path is used to record the path, sum is used to record the sum, and idx is used to record the subscript. The termination condition of dfs is that sum is not less than target. If sum is equal to target, push path into results.
Finally, there is single-layer logic. Backtracking generally uses horizontal traversal (for loop) plus vertical traversal (dfs) to traverse the whole tree. Upper Code:
var combinationSum = function(candidates, target) { let results=[] let len=candidates.length candidates.sort() function dfs(path,sum,start){ if(sum>=target){ if(sum==target){ results.push(path.slice())//Copy of path } return } for(let i=start;i<len;i++){ path.push(candidates[i]) dfs(path,sum+candidates[i],i) path.pop() } } dfs([],0,0) return results };
There are also variants of this topic. What if you can't reuse a number and there can't be repeated combinations?
Three board axe analysis: dfs must have a traversal starting point, so dfs(path,idx). The termination conditions are the same as above, but one thing is
Sort the array first. Determine the single-layer search logic and traverse the array horizontally from the subscript idx. Change the value of sum and push path and dfs(path,i+1). Because the same value cannot be taken, i+1. Backtracking is to modify the value of sum and launch path. Upper Code:
var combinationSum2 = function(candidates, target) { let results=[] let sum=0 candidates.sort((a,b)=>a-b) dfs([],0) return results function dfs(path,k){ if(sum>=target){ if(sum==target){ results.push(path.slice()) } return } for(let i=k;i<candidates.length;i++){ if(target<candidates[i])break if(i>k && candidates[i]==candidates[i-1]){ continue } sum+=candidates[i] path.push(candidates[i]) dfs(path,i+1) path.pop() sum-=candidates[i] } } };
2. Full Permutation Problem
Give an array without duplicate numbers nums , Return to its All possible permutations .
Problem analysis: this problem is still different from the combination problem. The combination problem has no backward orientation and can only be found behind the selected elements, but the arrangement problem can be "selected back". So the single-layer logic must be different.
Three board axe analysis: since you can still select the front side, you have to start from 0 every time when traversing horizontally, and the parameter list is a path. The termination condition is that the length of path is as long as that of num. The traversal logic constructs a hash table test to record whether it is used or not. The operation on the node is to change the authenticity of the hash table and push the path. Backtracking is to withdraw the modification of the hash table and push the path. Upper Code:
var permute = function(nums) { let results=[] let len=nums.length //Use a hash table to record whether an item in an array is used let test={} function dfs(path){ if(path.length==len){ results.push(path.slice()) return } for(let i=0;i<len;i++){ if(test[nums[i]]){ continue } path.push(nums[i]) test[nums[i]]=true dfs(path,i+1) path.pop() test[nums[i]]=false } } dfs([]) return results };
There is also a variation of full sorting, that is, if there are duplicate items and duplicate sorting is not allowed. After analysis, you can arrange the array first and make the same items into a set. As for how to bypass duplicates, continue or break in the horizontal comparison, resulting in failure to meet the requirements. Skip when test [I-1] = false & & test [i] = = test [I-1].
Upper Code:
var permuteUnique = function(nums) { let results=[] nums.sort((a,b)=>a-b) let test={} function dfs(path){ if(path.length==nums.length){ results.push(path.slice(0)) return } for(let i=0;i<nums.length;i++){ if(test[i] || (i>0 && nums[i]==nums[i-1] && !test[i-1]))continue test[i]=true path.push(nums[i]) dfs(path) test[i]=false path.pop() } } dfs([]) return results };
To sum up, the for loop is the key to logic when traversing single-layer logic. How to avoid duplicates.
There are many variants of backtracking algorithm. But the core idea is to traverse the whole tree horizontally and vertically in dfs.
After that, I will share more algorithm knowledge and summarize a little every day.