Ideas and common problems of backtracking algorithm

Time:2021-12-9

introduce

Backtracking algorithm is a violent search algorithm. The backtracking algorithm adopts the idea of trial and error. In the algorithm, the problem is often divided into multiple steps, and each step has a variety of choices. If it is found that a step is wrong, it will return to the original path and choose another option until it comes out of the correct solution step. It can be seen that the essence of backtracking algorithm isA depth first traversal of the decision tree, or preorder traversal. When you find that you have taken the wrong way and come to the end, return to the original way and take other branches; When you find that you are going the right way, go on; If you find that you are on the right path (that is, the decisions you have made meet the requirements of the topic), add the path (that is, the decisions you have made) to the result set.

Backtracking algorithm is often implemented by recursion, and because it is a violent algorithm, the time complexity is often exponential. Therefore, this kind of problem often does not have a large amount of data, otherwise it is easy to timeout and cause stack overflow.

Template

As we said, the backtracking algorithm can be regarded as a depth first traversal of the decision tree. Here, in order to make it easier to understand, we will use a fully arranged topic as an example.
Ideas and common problems of backtracking algorithm
In this figure, inside each box is a decision list, and on the line is a path (i.e. a decision that has been made). The traversal process is that each time the decision is completed, the decision is added to the path and the next round of decision is made (i.e. entering the next layer of recursion). The decision list of the next level recursion will be affected by the previous level recursion in this topic. When the path meets a certain condition, for example, in this topic, if the path length is equal to the original output length, add the result to the result set, then backtrack, return to the previous level of recursion, cancel the previous decision, and select the next decision. Until the whole decision tree is traversed.

After understanding the concepts of decision list, taken path and cancellation decision, we can easily write this backtracking algorithm template.

Result set = []
Def backtracking (decision list, path taken):
    If the following path meets the following conditions:
        Result set. Append (path) 

    For decision in decision list:
        Path. Append # make a decision
        Backtracking (decision list, path taken)
        Path. Pop() # cancel decision

In the backtracking algorithm, we need to pay attention to the pathWhen is the decision satisfiedWhen do I need to go backDecision list for each round of decisionsHow it looks. Here is the time when backtracking is needed. The backtracking place is actually the exit of recursion. This is different for each topic. For example, in the full permutation, if the number of decision lists in each round is – 1 of the number of decision lists in the previous round, the recursive function will automatically return when the final decision is 0. However, in other topics, you may need to design backtracking conditions to prevent infinite recursion.

Common questions

Here I list the problems I have done and tell you my thoughts on these problems. It includes full permutation, Full Permutation 2, subset, subset 2, combination, combination sum, combination sum 2, combination sum 3, recovery IP address, N Queen and solution Sudoku.

Full arrangement

Leetcode 46
As mentioned above, we can design recursive functions. The number of decisions in each round is – 1 of the number in the previous round. The exit of recursion is that the path length is the length of the original array. Because this function is designed as the number of decisions in each round – 1, there is no decision in the last round of recursion, and the function will return automatically. In fact, you don’t have to writereturn

func permute(nums []int) [][]int {
    result := make([][]int, 0)
    bt([]int{}, nums, len(nums), &result)
    return result

}
func bt(path, nums []int, length int, result *[][]int) {
    if len(path) >= length {
        *result = append(*result, append([]int(nil), path...))
        //There is no need to return here because there is no decision in the last round
    }
    for i := 0; i < len(nums); i++ {
        path = append(path, nums[i])
        newNums := append([]int(nil), nums[:i]...)
        newNums = append(newNums, nums[i+1:]...)
        bt(path, newNums, length, result)
        path = path[:len(path)-1]
    }
}

Of course, you can also implement the backtracking function in another way. Use onevisitedThe array marks whether the elements are added to the path, which requires that the original array cannot have duplicate elements.

func permute(nums []int) [][]int {
    result := make([][]int, 0)
    visited:=make([]bool,len(nums))
    bt(nums,[]int{},visited,&result)
    return result
}
func bt(nums,path []int,visited []bool,result *[][]int){
    if len(path)==len(nums){
        *result = append(*result, append([]int(nil), path...))
    }
    for i:=0;i<len(nums);i++{
        if visited[i]{
            continue
        }
        visited[i]=true
        path=append(path,nums[i])
        bt(nums,path,visited,result)
        path=path[:len(path)-1]
        visited[i]=false
    }
}

