目录

回溯算法(或DFS)

1、框架

深度优先搜索就是回溯算法,解决一个回溯问题,实际上就是一个决策树的遍历过程。需要思考三个问题:

  • 路径:已经做出的选择。
  • 选择列表:当前可以做的选择。
  • 结束条件:到达决策树底层,无法再做选择的条件。

框架如下:

https://narcissusblog-img.oss-cn-beijing.aliyuncs.com/uPic/file-09/ScreenShot2021-09-14%2013.25.55.png

核心就是for循环里面的递归,在递归调用之前做选择,递归调用之后撤销选择

注意:回溯算法一般复杂度较高,需要配合剪枝。

2、全排列问题

n个不重复的数,全排列有n!个。比如给三个数[1,2,3],决策树如下:

https://narcissusblog-img.oss-cn-beijing.aliyuncs.com/uPic/file-09/ScreenShot2021-09-14%2013.35.58.png

回溯算法的核心框架如下:

https://narcissusblog-img.oss-cn-beijing.aliyuncs.com/uPic/file-09/ScreenShot2021-09-14%2013.43.34.png

只要在递归之前做出选择,在递归之后撤销刚才的选择,就能正确得到每个节点的选择列表和路径。代码如下:

func permute(nums []int) [][]int {
    // 回溯算法
    var result [][]int

    var backtrack func(track, nums []int, length int)
    backtrack = func(track, nums []int, length int) {
        // 结束条件
        if len(track) == length {
            temp := make([]int, length)
            copy(temp, track)
            result = append(result, temp)
            return
        }
        n := len(nums)
        for index, val := range nums {
            // 做选择
            track = append(track, val)
            nums[index], nums[n - 1] = nums[n - 1], nums[index]
            nums = nums[:n - 1]
            // 进入下一层决策
            backtrack(track, nums, length)
            // 撤销选择
            track = track[:len(track) - 1]
            nums = append(nums, val)
            nums[index], nums[n - 1] = nums[n - 1], nums[index]
        }
    }

    var track []int
    length := len(nums)
    backtrack(track, nums, length)
    return result
}

3、N皇后问题

https://narcissusblog-img.oss-cn-beijing.aliyuncs.com/uPic/file-09/ScreenShot2021-09-18%2012.38.27.png

回溯算法一般要配合剪枝降低复杂度。代码如下:

func solveNQueens(n int) [][]string {
    // 回溯算法
    var (
        result [][]string
        dfs func(board [][]byte, row int)
    )

    dfs = func(board [][]byte, row int) {
        // 结束条件
        if row == n {
            var temp []string
            for i := 0; i < n; i++ {
                temp = append(temp, string(board[i]))
            }
            result = append(result, temp)
            return
        }
        for col := 0; col < n; col++ {
            // 剪枝
            if !isValid(board, row, col, n) {
                continue
            }
            // 做选择
            board[row][col] = 'Q'
            // 下一层决策
            dfs(board, row + 1)
            // 撤销决策
            board[row][col] = '.'
        }
    }

    board := make([][]byte, n)
    // 初始化
    for i := 0; i < n; i++ {
        board[i] = make([]byte, n)
        for j := 0; j < n; j++ {
            board[i][j] = '.'
        }
    }
    dfs(board, 0)
    return result
}

func isValid(board [][]byte, row, col, n int) bool {
    // 检查列
    for i := 0; i < row; i++ {
        if board[i][col] == 'Q' {
            return false
        }
    }
    // 检查左斜线
    for i := 1; row - i >= 0 && col - i >= 0; i++ {
        if board[row - i][col - i] == 'Q' {
            return false
        }
    }
    // 检查右斜线
    for i := 1; row - i >= 0 && col + i < n; i++ {
        if board[row - i][col + i] == 'Q' {
            return false
        }
    }
    return true
}
  • 代码中,通过isValid函数对回溯算法进行了剪枝。不符合就不需要继续进入下一层决策,降低了复杂度。

4、回溯解决排列,子集,组合问题

求子集、求排列、求组合这几个问题都可以用回溯思想解决。(排列问题见上面)

子集

https://narcissusblog-img.oss-cn-beijing.aliyuncs.com/uPic/file-09/ScreenShot2021-09-19%2010.53.05.png

代码如下:

