美文网首页
回溯套路

回溯套路

作者: Robin92 | 来源:发表于2022-06-10 11:28 被阅读0次

回溯的逻辑上应该很好理解,但许久不做题后一想到用回溯,总觉得无法下码,其实是对代码代码结构认识不足。这里整理一下。

回溯是一种暴力方法,在算法中一般要返回所有的组合的时候可以用回溯,如果要返回个数值,应该考虑动态规划等其他方法。

力扣刷题提醒

  • 看力扣题最后的提示,数据量不大的时候用暴力!!!(竞赛拼手速)

基本算法模型设计

  • 回溯问题是一个遍历问题;
  • 在遍历过程中对一个元素有两种处理方式:选择 和 不选择;
  • 在选择和不选择之后,都需要考虑下一个节点,所以一定会用到递归;
  • 如果用了 for 循环,在不选择(第二次处理)之后,可以通过 for 代替这次递归;
  • 一般需要两个容器:选择列表的容器和存放结果的容器;
  • 在选择和或不选择之间,常常需要恢复选择列表容器的“选择场景”;
  • 由上特性,这两个容器应该是引用传递的、或者全局的、或者使用闭包式。
  • 剪枝:如果后面的肯定不符合条件,则结束向后递归。

    剪枝的条件找的全的话也能甩力扣一大波人,有些剪枝条件不好找。

基本模型代码

func backtrace(nums []int) [][]int {
    // 1. init containers: result and temp

    // 2. define and implement dfs
    var dfs func(args ...) // define
    dfs = func(args ...) { // implement
        // 2.1 judge and pruning
        // 2.x judge result and save to []result(根据题意不同位置)

        // 2.2 do choice
        // 2.3 next with choice
        dfs(i + 1)

        // 2.4 recover scenario (do no choice)
        // 2.5 next with no choice
        dfs(i + 1)
    }
    // start call
    dfs(0)

    return result
}

例题

N 个数的全排列

每个节点都发生了取值,但取值的顺序不一样,所以这个递归套路和上方的不太一样了。

func allArrangeN(n int) [][]int {
    nums := make([]int, n)
    for i := 0; i < n; i++ {
        nums[i] = i + 1
    }

    res := make([][]int, 0)
    temp := make([]int, 0, n)

    var dfs func(ns []int)
    dfs = func(ns []int) {
        // pruning
        if len(temp) == n {
            res = append(res, append([]int{}, temp...))
            return
        }
        for i := 0; i < len(ns); i++ {
            // do choice
            temp = append(temp, ns[i])
            // next with do choice
            dfs(append(append([]int{}, ns[:i]...), ns[i+1:]...))
            // recover
            temp = temp[:len(temp)-1]
            // next with no choice
            dfs(append(append([]int{}, ns[:i]...), ns[i+1:]...))
        }
    }
    dfs(nums)
    fmt.Println(res)
    return res
}
// 上述 dfs 可以变成这样,做选择只选第一个元素。看代码会简单一些
// 内存的使用???
    dfs = func(ns []int) {
        // pruning
        if len(temp) == n {
            res = append(res, append([]int{}, temp...))
            return
        }
        for i := 0; i < len(ns); i++ {
            ns[0], ns[i] = ns[i], ns[0] // 每次交换到第一个,做选择也只选择第一个
            // do choice
            temp = append(temp, ns[0])
            // next with do choice
            dfs(append([]int(nil), ns[1:]...))
            // recover
            temp = temp[:len(temp)-1]
        }
    }

求 n 个数中所有 k 个元素的组合

给定 n 即表示 [1, n] 的 n 个数。

力扣:77. 组合

func combine(n int, k int) [][]int {
    if n < k {
        return nil
    }
    // 1. init containers: result and temp
    res := make([][]int, 0, n*n)
    temp := make([]int, 0, k)

    // 2. define and implement dfs
    var dfs func(i int)
    dfs = func(i int) {
        // 2.1 pruning
        if i > n || len(temp) > k || len(temp)+n-i+1 < k {
            return
        }

        // 2.2 do choice
        temp = append(temp, i)
        // 2.x add result
        if len(temp) == k {
            res = append(res, append([]int{}, temp...))
        }
        // 2.3 next with choice
        dfs(i + 1)

        // 2.4 recover scenario (do no choice)
        temp = temp[:len(temp)-1]
        // 2.5 next with no choice
        dfs(i + 1)
    }

    dfs(1)

    return res
}