Full arrangement II

Leetcode 47
The original array input this time contains duplicate elements. It is required that there should be no duplicate elements in the output arrangement. We can see how the decision tree should be drawn.
Ideas and common problems of backtracking algorithm
Suppose we enter[1,2,2], if you continue to use the recursive function of the previous question, there will be three more repeated results. Therefore, the decision tree needs to be pruned. The cross in the picture is the subtracted branch. So how to design pruning algorithm?

Of course, you can detect whether the current path already exists in the result set at the exit of each recursion, but that’s too time complexity. It can be calculated in a simpler way.

We can notice that in our decision tree, the pruned tree nodes contain repeated elements. There are multiple subtrees under each repeating element, and we only traverse the subtree on the left. For example, in the left part of the figure, after traversing to[2,2]When selecting nodes, we only select the one on the left2, cut the one on the right2。 When we traverse the top[1,2,2]Node, we only traverse the left2, cut off the one on the right2。 This is because we need to remove the duplicate, so when we encounter a duplicate element, we only traverse it once. Here, we only select the leftmost one in a group of duplicate elements.

Therefore, our pruning idea can be designed as,In each round of decision-making list, a group of repeated elements are encountered, and only the first one is selected to be traversed。 Of course, in order to implement such an algorithm, we must modify the arraysortSo that we can select the first of a set of repeating elements.

In the algorithm, elements with an index greater than 0 and repeated with the previous element will be skipped.

import "sort"

func permuteUnique(nums []int) [][]int {
    sort.Ints(nums)
    length := len(nums)
    result := make([][]int, 0)
    bt([]int{}, nums, length, &result)
    return result
}

func bt(path, nums []int, length int, result *[][]int) {
    if len(path) >= length {
        *result = append(*result, append([]int(nil), path...))
        return
    }
    for i := 0; i < len(nums); i++ {
        if i > 0 && nums[i] == nums[i-1] {
            continue
        }
        path = append(path, nums[i])
        newNums := append([]int(nil), nums[:i]...)
        newNums = append(newNums, nums[i+1:]...)
        bt(path, newNums, length, result)
        path = path[:len(path)-1]
    }
}

subset

Leetcode 78
The title requires that you enter a non repeating array and return its power set.

The difference between subsets and permutations is that sets are disordered, so[1,2,3]and[2,1,3]Is equivalent.
Ideas and common problems of backtracking algorithm

After simplification, it is like this.
Ideas and common problems of backtracking algorithm

We continue to use the above decision tree. The graph includes the top empty set. Each line is a subset and must be added to the result set. If we continue to use the above decision tree, we can find that there are many branches to be cut. For example, on the second floor, we will[2,1]Cut it off because it’s in the collection[2,1]and[1,2]Is the same. These subtracted branches have a feature that they contain elements smaller than the current path subscript. Therefore, we found the rule of pruning. Do not select elements smaller than the maximum subscript of the current path, because they have been added to the result set before. In other words, keep the subscripts of the path in ascending order.

We can design a backtracking algorithm, one for each roundposThe (position) variable represents the maximum subscript of the current path and the next round of decision-making frompos+1Start selection at subscript. Some bloggers will name this variablebeginstart

func subsets(nums []int) [][]int {
    result := make([][]int, 0)
    bt(nums, 0, []int{}, &result)
    return result

}
func bt(nums []int, pos int, path []int, result *[][]int) {
    *result = append(*result, append([]int(nil), path...))
    for i := pos; i < len(nums); i++ {
        path = append(path, nums[i])
        bt(nums, i+1, path, result)
        path = path[:len(path)-1]
    }
}

In fact, subset problem can be implemented in other ways besides backtracking. But in this article, we only discuss the backtracking algorithm for the time being.

Subset II

