用最通俗的话解释回溯思想:
:盆友,玩过联盟吗?
:没有啊,好玩吗?
:好玩啊,你试试
:mmp,试过了,不好玩,还是吃鸡去,吃鸡使我快乐
你的目的是快乐,发现打联盟不能使你快乐,马上找另外一条能使你快乐的路
------------------------------(一条很正经的分割线)---------------------------------------------
回溯思想其实也可以叫做试探思想
有时候我们需要得到问题的解,先从其中某一种情况进行试探,在试探的过程中,一旦发现原来的选择是错
误的,那么就退回一步重新选择,然后继续向前试探,反复这样的过程直到求解出问题的解(试探思想充斥
在生活的各个地方)
其实一句话:回溯就是带优化的穷举,归根结底,还是一种有暴力的影子在里面,只不过带剪枝而已
牢记6个字:带优化的穷举,记住了回溯想不懂都难
看看最正统的回溯思想概念:
很官方很学术的一种解释,但无疑是挑不出毛病的,但是第一次看的话,比较难懂,需要注意的是采用回溯
法的时候我们是边搜索边构造状态空间树,想想,如果我们一开始就是把状态空间树全部构造出来,然后再
搜索的话,会消耗很大的空间,且在Cutting的时候大部分的枝叶都剪掉了,全部构造出来完全没有必要嘛
回溯与分支限界很类似,回溯是DFS+Cutting,而分支限界是BFS+Cutting
如果要用回溯求解问题的所有解,则要回溯到根,且根结点的所有子树都已经被搜索才结束
如果只要求解任一解,则只要搜索到问题的一个解就可结束
来看几个经典样例:
经典样例1:(01背包问题的回溯解法)
01背包问题真的是一个老生常谈的问题,分治,dp,贪心,回溯到处都可以见到它,用分治的话效率低
下,用贪心的话解不出来,但是可以求出近似最优当作参考
第一步:画解空间树
A
1 / 0\
B B
1 / 0\ 1/ 0\
C C C C
A,B,C三个物品,1代表选择,0代表不选
这就是所谓的解空间树,搜索到叶子结点,问题也就结束了
第二步:设计Cutting函数
有时候做题,超时还是不超时,完全就是看你的Cutting函数设计的怎么样,回溯的重点不在于解空间树,
而是在于Cutting函数的设计
CUtting函数有两个分类:
第一个:约束函数:即不满足约束条件,比如01背包问题中的背包装不下了
第二个:限界函数:即这条路走了,没有走另外一条路好,少年,回头吧,走另外一条路
下面开始我们Cutting函数的设计
最简单的一个:背包装不下的时候肯定不能再装该物品
即:要求已经装了的物品重量加上该物品的重量<=背包容量
即装得下就装,装不下就剪
这个是考虑得装不下的情况,即剪的是右枝
现在考虑剪左枝的情况
左枝:即当前考虑的物品不装入背包的Cutting函数
如果当前已经选了的物品的总价值加上该物品后面可以装的物品的价值大于最优总价值
那么当前物品就没有必要装
为什么呢?这个可能很难理解
因为我装后面的物品得到的价值比当前的最优价值还大,那我当前物品完全就没有装的必要嘛
最优价值是更新的
贴个代码:
#include<bits/stdc++.h> using namespace std; #define max_v 105 int n; int v[max_v],w[max_v]; int c;//背包容量 int cv=0;//当前装的物品的总价值 int cw=0;//当前装的物品的总重量 int bestv=0;//最优总价值 int bestx[max_v];//最优解 int x[max_v]={ 0};//当前解 int bound(int i) { int l=c-cw; int b=cv; while(w[i]<=l&&i<=n) { b+=v[i]; l-=w[i]; i++; } if(i<=n) b+=l*v[i]/w[i]; return b; } void dfs(int i) { if(i>n)//搜完了一条路 { for(int j=1;j<=n;j++) bestx[j]=x[j]; bestv=cv; }else { if(cw+w[i]<=c)//装得下 { x[i]=1;//该物品装 cw+=w[i]; cv+=v[i]; dfs(i+1);//搜索下一个物品 x[i]=0;//回退 cw-=w[i]; cv-=v[i]; } if(bound(i+1)>bestv)//最优总价值小于上界,当前已经选了的物品的总价值加上该物品后面可以装的物品的价值大于最优总价值,那么当前物品就没有必要装 { x[i]=0; dfs(i+