求数组中所有可能子集

子集包含空集,子集内元素不考虑顺序,结果不考虑顺序

力扣:78. 子集

func subsets(nums []int) [][]int {
    // init containers: result and temp
    result := make([][]int, 0)
    result = append(result, []int{}) // 题意:每个数组一定会有一个空
    temp := make([]int, 0, len(nums))

    // define and implement dfs
    var dfs func(i int) // define
    dfs = func(i int) { // implement
        // pruning
        if i >= len(nums) {
            return
        }

        // do choice
        temp = append(temp, nums[i])
        // add result
        result = append(result, append([]int{}, temp...))
        // next with choice
        dfs(i + 1)

        // recover scenario (do no choice)
        temp = temp[:len(temp)-1]
        // next with no choice
        dfs(i + 1)
    }

    dfs(0)

    return result
}

从矩阵中查看单词是否存在

力扣:79. 单词搜索

func exist(board [][]byte, word string) bool {
    if word == "" {
        return true
    }
    if len(board) == 0 || len(board[0]) == 0 {
        return false
    }
    m, n := len(board), len(board[0])

    moves := [][2]int{{0, 1}, {0, -1}, {1, 0}, {-1, 0}}
    used := make(map[[2]int]struct{}, 0)
    result := false

    var dfs func(i int, cur [2]int)
    dfs = func(i int, cur [2]int) {
        // pruning
        if result {
            return
        }
        if _, ok := used[cur]; ok || !validPos(cur, m, n) || board[cur[0]][cur[1]] != word[i] {
            return
        }
        if i == len(word)-1 {
            result = true
            return
        }
        // do choice
        used[cur] = struct{}{}
        // next with do choice
        for _, move := range moves {
            newCur := [2]int{cur[0] + move[0], cur[1] + move[1]}
            dfs(i+1, newCur)
        }
        // recover scenario (do no choice)
        delete(used, cur)
        // next for loop
    }

    for i := 0; i < len(board); i++ {
        for j := 0; j < len(board[0]); j++ {
            // start
            dfs(0, [2]int{i, j})

            // pruning
            if result {
                return result
            }
        }
    }
    return result
}

func validPos(pos [2]int, m, n int) bool {
    if pos[0] < 0 || pos[0] >= m {
        return false
    }
    if pos[1] < 0 || pos[1] >= n {
        return false
    }
    return true
}

只打败了 5% 【捂脸】:执行耗时:696 ms,击败了5.01% 的Go用户,内存消耗:2.1 MB,击败了6.76% 的Go用户。但回溯的思路没问题,而且和官方给出的思路也差不多。

尝试逐步优化:
用和 board 等量的 bool 矩阵替换 used 后:执行耗时:168 ms,击败了8.97% 的Go用户,内存消耗:1.9 MB,击败了62.02% 的Go用户
传递时用 i, j 代替 [2]int{i,j} 后:执行耗时:128 ms,击败了12.74% 的Go用户,内存消耗:1.9 MB,击败了62.02% 的Go用户

直接用官网给出的方法:执行耗时:96 ms,击败了37.84% 的Go用户,内存消耗:1.9 MB,击败了30.39% 的Go用户
不优化了,不过根据上方的优化变动,可以思考下面两点信息(不排除 力扣 对相同代码每次跑的结果不一样):

  • 频繁用 map 增加删除和用固定长度的数组相比,效率不高
  • 传数组似乎比只传下标更耗时些

(闲来无事做了这个 check,不一定准确,不要以此为准)

有重复元素的所有子集