Leetcode 90
The topic input array contains duplicate elements. It is required that the output power set does not contain duplicate subsets.
Ideas and common problems of backtracking algorithm
Continuing to use the decision tree above, we found that pruning was needed. The interpretation of pruning is similar to that of full arrangement II, that is, when encountering a group of repeated tuples, only the leftmost traversal is selected. Therefore, we can sort the array in the way of full arrangement 2. In each round of decision-making, if we encounter a group of repeated elements, only the first one will be traversed, and the rest will be skipped.

import "sort"

func subsetsWithDup(nums []int) [][]int {
    result := make([][]int, 0)
    sort.Ints(nums)
    bt(nums, []int{}, 0, &result)
    return result
}

func bt(nums, path []int, pos int, result *[][]int) {
    *result = append(*result, append([]int(nil), path...))
    for i := pos; i < len(nums); i++ {
        if i > pos && nums[i] == nums[i-1] {
            continue
        }
        path = append(path, nums[i])
        bt(nums, path, i+1, result)
        path = path[:len(path)-1]
    }

}

combination

Leetcode 77
Topic inputnandk, return range required[1, n]All possiblekA combination of numbers. In fact, the essence is to input an array[1,..,n], output length iskSubset of.

So we can easily write code based on subsets. Be careful, don’t forgetreturn

func bt(nums, path []int, pos, length int, result *[][]int) {
    if len(path) >= length {
        *result = append(*result, append([]int(nil), path...))
        return
    }
    for i := pos; i < len(nums); i++ {
        path = append(path, nums[i])
        bt(nums, path, i+1, length, result)
        path = path[:len(path)-1]
    }

}

func combine(n int, k int) [][]int {
    result := make([][]int, 0)
    nums := make([]int, n)
    for i := 0; i < n; i++ {
        nums[i] = i + 1
    }
    bt(nums, []int{}, 0, k, &result)
    return result

}

Combined sum

Leetcode 39

Given a positive integer array of candidates without duplicate elements and a positive integer target, find all the unique combinations in candidates that can make the number and target the target number.

The number in candidates can be selected repeatedly without limit. If the number of at least one selected number is different, the two combinations are unique.

For a given input, the number of unique combinations of guaranteed sum and target is less than 150.

To entercandidate=[2,3,5],target=8For example, draw the decision tree. For convenience, only the path that meets the requirements is drawn here.
Ideas and common problems of backtracking algorithm
Different from other questions above, the number of this question can be selected repeatedly, so the list of each round of decision-making can be the same. At the same time, the question is required not to have repeated combinations. That is, it can’t appear at the same time,[2,3,5]and[5,2,3], the weight needs to be removed.

In fact, there are many similarities with subsets, except that the exit of recursion becomes the sum of pathstarget。 Numbers can be selected repeatedly, but there cannot be repeated combinations in the result set.

So we refer to the subset and write the following code

func sum(path []int)int{
    sum:=0
    for _,v := range path{
        sum+=v
    }
    return sum
}
func bt(nums,path []int,pos,target int, result *[][]int){
    if sum(path)>target  {
        return
    } else if sum(path)==target {
        *result = append(*result, append([]int(nil), path...))
        return
    }
    for i := pos; i < len(nums); i++ {
        path = append(path, nums[i])
        BT (Num, path, I, target, result) // the next round starts with I instead of I + 1
        path = path[:len(path)-1]
    }

}

func combinationSum(candidates []int, target int) [][]int {
    result := make([][]int, 0)
    bt(candidates, []int{}, 0,target, &result)
    return result
}

Note that we still useposVariables, but because numbers can be selected repeatedly, the next round of decision-making is still fromiStart, noti+1。 For the exit of recursion, in addition to considering that the total path is target, the total path is greater than target. In this case, if you continue recursion, the total path will become larger and farther away from the target, so you need to return in time.

For the above code, we can do a little optimization. Each round of recursion willtargetSubtract the currently selected number and pass it to the next recursion. This eliminates the need to traverse the path summation for each round of recursion. When I test on leetcode, the time and space are the same whether there is optimization or not. However, in the actual test with go benchmark, it can be twice as fast after optimization under large data.

func bt(nums,path []int,pos,target int, result *[][]int){
    if target < 0 {
        return
    } else if target == 0 {
        *result = append(*result, append([]int(nil), path...))
        return
    }
    for i := pos; i < len(nums); i++ {
        path = append(path, nums[i])
        bt(nums, path,i ,target-nums[i], result)
        path = path[:len(path)-1]
    }

}

