我觉得,回溯的算法语言很像是在做一个对人在现实中做决策情况的模拟,对于不确定对不对的决策,先试试,不行再撤销。
在不剪枝的情况下,通过选择和撤销,回溯法(或者说其实就是dfs)可以遍历决策树的全部节点,因为很适合做一些枚举全部可能解的工作。剪枝则可以快速探寻最优解问题(虽然不如分支限界)
为了能够顺利的选上和撤销,我们需要构建合适的抽象语言来描述当前的状态
作出尝试,改变相应的状态参数
递归的探索下一个分支点
在到达叶子节点或是出现某个条件不满足的情况时返回
每次返回时,恢复状态参数至尝试前的样子,以便进行可能还有的其他尝试。
把对某个元素的选与不选看作一个节点的左子树和右子树,那么对子集的枚举就是对一棵树所有叶节点的枚举。利用dfs的框架,再利用一个visit数组,就可以在遍历过程中记录当前路径的选择情况,最后在到达叶子节点的时候完成打印。
?void print(){//在到达叶子节点时打印当前分支的结果 ? ? ?for(int i = 1; i <= n; i++){ ? ? ? ? ?if(vis[i] == 1) cout << a[i] << " "; ? ? } ?} ?void dfs(int step){ ? ? ?if(step > n) {//到达叶子节点,退出 ? ? ? ? ?print(); ? ? ? ? ?return; ? ? } ? ? ?vis[step] = 1;//选当前节点 ? ? ?dfs(step + 1);//选了当前节点的前提下,继续遍历下一层 ? ? ?vis[step] = 0;//不选 ? ? ?dfs(step + 1);//不选当前节点的前提下,继续遍历下一层 ?}
排列问题也可以形成自己的决策树,但不再是二叉树,而是一棵从上往下分支数量逐渐减少的树,但依旧可以使用树的遍历方法来解决此问题。同时需要一个额外的数组来记录路径。
?void dfs(int step){ ? ? ?if (step > n){//叶子节点 ? ? ? ? ?print(); ? ? ? ? ?return; ? ? } ? ? ?for (int i = 1; i <= n; i++) ? ? { ? ? ? ? ?if (a[i]) continue; //已经用过了,跳过 ? ? ? ? ?a[i] = true; ? ? ? ?//没用过则用它 ? ? ? ? ?b[step] = i; ? ? ? ?//记录路径 ? ? ? ? ?dfs(step + 1); ? ? //继续搜索 ? ? ? ? ?a[i] = false; ? ? ? //当前路径用完退出了,还原,这样其他路径才能用 ? ? } ?}
?// flag1[j]记录第j列有没有皇后 ?// flag2[i + j]记录行列和为i+j有没有皇后 ?// flag3[i - j + n]记录行列差为i-j+n有没有皇后 ?void dfs(int step){ // step代表第几行 ? ? ?if(step > n) { // 找到一个可行解后,打印 ? ? ? ? ?for(int i = 1; i <= n; i++) ? ? ? ? ? ? ?cout << a[i] ?<< " "; ? ? } ? ? ?// 对每一行,尝试1 ~ n位置 ? ? ?for(int y = 1 ; y <= n; y++){ ? ? ? ? ?int x = step; ? ? ? ? ?//同列,同斜线,同反斜线有皇后,不能放 ? ? ? ? ?if(flag1[y] || flag2[x + y] || flag3[x-y+n] ) continue; ? ? ? ? ?a[x] = y;//如果有不冲突位置,放上,并更改一系列flag ? ? ? ? ?flag1[y] = true; ? ? ? ? ?flag2[x + y] = true; ? ? ? ? ?flag3[x-y+n] = true; ? ? ? ? ?//在此状态下继续试下一行 ? ? ? ? ?dfs(x + 1); ? ? ? ? ?//试完恢复状态 ? ? ? ? ?flag1[y] = false; ? ? ? ? ?flag2[x + y] = false; ? ? ? ? ?flag3[x-y+n] = false; ? ? } ?}
构建了上界的概念,用于剪枝。基本遵循了选择-改状态-step+1-恢复状态的回溯标准步骤,在标准步骤执行完以后增加了不选当前节点的情况并判断是否剪枝,且因为不选,没改变状态,所以该分支只有进一步搜索,没有状态改变和恢复的过程。在叶子节点不是打印,而是判断是否更新最佳价值。
?int bound(int x){ //计算价值上界 ? ? ?int rw = 0; //rp:第x个商品~第n个商品全部装入的总价值,先初始化为0 ? ? ?while(x<=n) { ? ? ? ? ?rw+=w_i[x]; ? ? ? ? ?x++; ? ? } ? ? ?return rw+curw; //返回当第t个商品不装时,返回前t个商品(不包括第t个)的总价值+剩余的全部商品价值 ?} ?void dfs(int step){ ? ? ?if(step > n){ //某个分支搜索到叶子节点 ,找到一个可行解 ? ? ? ? ?if(curw > bestw){ //如果当前价值大于最优价值,更新最优解 ? ? ? ? ? ? ?bestw = curw; ? ? ? ? } ? ? ? ? ?return ; ? ? } ? ? ?if(curv + v_i[step] <= v){ //判断放入第step个物品不超重,尝试放入 ? ? ? ? ?x[step] = 1; //标记放入 ? ? ? ? ?curv += v_i[step]; // 累加体积 ? ? ? ? ?curw += w_i[step]; //累加价值 ? ? ? ? ?dfs(step+1); ? ? ? ? ?curv -= v_i[step]; // 回溯,恢复当前体积 ? ? ? ? ?curw -= w_i[step]; // 回溯,恢复当前价值 ? ? } ? ? ?if(bound(step + 1) > bestw){ //判断不放入第step个物品,是否还有必要继续在该分支下搜索 ? ? ? ? ?x[step] = 0; // 尝试不放入第i个物品 ? ? ? ? ?dfs(step+1); ? ? } ?}