func subsetsWithDup(nums []int) [][]int {
    res := make([][]int, 0)
    res = append(res, []int{})
    if len(nums) == 0 {
        return res
    }

    sort.Ints(nums)
    counts := make(map[int]int, len(nums))
    counts[nums[0]]++
    cur := 0
    // 去重、统计 counts
    for i := 1; i < len(nums); i++ {
        counts[nums[i]]++
        if nums[i] != nums[cur] {
            cur++
            nums[cur] = nums[i]
        }
    }
    nums = nums[:cur+1]

    temp := make([]int, 0, len(nums))
    var dfs func(i int)
    dfs = func(i int) {
        if i >= len(nums) {
            return
        }
        elem := nums[i]
        count := counts[elem]

        // do choice
        for c := 1; c <= count; c++ {
            // do N choice
            for k := 0; k < c; k++ {
                temp = append(temp, elem)
            }
            res = append(res, append([]int{}, temp...))
            // next with do N choice
            dfs(i + 1)
            // recover N (do no choice)
            for k := 0; k < c; k++ {
                temp = temp[:len(temp)-1]
            }
        }
        // next with do no choice
        dfs(i + 1) // 注意这里必须在上面的 for 外面,否则会导致多次 do no choice
    }
    dfs(0)
    return res
}

执行结果:执行耗时:0 ms,击败了100.00% 的Go用户,内存消耗:2.4 MB,击败了12.90% 的Go用户

尽可能公平地分桶

5289. 公平分发饼干

func distributeCookies(cookies []int, k int) int {
    buckets := make([]int, k)
    n := len(cookies)
    max := func(a ...int) int {
        m := 0
        for _, i := range a {
            if i > m {
                m = i
            }
        }
        return m
    }
    var dfs func(i int)
    minRes := math.MaxInt
    dfs = func(i int) {
        // pruning
        if i == n {
            if m := max(buckets...); m < minRes {
                minRes = m
            }
            return
        }
        // do choice by buckets
        for j := 0; j < len(buckets); j++ {
            // do choice
            buckets[j] += cookies[i]
            // next with do choice
            dfs(i + 1)
            // recover
            buckets[j] -= cookies[i]
            // next with recover
            // dfs(i + 1) // 因为这个元素必须选一个桶放,如果不选就不能向下递归了,所以这里没有此行
        }
    }
    dfs(0)
    return minRes
}

相关文章

  • 回溯套路

    回溯的逻辑上应该很好理解,但许久不做题后一想到用回溯,总觉得无法下码,其实是对代码代码结构认识不足。这里整理一下。...

  • LeetCode 第112题:路径总和II

    1、前言 2、思路 使用回溯的方式来做,这个回溯的套路要记住,记得与之前的回溯写法区分。二叉树的回溯本质上就是左右...

  • 216. Combination Sum III

    题目 分析 这道题和之前的Combination Sum I II类似,都可以用回溯解法。回溯解法的套路是: ch...

  • LeetCode51. N皇后问题

    还是使用回溯法的套路来完成,具体代码如下,这个题目里的回溯其实就是把放了Q的,重置回'.',也就是空棋,这里返回的...

  • 回溯算法解题套路框架

    读完本文,你可以去力扣拿下如下题目: 46.全排列[https://leetcode-cn.com/problem...

  • 4.2 回溯法(2)

    套路 解决全排列问题可以用到回溯 全排列问题往往可以用交换两位置元素的方法,完成后续步骤后,需要回溯时再交换回原来...

  • 回溯算法

    回溯算法 回溯算法介绍   回溯算法并不是非常复杂的算法, 很多人接触过但是未必知道这是回溯算法; 典型的回溯算法...

  • 回溯算法:八皇后问题和0-1背包问题

    文章结构 如何理解回溯算法 回溯算法的经典应用 完整源码 图片来源 1. 如何理解回溯算法 1.1 什么是回溯算法...

  • LeetCode之回溯算法

    回溯法也可以叫做回溯搜索法,它是一种搜索的方式。回溯是递归的副产品,只要有递归就会有回溯。因为回溯的本质是穷举,穷...

  • N皇后

    回溯法核心代码: n皇后问题回溯法

网友评论

      本文标题:回溯套路

      本文链接:https://www.haomeiwen.com/subject/fmzvmrtx.html