func combinationSum(candidates []int, target int) [][]int {
    result := make([][]int, 0)
    bt(candidates, []int{}, 0,target, &result)
    return result
}

Combined sum II

Leetcode 40
This time, only one time is allowed for each number of times. At the same time, there may be duplicate data in the input array, so we can repeat the old technique. Refer to subset 2 to sort the array. When encountering a group of duplicate elements, only traverse the first one and skip the rest.

Write the code as follows

import "sort"


func bt(nums, path []int, pos, target int, result *[][]int) {
    if target < 0 {
        return
    } else if target == 0 {
        *result = append(*result, append([]int(nil), path...))
        return
    }
    for i := pos; i < len(nums); i++ {
        if i > pos && nums[i] == nums[i-1] {
            continue
        }
        path = append(path, nums[i])
        bt(nums, path, i+1, target-nums[i], result)
        path = path[:len(path)-1]
    }

}

func combinationSum2(candidates []int, target int) [][]int {
    result := make([][]int, 0)
    sort.Ints(candidates)
    bt(candidates, []int{}, 0, target, &result)
    return result
}

Combined sum III

Leetcode 216

Find the combination of all k numbers whose sum is n. Only positive integers of 1 – 9 are allowed in the combination, and there are no duplicate numbers in each combination.
Note: all numbers are positive integers. The solution set cannot contain duplicate combinations.

Although this is the last question of the sum of combinations, it is still relatively common on the whole.

We can manually build an array of 1-9, and then add the case that the path length is K and the path length is greater than k at the recursive exit.

func bt(nums, path []int, pos, length, target int, result *[][]int) {
    if target < 0 {
        return
    } else if len(path) > length {
        return
    } else if target == 0 && len(path) == length {
        *result = append(*result, append([]int(nil), path...))
        return
    }
    for i := pos; i < len(nums); i++ {
        path = append(path, nums[i])
        bt(nums, path, i+1, length, target-nums[i], result)
        path = path[:len(path)-1]
    }
}
func combinationSum3(k, n int) [][]int {
    result := make([][]int, 0)
    candidates := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
    bt(candidates, []int{}, 0, k, n, &result)
    return result
}

Restore IP address

Leetcode 93

Given a string containing only numbers to represent an IP address, return all valid IP addresses that may be obtained from S. You can return answers in any order.

A valid IP address consists of exactly four integers (each integer is between 0 and 255 and cannot contain a leading 0). Integers are separated by ‘.’.
For example, “0.1.2.201” and “192.168.1.1” are valid IP addresses, but “0.011.255.245”, “192.168.1.312” and “192 [email protected] “Is an invalid IP address.

I think IP recovery is still an interesting topic, and more situations need to be considered. For example, each segment of IP partition cannot have a leading 0. If so, only this segment can be separated as 0. We also need to think about how to design the path storage.

We can design such an idea, a pointeriTraverse the IP string from left to right, if subscript0reachiThis section of the IP complies with the IP separation format (e.g0Or less than or equal to255The next division symbol.Enter the next round of recursion. The next round of recursion starts from the character after the separator of the previous round, if the previous separator and subscriptiThis section also conforms to the format of IP separation, and then enters the next round of recursion. When it has been laid3When there are two separators, you can detect whether the current IP address conforms to the format. If yes, add the result set.

This is a decision tree. Because there are many branches in the decision tree of this problem, ellipsis is is used in many places. The paths that meet the requirements are marked with red lines.
Ideas and common problems of backtracking algorithm

So how to complete the “mark separator” and “detect whether the current IP conforms to the format”? We can change our thinking a little. When we want to type a separator, we will0orPrevious separatorTo current pointeriThis segment is added to a path whose structure is a string array. If there are 4 segments of strings in the path. Check whether the string has been traversed. If it has been traversed, it can be added to the result set.

func bt(ipStr string, path []string, pos int, result *[]string) {
    if len(path) == 4 && pos == len(ipStr) {
        ipAddr := ""
        for i, v := range path {
            ipAddr += v
            if i < len(path)-1 {
                ipAddr += "."
            }
        }
        *result = append(*result, ipAddr)
        //Return is not required here
    } else if len(path) == 4 && pos < len(ipStr) {
        return
    }
    for i := pos; i < len(ipStr); i++ {
        if i != pos && ipStr[pos] == '0' {
            return
        }
        if num, _ := strconv.Atoi(string(ipStr[pos : i+1])); num > 255 {
            return
        }
        path = append(path, ipStr[pos:i+1])
        bt(ipStr, path, i+1, result)
        path = path[:len(path)-1]
    }
}

func restoreIpAddresses(s string) []string {
    result := make([]string, 0)
    bt(s, []string{}, 0, &result)
    return result
}

Write code. PointeriThe last separator (last round) is detectediStay position+1)Or subscript0Whether the current segment conforms to the format of IP separation. If it is 0 and less than or equal to 255, it will enter the next round. Here, we design that if the current partition has a leading 0 or greater than 255, it will be returned directly, and the rest will enter the next round of recursion. After 4 segments are stored in the path, check whether the traversal has been completed, and add the results to the result set. Otherwise, it returns. Because the recursion to the last string has been traversed, the operation of adding the result set does not need to be completedreturn

