Table of contents
- Understanding retrospective thinking
- Backtracking code framework
- Starting from N-ary tree
- For some problems, brute-force search won’t work either.
- Backtracking = recursion + local enumeration + letting go of predecessor
- Go code【LeetCode-77. Combination】
- Backtracking warm-up-discussing the path problem of binary trees again
Backtracking is one of the most important algorithmic ideas. It mainly solves some problems that cannot be solved by violent enumeration, such as combination, segmentation, subsets, permutations, chessboards, etc. From a performance point of view, the efficiency of the backtracking algorithm is not high, but it is good if the algorithm can produce results that cannot be solved by violence. It does not matter if the efficiency is low.
Understanding retrospective thinking
Backtracking can be regarded as an extension of recursion. Many ideas and solutions are closely related to recursion. Therefore, when learning backtracking, you will have a deeper understanding of recursion to analyze its characteristics.
Regarding the difference between recursion and backtracking, imagine a scenario where a hunk wants to get out of singles. There are two strategies:
- Recursive strategy: first create a chance encounter with the person you like, then get to know their situation, then make an appointment for dinner, try to hold hands when you have a crush, and confess your love if there is no rejection.
- Backtracking strategy: First count all the single girls around you, and then confess your love one by one. If you are rejected, say "I'm drunk", then pretend that nothing happened and move on to the next one.
In fact, the essence of backtracking is such a process.
The biggest benefit of backtracking: There is a very clear template, and all backtracking is a large framework. Therefore, the framework of transparently understanding backtracking is the basis for solving all backtracking problems. So let’s analyze this framework.
Backtracking is not omnipotent, and the problems it can solve are very clear, such as: combination, segmentation, subsets, permutations, chessboards, etc. However, there are many differences in the specific processing of these problems, which require specific analysis of specific problems.
Backtracking can be understood as an expansion of recursion, and the code structure is particularly similar to deeply traversing an N-ary tree, so as long as you know recursion, it is not difficult to understand backtracking. The difficulty is that many people don't understand why there is an "undo" operation after recursive language. Let’s assume a scenario: You have a new girlfriend. Before coming to your house, would you quickly hide your ex’s belongings? The same goes for backtracking, some information belongs to the predecessor and needs to be dealt with before starting over.
Backtracking code framework
func Backtracking(参数) {
if 终止条件 {
存放结果
return
}
for 选择本层集合中元素(画成树,就是树节点孩子的大小) {
处理节点
Backtracking()
回溯,撤销处理结果
}
}
Starting from N-ary tree
Let’s first look at the problem of N-ary tree traversal, pre-order traversal of a binary tree, the code is as follows:
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
func preorderTraversal(root *TreeNode) []int {
ret := make([]int, 0)
if root == nil {
return ret
}
ret = append(ret, root.Val)
ret = append(ret, preorderTraversal(root.Left)...)
ret = append(ret, preorderTraversal(root.Right)...)
return ret
}
What if it is a three-way, four-way or even N-way tree? Obviously you can't use Left and Right to represent branches at this time. It's better to use a slice, like this:
/**
* Definition for a Node.
* type Node struct {
* Val int
* Children []*Node
* }
*/
func preorder(root *Node) []int {
ret := make([]int, 0)
if root == nil {
return ret
}
ret = append(ret, root.Val)
for _, v := range root.Children {
ret = append(ret, preorder(v)...)
}
return ret
}
At this point, have you found that it is very similar to the backtracking template mentioned above? Yes! very alike! Since they are very similar, it means there must be some kind of relationship between the two. Continue reading
For some problems, brute-force search won’t work either.
We say that backtracking mainly solves problems that cannot be solved by violent enumeration.
Let’s look at an example: Question link: LeetCode-77. Combination
For example 1, it is easy to write code, double layer Looping is easy:
func combine(n int, k int) [][]int {
ret := make([][]int, 0)
for i:=1; i<=n; i++ {
for j:=i+1;j<=n;j++ {
arr := []int{
i, j}
ret = append(ret, arr)
}
}
return ret
}
What if k becomes larger, such as k=3? It’s also possible. The three-layer loop is basically done:
func combine(n int, k int) [][]int {
ret := make([][]int, 0)
for i:=1; i<=n; i++ {
for j:=i+1;j<=n;j++ {
for u:=j+1;u<=n;u++ {
arr := []int{
i, j, u}
ret = append(ret, arr)
}
}
}
return ret
}
What if k=5 here, or even k=50? How many loops do you need? Even telling you that k is an unknown positive integer k, how do you write a loop? There is nothing you can do at this point, so brute force search will not work.
This is a combination type problem. In addition, there are similar problems in subsets, permutations, cuts, chessboards, etc.
Backtracking = recursion + local enumeration + letting go of predecessor
Continue studying Question link: LeetCode-77. Combination , illustrate the process of enumerating all the answers above.
Each time an element is selected from the collection, the range of options will gradually shrink, and when 4 is selected, it will be empty.
Observing the tree structure, you can find that every time a leaf node (green box in the picture) is visited, a result is found. Although the last one is empty, it does not affect the result. This is equivalent to only needing to collect the selected content (branch) starting from the root node when it reaches the leaf node, which is the desired result.
The number of elements n is equivalent to the width of the tree (horizontally), and the number of elements k of each result is equivalent to the depth of the tree (vertically). So we say that the backtracking algorithm is just one vertical and one horizontal line. Let’s analyze other rules:
- Each selection is made one by one from sequences like "1 2 3 4" and "2 3 4". This islocal enumeration, and the enumeration range becomes smaller the further it goes.
- When enumerating, it is a simple brute force test, verifying one by one whether it can meet the requirements. As you can see from the above figure, this is the process of N-ary tree traversal, so the two codes must be very similar.
- As can be seen from the figure, each subtree is a substructure that can recurse.
In this way we perfectly combine backtracking with N-ary trees.
However, there is still a big problem: backtracking usually involves a manual undo operation. Why? Continue to observe the picture above:
It can be found that collecting each result is not for leaf nodes, but for branches. For example, if the top layer selects 1 first, if the lower layer selects 2, the result will be "1 2", if the lower layer selects 3, the result will be "1 3" ,So on and so forth. Now the question is, after getting the first result "1 2", how to get the second result "1 3"?
It can be found that after getting "1 2", you can cancel 2, and then continue to get 3, so you get "1 3". In the same way, you can get "1 4", and then the current layer will be gone. You can cancel 1 and continue taking 2 from the top.
Corresponding code operation: first put the first result in the temporary list path, and then put the contents of the path into the result list after getting the first result "1 2". After that, undo the 2 in the path, continue to look for the next result "1 3", then continue to put the result in the path, and then undo the search again.
Go code【LeetCode-77. Combination】
Question link: LeetCode-77. Combination
func combine(n int, k int) [][]int {
ret := make([][]int, 0)
if k <= 0 || n < k {
return ret
}
path := make([]int, 0)
var dfs func(int)
dfs = func(start int) {
if len(path) == k {
// 关键
pathcopy := make([]int, k)
copy(pathcopy, path)
ret = append(ret, pathcopy)
return
}
for i:=start;i<=n;i++ {
path = append(path, i)
dfs(i+1)
path = path[:len(path)-1]
}
}
dfs(1)
return ret
}
Backtracking warm-up-discussing the path problem of binary trees again
Topic: All paths in a binary tree
Question link: LeetCode-257. All paths in a binary tree
Go code
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
func binaryTreePaths(root *TreeNode) []string {
ret := make([]string, 0)
if root == nil {
return ret
}
path := make([]int, 0)
var dfs func(*TreeNode)
dfs = func(node *TreeNode){
if node == nil {
return
}
path = append(path, node.Val)
if node.Left == nil && node.Right == nil {
ret = append(ret, conv(path))
path = path[:len(path)-1]
return
}
dfs(node.Left)
dfs(node.Right)
path = path[:len(path)-1]
}
dfs(root)
return ret
}
func conv(arr []int) string {
length := len(arr)
strarr := make([]string, length)
for i, v := range arr {
strarr[i] = strconv.Itoa(v)
}
return strings.Join(strarr,"->")
}
Compare the previous recursive writing method (no retraction step, not backtracking writing method)
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
func binaryTreePaths(root *TreeNode) (res []string) {
if root == nil {
return nil
}
var a func(*TreeNode, string)
a = func(node *TreeNode, path string) {
if node == nil {
return
}
str := fmt.Sprintf("%d", node.Val)
path = path+str
// 叶子节点
if node.Left == nil && node.Right == nil {
res = append(res, path)
return
}
a(node.Left, path+"->")
a(node.Right, path+"->")
}
a(root, "")
return
}
Topic: Path Sum II
Question link: LeetCode-113. Path sum II
Go code
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
func pathSum(root *TreeNode, targetSum int) [][]int {
ret := make([][]int, 0)
if root == nil {
return ret
}
path := make([]int, 0)
var dfs func(*TreeNode, int)
dfs = func(node *TreeNode, sum int) {
if node == nil {
return
}
path = append(path, node.Val)
// 叶子节点
if node.Left == nil && node.Right == nil {
// 路径匹配,加入结果列表
if node.Val == sum {
pathcopy := make([]int, len(path))
copy(pathcopy, path)
ret = append(ret, pathcopy)
}
path = path[:len(path)-1]
return
}
dfs(node.Left, sum-node.Val)
dfs(node.Right, sum-node.Val)
path = path[:len(path)-1]
}
dfs(root, targetSum)
return ret
}