func subsets(nums []int) [][]int {
    // 回溯算法
    var (
        result [][]int
        dfs func(track []int, index int)
    )

    dfs = func(track []int, index int) {
        // 加入结果集
        temp := make([]int, len(track))
        copy(temp, track)
        result = append(result, temp)
        // 结束条件
        if index == len(nums) {
            return 
        }
        for i := index; i < len(nums); i++ {
            // 做选择
            track = append(track, nums[i])
            // 进入下一层决策
            dfs(track, i + 1)
            // 撤销决策
            track = track[:len(track) - 1]
        }
    }

    track := []int{}
    dfs(track,  0)
    return result
}

决策树如下:

https://narcissusblog-img.oss-cn-beijing.aliyuncs.com/uPic/file-09/ScreenShot2021-09-19%2012.07.29.png

组合

https://narcissusblog-img.oss-cn-beijing.aliyuncs.com/uPic/file-09/ScreenShot2021-09-19%2012.10.24.png

代码如下:

func combine(n int, k int) [][]int {
    // 回溯思想
    var (
        result [][]int
        dfs func(track []int, index int)
    )

    dfs = func(track []int, index int) {
        // 结束条件
        if len(track) == k {
            temp := make([]int, k)
            copy(temp, track)
            result = append(result, temp)
            return
        }
        // 剪枝
        if len(track) + n - index + 1 < k {
            return 
        }
        for i := index; i <= n; i++ {
            // 做出选择
            track = append(track, i)
            // 进入下一层
            dfs(track, i + 1)
            // 撤销决策
            track = track[:len(track) - 1]
        }
    }

    var track []int
    dfs(track, 1)
    return result
}

决策树如下:

https://narcissusblog-img.oss-cn-beijing.aliyuncs.com/uPic/file-09/ScreenShot2021-09-19%2012.07.40.png

5、解数独

https://narcissusblog-img.oss-cn-beijing.aliyuncs.com/uPic/file-09/ScreenShot2021-09-20%2009.48.42.png

代码如下:

func solveSudoku(board [][]byte)  {
    // 回溯
    var dfs func(board [][]byte, row, col int) bool
    dfs = func(board [][]byte, row, col int) bool {
        m, n := 9, 9
        // 结束条件
        if row == m {
            return true
        }
        for i := col; i < n; i++ {
            if board[row][i] != '.' {
                continue
            }
            for c := '1'; c <= '9'; c++ {
                // 剪枝
                if !isValid(board, row, i, byte(c)) {
                    continue
                }
                // 做选择
                board[row][i] = byte(c)
                // 进入下一层决策
                if dfs(board, row, i + 1) {
                    return true
                }
                // 撤销决策
                board[row][i] = '.'
            }
            return false
        }
        // 进入下一行
        return dfs(board, row+1, 0)
    }

    dfs(board, 0, 0)
}

func isValid(board [][]byte, row, col int, char byte) bool {
    for i := 0; i < 9; i++ {
        // 判断行是否有重复
        if board[row][i] == char {
            return false
        }
        // 判断列是否有重复
        if board[i][col] == char {
            return false
        }
        // 判断小九宫格是否有重复
        if board[(row / 3)*3 + i / 3][(col / 3)*3 + i % 3] == char {
            return false
        }
    }
    return true
}

6、合法括号生成

括号问题可以简单分为两类:一是括号合法性判断;另一个是括号生成。括号合法性判断一般需要用到栈,括号生成一般考虑回溯思想。

关于括号问题,有以下两个特点:

  1. 一个合法括号组合的左括号数量一定等于右括号数量
  2. 对于一个合法的括号字符串组合p,必然对于任何0 <= i < len(p)都有:子串p[0..i]中左括号的数量都大于或等于右括号的数量

https://narcissusblog-img.oss-cn-beijing.aliyuncs.com/uPic/file-09/ScreenShot2021-09-20%2009.52.26.png

代码如下:

func generateParenthesis(n int) []string {
    // 回溯
    var (
        result []string
        dfs func(track []byte, leftNum, rightNum int)
    )

    dfs = func(track []byte, leftNum, rightNum int) {
        // 结束条件
        if len(track) == 2 * n {
            if leftNum == rightNum {
                temp := make([]byte, 2 * n)
                copy(temp, track)
                result = append(result, string(temp))
            }
            return
        }
        // 剪枝
        if rightNum > leftNum || leftNum > n {
            return
        }

        track = append(track, '(')
        dfs(track, leftNum + 1, rightNum)
        track = track[:len(track) - 1]
        track = append(track, ')')
        dfs(track, leftNum, rightNum + 1)
        track = track[:len(track) - 1]
    }

    var (
        track []byte
        leftNum, rightNum int
    )
    dfs(track, leftNum, rightNum)
    return result
}