Queen n

Leetcode 51
Queen n to be honest, I don’t think it’s a problem, but there’s a lot of code.

The n queen problem studies how to place n queens in n × N’s chessboard, and the Queens can’t attack each other.

Give you an integer n and return the solution of all different N Queen problems.

Each solution contains a different chess placement scheme for the n-queen problem, in which ‘Q’ and ‘.’ represent the queen and the vacancy respectively.

The specific idea is actually very simple. First, put down a queen in the first column of the first row of the chessboard, and then start from the second row to check whether it can be put down one by one from left to right, and so on. When all the squares in a row can’t put down the queen, go back to the previous row, move the queen of this row to the right to the next position that can be placed, and if it can’t be placed, go back again. When the chess pieces are put down in the last line of the chessboard, the current state is recorded.

Personally, I think the main core of this problem is to find the position where the queen can be placed in each line. Put the first version of the code first.

//Check whether row and col columns can be dropped
func isValid(board [][]byte, row, col int) bool {
    //Vertical
    for row := row; row >= 0; row-- {
        if board[row][col] == 'Q' {
            return false
        }
    }
    //Upper left oblique
    for row, col := row-1, col-1; row >= 0 && col >= 0; row, col = row-1, col-1 {
        if board[row][col] == 'Q' {
            return false
        }

    }
    //Upper right corner oblique
    for row, col := row-1, col+1; row >= 0 && col < len(board[row]); row, col = row-1, col+1 {
        if board[row][col] == 'Q' {
            return false
        }

    }
    return true
}

func bt(board [][]byte, row int, result *[][]string) {
    if row >= len(board) {
        snapshot := make([]string, 0)
        for _, row := range board {
            snapshot = append(snapshot, string(row))
        }
        *result = append(*result, snapshot)
        return
    }
    for col := 0; col < len(board); col++ {
        if isValid(board, row, col) {
            board[row][col] = 'Q'
            bt(board, row+1, result)
            board[row][col] = '.'
        }
    }

}

func solveNQueens(n int) [][]string {
    board := make([][]byte, n)
    for i := range board {
        board[i] = make([]byte, n)
    }
    result := make([][]string, 0)
    for i := range board {
        for j := range board[i] {
            board[i][j] = '.'
        }
    }
    bt(board, 0, &result)

    return result

}

Firstly, we construct a two-dimensional array chessboard according to the problemboard, when on pagerowLinecolColumn, thenborad[row][col]='Q'isValid()Function to receive checkerboard and row / column coordinates, and return whether the coordinates can place queens. Because the queen is placed from top to bottom,isValid()It will check whether there are other queens at the top left, right and right of the coordinate. If not, the coordinate can place queens, otherwise it cannot be placed.bt()The loop in will check whether it can be dropped column by column in this row. If there is a position, it will recursively enter the next row. If not, go back.

