回溯法Q&A
Q:为什么写这个?
A:1,觉得今天徐云老爷子讲的实在是一般(以下省略1k字的的评价),2,闲的蛋疼
Q:什么是搜索算法,为什么要搜索
A:一般的搜索算法也就是所谓的暴力算法,也就是对所有可能的解逐一的试验,看是否是问题的解。我们知道计算机科学里面有很多问题不是都有算法课上所说的很优雅的算法的,最直接的例子就是所谓的NP问题,这类问题到现在为止基本没有高效的哪怕是N^100解决的算法,只能用最原始的试解的办法去解决。但是得益于计算机硬件的高速发展,使得搜索算法的应用空间越来越大….以下歌功颂德1w字 略。任何问题都可以用搜索算法解决,因为它遍历了全部可能解,但是显然会很慢。
Q:那什么是回溯算法?和深度优先搜索算法有什么关系?
A:试解也要有一个过程,要系统的试,保证每个可能情况都要试到,不漏不重,所以就有了各种各样的搜索算法,比如深度优先搜索,广度优先搜索。他们的区别只在于构造可能解的方式不用,本质是一样的,都是试解~回溯算法可以认为就是一种采用深度优先方式来构造可能解的搜索算法。
Q:那为什么不直接叫深度优先搜索算法,而要叫回溯?
A:可能是pretend B?其实是可以这么叫的,一般来说确实平时在说到深度优先搜索算法就指的是回溯法的,但是其实深度优先搜索算法的概念更广一些,有很多算法采用的是深度优先,不过由于算法的一些特点,所以有一些特殊的名字,就是这样
Q:你为什么要告诉我这些废话?
A:……T_T
Q:怎么搜索?
A:正确答案是爱怎么搜怎么搜,也就是说你想个办法来生成所有可能的解,然后一个一个去试试,就是这么个过程,正因为要试全部可能的解,所以搜索算法很慢,但是绝对可以保证找到问题正确的解(如果你确实搜过了全部可能的解的话)。
Q:那为什么要有这么多的搜索算法?
A:一般来说搜索算法如果不做任何优化那么执行效率(也就是搜索的可能解的个数)是一样的因为一定要搜全部,但是这里还涉及一个搜索算法效率优化的问题,是,我们是要保证搜索到全部的可能的解,但是如果我们一步步搜索的过程中发现某个可能解在构造了一办已经和问题不符合已经不可能是解了,我们就没必要继续构造下去,这个就是所谓搜索算法里面的剪枝,不同的搜索算法就是为了尽快的找到那些不可能的解,从而搜索少一些的可能解,来达到提高搜索算法效率的目的。
Q:那什么是深度优先?
A:深度优先的概念就是在一棵树中(注意,深度优先不一定是针对树来说的概念,但即使不是对树结构作深度优先,那遍历完了之后肯定是一个棵树),我们从树根开始,一步步的想下走,只要还有没访问的子节点,就向下,直到走到叶节点,这个时候没路了,我们就退回到上一个节点,这个就是回溯,举个例子来说,看下面的这棵树:
深度优先的过程是这样的,从A开始,向子节点走,到B,再到D,这时候到了叶子节点了,于是我们回到B,检查B有没有没走过的子节点,有,于是我们走到了E,然后H,又是叶子节点,于是退会E,进入I,然后退回E,这时候E已经没有没访问过的叶子节点了,于是我们退回B,B节点也访问的叶子节点了,于是退回A,进入C…..这样下去,直到每个节点都被访问过。这个就是深度优先的过程。如果你问会不会有节点没访问到?不会,我这是在告诉你什么是深度优先遍历,真正的程序实现需要借助一定的数据结构来实现,遍历整个数据结构就能保证不会出现遗漏或者重复。也就是说,深度优先就是一种保证不漏不重的遍历所有树节点的方法。
Q:那图的深度优先遍历呢?
A:深度优先遍历的思想掌握了,再去看徐云老爷子的ppt吧,懒得说。
Q:那好吧,我已经理解了深度优先遍历,那这和回溯法有什么关系“
A:深度优先中当没有没访问过的子节点或者本身就是叶节点,往上退的那个过程就叫回溯
Q:那回溯法解决问题到底怎么解决?
A:回溯法适合解决的问题我们可以这么描述,就是….不说数学形式化的定义了,这么说吧,就是从一个最小的问题,有几个选择,从这些选择里面找出一个或者一些选择的组合使得某个问题得到满足。比如说0-1背包问题,举一个三个物品的例子物品重量是{10,2,30},那这个问题就可以这样描述,对于每个物品(就是我说的最小的问题)我们有两个选择,放进背包还是不放进背包,也就是说每一个可能解对应一个向量{x1,x2,x3},每个xi可能是0或1,0表示不放,1表示放,于是一共有2×2×2=8中可能的情况。我们用一个节点表示一个物品,向左的线表示放进去,向右的线表示不放,于是这个问题就可以得到下面的图:
徐sir只画了两层,大家就莫怪我,下面有个完整的例子,但是这张图更强调层的概念
这里要提醒大家注意的是虽然第三个节点标着3,事实上它表示的是第二个物品,也就是说一层表示一个物品,那为什么一层会有多个节点?因为还要考虑上一个物品的取舍情况,比如说标号为2的节点表示的就是把物品1放进背包的时候,第二个物品的决策状态,标号为3的节点就是物品1不放进背包的决策状态,所以这两个都是物品2.也就是老爷子左边标的x1,x2的意思。
这个树也就是所谓的解空间树,也就是说所有可能的解都在这棵树上了,那怎么找到可能的解?到也节点去,刚才我说过,一层表示一个物品到底是要还是不要,那从第一层到最后一层的一条路径不就决定了一个背包的装载状态。当然可能不满足条件,这就是搜索的任务了。
所以我可以说,回溯算法的即使用一个节点表示一个最小问题的决策状态,每一层表示一个小问题,一条从根节点到叶子节点的路径就表示了一个可能的解,换句话说,要得到问题的可能解,一定是搜索叶子节点。这是不是很符合深度优先的概念,就是每次我们从根节点开始,一直往下(对应一直做一个决策)一直搜到叶子节点(对应决策完了),这样得到一个可能解,然后再退,在决策,深搜嘛!
Q:你烦了没?
A:烦了,不想写了…我觉得讲到这里可以去看Ppt了,应该比较好理解了,最后说一个用回溯法解决3个物品的背包问题的具体过程(徐老爷子ppt12_13页的例子,本来有动画的,转成pdf就没了。。老爷子真小气):
我们看这个树,这就是按照上一个问题里面描述的方法构造的3 个问题的解空间树。等等,你不是说一层表示一个物品么,为什么三个物品有4层?我说的不是节点的层,我说的是边的层….3层,没错,节点标号是为了好说。我们具体的看这个问题,从A开始,到B,表示第一个节点放进背包,深度遍历,到D,再到H,是个叶节点,于是我们得到一个可能解,(1,1,1)就是下面标的那个,这意思就是三个东西都放进去对吧,然后我们这个时候算法就因该检验这个可能解是不是最优解,这个我们先不管,我们重点考虑回溯的过程,验证完之后因为是叶节点我们要回退,到D,这就是回溯了,从D到I,得到可能解(1,1,0)验证,完了退回D,D已经没有没访问过的页节点了,于是回退到B,进入E,以此类推,每次退,就是回溯,每次到叶节点,就是可能解,所有叶节点遍历完了,解空间也就遍历完了。就是这样,谁能搞到动画版的ppt看整个过程会更清楚。搞不到就理解深度优先遍历,然后自己算算看,一样。
Q:….那你还有什么要说的么?
A:我说一个应该是很重点但是老爷子没提的东西。就是算法怎么实现的问题。相信大多数人多有这样的问题,回溯算法理解了,但是为什么要怎么实现?一看老爷子的代码,居然还是用递归..立刻泪奔。。。递归不好理解啊。其实回溯法是有算法框架的,稍微讲一下。比如说我们还是看那个背包问题,我们把问题扩大一点,比如说k个物品,如果有一个理想的语言,我们可以这么写:
for(第一个物品)
{
设置放进去或者不放
for(第二个物品)
{
设置放进去或者不放
......
…….
好多的循环,具体的说是k个
for(第k个物品)
{
放进去或者拿出来
检验是不是最优的,是就做点什么,不是就算了
}
}
}
就是我们用k个循环,每个负责设置一个物品放进去或者不放的状态,在第k个循环里面验证这个可能解是不是最优的,是就做点什么,不是,就继续执行。
但是有问题,首先,k不是事先知道的,第二,即使知道k多大,也没用,万一是10000呢?
但是我们注意到每个for做的都是一样的事情,所以,可以写个小函数来实现
如下
//函数表示设置i物品放或者不放
Func(int i)
{
设置放进去,或者不放
}
如果我们在函数func里头再调用func,也就是所谓的递归调用,然后传给他们不同的参数,那不就实现了上面的循环么?
所以,背包问题的回溯框架是
void func(int i)
{
if (i == k+1)
{
到叶节点了,找到一个可能解了
验证,做点什么
}
for(int t = 放 和 不放)//这里是循环,t有两个状态
{
设置物品i的状态为t
func(i+1);
}
}
解决了~也就是说我们利用的是计算机内部的递归栈来实现那些循环的嵌套(当然执行过程会和直接用循环不一样,但是那不重要,重要的是效果是一样的)
所以,回溯法一般的搜索框架也就出来了,这样的:
void func(int i)
{
if (i == k+1)
{
到叶节点了,找到一个可能解了
验证,做点什么
}
for(int t = 一个子问题的各种状态)
{
设置字问题i的状态为t
func(i+1);
}
}
还是那句话,用一个函数表示一个小问题,在函数里面设置各种状态,同时调用下一个子问题的函数,就递归了.
Q:好吧,我掌握回溯算法了
A:早着呢,这只是最粗糙的回溯算法,其实这个小文章只能说是回溯算法的扫盲帖,只是教你怎么实现一个回溯算法,真正的回溯算法(或者说所有搜索算法)的关键在于剪枝,就是怎么减少搜索可能解的数量,难着呢,具体的看ppt里面在递归调用前面的那个if()判断就是剪枝了,如果没有通过if判断,就不用递归调用了,就剪枝了…...最后,enjoy algorithm~
Q:你以后是想做老师么?
A:不,我以后想做狗仔队~