从全排列看回溯算法

从全排列看回溯算法

最近又刷起了算法,仿佛回到了大一时奋战到深夜场景,走上社会之初发现大学里学的都是啥玩意儿,工作中基本遇不到,各种数据结构都被封装的妥妥的根本不需要我们去操心,以至于越来越浮于表面。

现在觉得大学的课程是真功夫,是无数学者总结提炼的精华,是计算机从业人员是基本功,基本功不扎实很快就会遇到瓶颈,对算法与数据结构掌握与理解不透彻很难写出非常优秀的软件,亡羊补牢为时不晚,所以拿起旧书本回炉重造磨练自己基本功。 学习算法不仅会收获很多还会给你带来成就感。

下面用通俗的方式结合例子给大家介绍回溯算法

回溯算法框架

func backtrack(选择列表,路径) {
   if 结束条件 {
       得到一种结果
   }
   for i in 选择列表 {
      if 减支条件 {
         continue
      }
      选择列表加入路径
      backtrack(选择列表,路径)
      撤销选择
   }
}

这个先看不太懂没关系,读完全文可再返回阅读。 回溯算法本质就是一个多叉树遍历问题

我们以在袋子里抓球为力来解释一下上面几个名词。 假设袋子里三个球,抓一个那么就有三种选择,所以选择列表是:[1,2,3] , 如果你抓到1,那么[1]便是路径,对应的是树的树枝。

结束条件:比如你只抓一次,那么结束条件就是路径长度等于1减支条件:比如抓完放回球,那么就没有减支条件,如果抓完不放回那么条件就是路径里如果已经存在就不再遍历。 很多时候不同的问题都是在减支条件这里做手脚,比如N皇后问题.撤销选择:为啥要撤销选择?其实就是在遍历到叶子节点之后我们需要重新返回到父节点重新寻找其它路径

全排列

给定一个不幸串,输出它的全排列

先来看个最简单的场景:

袋子里有两个球,取出一个记下,放回袋子,再取一个,有多少种结果

输入:[1,2]输出:[[1,1],[1,2],[2,1],[2,2]]

这样就得出一个二叉树:

lC59oR.jpg

所有到叶子节点的路径就是我们需要求解的解集,所以这个问题变成了一个多叉树遍历问题:

func tree(选择,路径){
   结束条件
   遍历分叉
     进入节点前干啥
     递归节点
     遍历节点后干啥
}

所谓回溯,就是在遍历节点后撤销我们选择路径中的选择,所以想一下第一次遍历:lC5iJx.jpg已经走到了一个叶子节点,这时我们已经得出一个解[1,1]并且已经把它存在res的结果中。

所以我们现在想从叶子节点A走回到B,下一步往C去走。这样在回溯到B之前路径是[1,1],回溯之后路径变成[1], 然后递归遍历到C时路径变成[1,2]得到第二个解

res [][]int
func tree(nums []int, track []int) {
   if len(track) == 2 //我们取两次 {
       // 取到了一个结果
       res = append(res,track)
   }
   for _,n := range nums {
      // 遍历选择列表,把选择列表加入到路径中,所以选择列表多长就是多少叉树
      track = append(track, n)
      tree(nums, track)
      track = track[:len(track) - 1] // 撤销路径最后一个选择,在此之前已经遍历到叶子节点并把解记录到了res中,因为递归时已经满足了结束条件
   }
}

有了回溯法,寥寥几行代码就解决了上面问题。下面来加大一下难度:

全排列

一串不重复的数字,输出其全排列,如:

输入:[1,2]输出:[[1,2],[2,1]]

一眼就能看到结果是上面题目的子集,说明啥?多叉树被剪枝了! 如何剪枝?

很简单,满足一定条件啥也不做就行,不去选择->递归->撤销选择, 所以:

func tree(选择,路径){
   结束条件
   遍历分叉
     if 满足剪枝条件
        continue
     进入节点前干啥
     递归节点
     遍历节点后干啥
}

问题就简化为剪枝条件是啥?

显然特点就是已经出现在路径中的元素就不能再选择:

lC5pw9.jpg

代码其它部分不变,for循环里变成:

for _,n := range nums {
   if has(track,n) { //表示track列表中包含n
      continue
   }
   track = append(track, n)
   tree(nums, track)
   track = track[:len(track) - 1] // 撤销路径最后一个选择,在此之前已经遍历到叶子节点并把解记录到了res中,因为递归时已经满足了结束条件
}

轻松搞定

有重复元素的全排列

现在假设选择列表nums中有重复元素如[1,1,2,3]那又该怎么做?

聪明人立马会意识到,其它不变,只是剪枝条件发生了变化:

  1. 选择列表中的元素没有被遍历过
  2. 任何节点的树枝不能重复

要注意不能被重复剪枝,在判断是不是重复时不用考虑已经被剪枝的树枝

lC4xL4.jpg

所以最主要的是修改剪枝条件,但是判断某个元素是否被访问过我们需要引入一个数组来保存选择列表某个元素是否被访问

        if flag[i] {
            continue
        }
        if Has(num[:i], num[i], flag) {
            continue
        }

        track = append(track, num[i])
        flag[i] = true
        back(num, flag, track)
        track = track[:len(track)-1]
        flag[i] = false

func Has(a []int, b int, flag []bool) bool {
    l := len(a)
    if l == 0 {
        return false
    }
    for i := 0; i < l; i   {
        if flag[i] {        // 细节,不用考虑已经被剪枝的树枝
            continue
        }
        if a[i] == b {
            return true
        }
    }

    return false
}

至此你已经掌握了回溯算法的精髓,然后就是活学活用推广到其它问题中。一定要动手再细细琢磨才能触类旁通。

N皇后问题

在一个N*N的棋盘上摆N个皇后,彼此不攻击对方的摆法。

有了回溯算法的基础此问题就变得简单了。

一行一行的放皇后,第一行就有N种放法,如此就又变成了一颗N叉树,思考三个核心元素: 选择列表是啥,路径是啥,剪枝条件是啥

  1. 选择列表就可以用一个N位数组
  2. 路径可以用二维数组
  3. 剪枝条件就变成放的位置横竖斜有没有皇后

如此问题便可解决,如果建议学完回溯算法再拿N皇后稳定巩固一下。

本文由博客一文多发平台 OpenWrite 发布!

发布了44 篇原创文章 · 获赞 5 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/github_35614077/article/details/103685703