In the code above,isValid()The function traverses the upper left, upper right and upper right squares each time. We can optimize it with a hash table. Using the diagram of leetcode problem solution, we can see the law that the grid on the same slash from top left to bottom rightLine number column numberThe values are often equal, from the top right to the bottom left of the grid on the same slashLine number + column numberOften equal. We can designdiagonalanddiagona2Two hash tables indicate whether there is a queen on the slash in these two directions. Plus onecolumnsHash table indicating whether there are queens on the same column. When inrowLinecolPut the queen in the column,diagonal1[row-col]diagonal2[row+col]columns[col]Will be set totrue, when deselected, it is set toflase
Ideas and common problems of backtracking algorithm
Ideas and common problems of backtracking algorithm
The code is as follows

func bt(board [][]byte, row int, columns, diagonal1, diagonal2 map[int]bool, result *[][]string) {
    if row >= len(board) {
        snapshot := make([]string, 0)
        for _, row := range board {
            snapshot = append(snapshot, string(row))
        }
        *result = append(*result, snapshot)
        return
    }
    for col := 0; col < len(board); col++ {
        if columns[col] || diagonal1[row-col] || diagonal2[row+col] {
            continue
        } else {
            
            columns[col] = true
            diagonal1[row-col] = true
            diagonal2[row+col] = true
            board[row][col] = 'Q'
            bt(board, row+1, columns, diagonal1, diagonal2, result)
            board[row][col] = '.'
            diagonal2[row+col] = false
            diagonal1[row-col] = false
            columns[col] = false
            
        }
    }

}

func solveNQueens(n int) [][]string {
    board := make([][]byte, n)
    for i := range board {
        board[i] = make([]byte, n)
    }
    result := make([][]string, 0)
    for i := range board {
        for j := range board[i] {
            board[i][j] = '.'
        }
    }
    columns := make(map[int]bool)
    diagonal1 := make(map[int]bool)
    diagonal2 := make(map[int]bool)
    bt(board, 0, columns, diagonal1, diagonal2, &result)

    return result

}

Solution Sudoku

Leetcode 37

Write a program to solve the Sudoku problem by filling in spaces.

Sudoku solution shall follow the following rules:

The numbers 1-9 can only appear once in each line.
The numbers 1-9 can only appear once in each column.
The numbers 1-9 can only appear once in each 3×3 uterus separated by a thick solid line. (please refer to the example diagram)

Numbers have been filled in the space of Sudoku, and the blank space is represented by ‘.’.

Sudoku people think it’s like queen n. It is nothing more than line by line scanning to detect whether a number can be written in a blank position. If so, write it down and view the next blank position. If you can’t write down any number, go back.

We refer to Queen N and write the first version of the code.

func IsVaild(i, j int, num byte, board [][]byte) bool {
    //Detect peers
    for col := 0; col < len(board[i]); col++ {
        if board[i][col] == num {
            return false
        }
    }
    //Detect the same column
    for row := 0; row < len(board); row++ {
        if board[row][j] == num {
            return false
        }
    }
    //Detect the same grid
    palaceI := i / 3
    palaceJ := j / 3
    for row := palaceI * 3; row < palaceI*3+3; row++ {
        for col := palaceJ * 3; col < palaceJ*3+3; col++ {
            if board[row][col] == num {
                return false
            }
        }
    }
    return true
}

func nextPosition(i, j int, board [][]byte) (int, int) {
    for row := i; row < len(board); row++ {
        for col := j; col < len(board[row]); col++ {
            if board[row][col] == '.' {
                return row, col
            }
        }
        j = 0

    }
    return -1, -1
}

func bt(i, j int, board [][]byte) {

    i, j = nextPosition(i, j, board)
    if i == -1 && j == -1 {
        Panic ("traversal complete")
        //Throw an exception and exit directly to the outermost function to prevent backtracking
    }
    for num := 1; num < 10; num++ {
        if IsVaild(i, j, byte(num+'0'), board) {
            board[i][j] = byte(num + '0')
            BT (I, J, board) // you can also pass in I, j + 1, but not I + 1, j + 1
            board[i][j] = '.'
        }
    }

}

func solveSudoku(board [][]byte) {
    defer func() { recover() }()
    bt(0, 0, board)

}

Both Sudoku and queen n have a chessboard, but the difference is that queen n requires that there should be no other queens on the slash, while Sudoku requires that there should be no same number in the same palace. The chessboard is divided into 9 squares, with 3 in each row and column. It is regarded as a 3×3 chessboard. Each palace can be represented by row number and column number, and the range is[0-2]。 withi,jIs the coordinate, the coordinate is[i][j]Your number is located in[i//3][j//3]//It is divided by (i.e. rounded down) on the grid. For example, the coordinates are[1][1]Your number is right here[0][0]Gong Ge. Knowing this, it’s easy to writeisVaild()Function.

In addition, some numbers will be placed in the Sudoku chessboard in advance. We can’t change these placed numbers. We can only fill in the blank space. So I designed it in the above codenextPosition()Function, which returns including coordinates[i][j]Coordinates of the next blank bit within. The coordinates selected in each round are determined bynextPosition()decision. Note that when you select to transfer coordinates in the next round, you can actually transfer coordinatesi,j+1, but cannot be passed ini+1,j+1Because the algorithm is progressive,i+1It means going straight across the line.The difference between Queen N and queen n is that queen n’s decision-making list in each round is checkerboard, while Sudoku’s decision-making list is checkerboard1-9Nine numbers。 If the chessboard comes to the end,nextPosition()Is returned-1,-1。 Because this problem does not need to write the path to the result set, I directly throw an exception here to prevent backtracking and catch it in the outermost function. Achieve the effect of directly jumping out of multiple recursion.

The above code is the most intuitive code, but it is not optimal. We can use arrays to optimize it.

func solveSudoku(board [][]byte) {
    defer func() { recover() }()
    row := [9][9]bool{}
    columns := [9][9]bool{}
    block := [3][3][9]bool{}
    space := make([][]int, 0)
    for i := range board {
        for j := range board[i] {
            if board[i][j] == '.' {
                space = append(space, []int{i, j})
            } else {
                num := board[i][j] - '1'
                row[i][num] = true
                columns[j][num] = true
                block[i/3][j/3][num] = true
            }
        }
    }
    bt(0,board,space,row,columns,block)

}
func bt(pos int, board [][]byte, space [][]int, row, columns [9][9]bool, block [3][3][9]bool) {
    if pos == len(space) {
        Panic ("done")
    }
    i, j := space[pos][0], space[pos][1]
    for num := 0; num < 9; num++ {
        if row[i][num] || columns[j][num] || block[i/3][j/3][num] {
            continue
        } else {
            row[i][num] = true
            columns[j][num] = true
            block[i/3][j/3][num] = true
            board[i][j] = byte(num + '1')
            bt(pos+1, board, space, row, columns, block)
            board[i][j] = '.'
            block[i/3][j/3][num] = false
            columns[j][num] = false
            row[i][num] = false
        }
    }

}

In fact, this code is very similar to the official solution of leetcode, except that it does not use the writing method of closure. It may look clearer, but there are many references. We designrow[9][9]boolcolumns[9][9]boolblock[3][3][9]boolTo represent whether there is a number in the same row, column and grid. Note that because we now use the array subscript to store the occurrence of a number, we need to start from0Start to8end.row[i][num]==trueRepresents the secondiNumber already exists in linenum+1columns[j][num]==trueRepresents the secondjNumber in columnnum+1block[i/3][j/3][num]==tureexpressithat ‘s okjThere are numbers in the grid where the column is locatednum+1

Finally, for convenience, I directly throw an exception to jump out of multiple loops. Of course, you can also add the return value to the backtracking function like leetcode’s official problem solution to judge whether it jumps out of recursion.

func bt(pos int, board [][]byte, space [][]int, row, columns [9][9]bool, block [3][3][9]bool) bool{
    if pos == len(space) { 
        return true    
        }
    i, j := space[pos][0], space[pos][1]
    for num := 0; num < 9; num++ {
        if row[i][num] || columns[j][num] || block[i/3][j/3][num] {
            continue
        } else {
            row[i][num] = true
            columns[j][num] = true
            block[i/3][j/3][num] = true
            board[i][j] = byte(num + '1')
            if bt(pos+1, board, space, row, columns, block){
                return true
                }
            board[i][j] = '.'
            block[i/3][j/3][num] = false
            columns[j][num] = false
            row[i][num] = false
        }
    }
    return false

}