目录
思维导图
回溯算法思想及框架
1. 结点
- 活结点:一个自身已生成但其儿子还没有全部生成的结点称作活结点
- 死结点:一个所有儿子已经产生的结点
- 扩展结点:一个正在生成儿子的结点
2.深度优先
- 如果对一个扩展结点R,一旦产生了一个儿子C,就把C当作新的扩展结点,R变成活结点。在完成对子树C(以C为根的子树)的穷尽搜索之后,将R重新变成扩展结点,继续生成R的下一个儿子(如果存在),若R的所有儿子结点都已产生,R变成死结点
3.回溯法
- 当需要找出问题的解集或者要求满足某些约束条件的最优解,常使用回溯法;回溯法的基本方法就是搜索,是一种组织井井有条的,能避开不必要搜索的穷举式搜索法;适用于解一些组合数相当大的问题
- 回溯法在问题的解空间树中,按深度优先策略,从根结点出发搜索整个解空间树;算法搜索至解空间树的任意一点时,先判断该结点是否包含问题的解:不包含,跳过,继续该结点兄弟结点的搜索;否则进入子树,继续按深度优先策略搜索
- 类似于树的深度遍历,但是有本质区别(数据结构中树的深度遍历方法,先创建树,再深度遍历)
- 问题的解向量:回溯法希望一个问题的解能够表示成一个n元组(x1,x2,...xn)
- 显约束:对分量xi的取值限定
- 隐约束:为满足问题的解而对不同分支施加约束
- 解空间的组织形式:子集树(所给问题是从N个元素的集合S中找出满足某种性质的子集),排列树(所给问题时确定n个元素满足某种性质的排列)
- 在搜索过程中动态产生问题的解空间树,即边搜索边扩展分支;在任何时刻,算法中只保存从根结点到当前扩展结点的路径。解空间树从根结点到叶结点的最长路径的长度为h(n),则回溯法所需的计算空间通常为O(h(n)),存储整个解空间子集树需要O(2^h(n)),排列树需要O(h(n)!)的内存空间。
- 4.递归回溯算法框架
参数t表示递归深度,即当前扩展结点在解空间树中的深度。n用来控制递归深度,当t>n时,算法已搜索到叶结点。此时,由Output(x)记录或输出得到的可行解x。算法 backtrack 的for循环中f(n, t)和g(n, t)分别表示在当前扩展结点处未搜索过的子树的起始编号和终止编号。h(i)表示在当前扩展结点处x[t]的第i个可选值。Constraint(t)和Bound(t)表示在当前扩展结点处的约束函数和限界函数。Constraint(t)返回的值为true时,在当前扩展结点处x[1:t]的取值满足问题的约束条件,否则不满足问题的约束条件,可剪去相应的子树。Bound(t)返回的值为true时,在当前扩展结点处x[1:t]的取值未使目标函数越界,还需由Backtrack(t+1)对其相应的子树做进一步搜索。否则,当前扩展结点处x[1:t]的取值使目标函数越界,可剪去相应的子树。执行了算法的for循环后,已搜索遍当前扩展结点的所有未搜索过的子树。Backtrack(t)执行完毕,返回t-1层继续执行,对还没有测试过的x[t-1]的值继续搜索。当t=1时,若已测试完x[1]的所有可选值,外层调用就全部结束。显然,这一搜索过程按深度优先方式进行。调用一次Backtrack(1)即可完成整个回溯搜索过程。
5.迭代回溯算法框架
6.子集树与排列树的回溯算法框架
适用条件
基本步骤
1.(简洁版)针对所给问题,定义问题的解空间;
2.确定易于搜索的解空间结构(树);
3.以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索
- 常用剪枝函数:用约束函数在扩展结点处剪去不满足约束的子树;用限界函数剪去得不到最优解的子树
例题
1.n后问题
- 问题描述:在n*n格的棋盘上放置彼此不受攻击的n个皇后。按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。n后问题等价于,在n*n格的棋盘上放置n个皇后,任何2个皇后不放在同一行或同一列或同一斜线上。以4叉树为例
- 问题的解空间树及关键代码(n叉树)
- 排列树
2.01背包问题
- 问题描述:现有 N NN 件物品和一个容量为 C CC 的背包。放入第 i ( 1 … N ) 件物品耗费的空间是 w i ,得到的价值是 v i 。求解将哪些物品装入背包可使价值总和最大。
- 解向量:n维0-1向量<x1,x2,...xn>,xi=1表示物品i选入背包
- 解空间状态树:0-1取值的二叉树(子集树),有2^n片树叶
- 可行性约束条件:
- 限制函数:一般分数背包问题的贪心
- 部分核心代码:
//回溯函数 void backtrack(int i){ doublebound(int i); if(i>n){ bestp=cp; return; } if(cw+w[i]<=c){ cw+=w[i]; cp+=v[i]; put[i]=1; backtrack(i+1); cw-=w[i]; cp-=v[i]; } if(bound(i+1)>bestp)//符合条件搜索右子数 backtrack(i+1); } //计算上界函数 double bound(int i){ doubleleftw= c-cw; double b =cp; while(i<=n&&w[i]<=leftw){ leftw-=w[i]; b+=v[i]; i++; } if(i<=n) b+=v[i]/w[i]*leftw; returnb; }
3.装载问题
- 问题描述:一批集装箱共n个要装上2艘载重量分别为c1和c2的轮船,其中集装箱i的重量为Wi且W1+W2+……+Wn<=c1+c2;试确定一个合理的装载方案使这n个集装箱装上这两艘轮船。
- 解向量:x{1,2,...n},x1=1表示装入到c1集装箱
- 解空间状态树:子集树
- 可行性约束条件:
- 上界函数(右分支):当前载重量cw+剩余集装箱的量r<=当前最优载重量bestw
- 最优装载方案:首先将第一艘轮船尽可能装满;将剩余的集装箱装上第二艘轮船
- 部分核心代码:
void backtrack(int i){//搜索第i层结点
if(i>n){//到达叶结点
更新最优解bestx,bestw;
return ;
}
r -= w[i];//r初值为全部集装箱重量之和
if(cw+w[i]<=c){//搜索左子树
x[i]=1;
cw += x[i];
backtrack(i+1);
cw -= w[i];
}
if(cw+r>bestw){//搜索右子树
x[i]=0;
backtrack(i+1);
}
r += w[i];
}
4.图的着色问题
- 问题描述:给定无向连通图G和m种不同的颜色。用这些颜色为图G的各顶点着色,每个顶点着一种颜色。如果有一种着色法使G中每条边的2个顶点着不同颜色,则称这个图是m可着色的。图的m着色问题是对于给定图G和m种颜色,找出所有不同的着色法。
- 解向量:(x1,x2,...,xn)表示顶点i所着颜色xi
- 可行性约束条件:顶点i与已着色的相邻顶点颜色不重复
- 注意:在邻接图g[][]中,g[i][j]=g[j][i]
- 部分核心代码:
void backtrack(int t){//搜索第i层结点 if(i>n){ sum++; } else{ for(int i=1; i<=m; i++){ x[t]=i; } if(ok(t)) backtrack(t+1); } } boolean ok(int k){//检查颜色可用性 for(int j=1; j<k; j++){ if(a[k][j] && (x[j]==x[k])) return false; } return true; }
5.旅行售货员问题
- 有n个城市,找从一个城市出发走遍n个城市的最短回路问题
- 解向量:{x1,x2,...,xn},xi表示走的第i个城市是xi
- 解空间树:排列树
- 约束条件:存在点k-1到当前搜索点k的通路,并且加上到k的路径长度小于当前最短路径长度
- 部分核心代码:
void dfs(int k){ if(k>n){ if(g[x[n]][1]!=rmax && c+g[x[n]][1]<bestc){ for(int i=2; i<=n; i++){ bestx[i]=x[i]; } bestc=c+g[x[n]][1]; } return ; } else{ for(int i=k; i<=n; i++){//是否可以进入x[i]子树 if(g[x[k-1]][x[i]]!=rmax && (c+g[x[k-1]][x[i]]<bestc)){ swap(x[k],x[i]); c=c+g[x[k-1]][x[k]]; dfs(k+1); c=c-g[x[k-1]][x[k]]; swap(x[k],x[i]); } } } }
6.最大团问题
- 问题描述:给定无向图G=(V,E),其中V是顶点集;E是V边集。如果U属于V,且对任意两个顶点u,v∈U有(u,v)∈E,则称U是G的完全子图。G的完全子图U是G的一个团当且仅当U不包含在G的更大的完全子图中。G的最大团是指G中所含顶点数最多的团。如果U属于V,且对任意u,v∈U有(u,v)不属于E,则称U是G的空子图。G的空子图U是G的独立集当且仅当U不包含在G的更大的空子图中。G的最大独立集是G中所含顶点数最多的独立集。对于任一无向图G=(V,E),其补图G'=(V',E')定义为:V'=V,且(u,v)∈E'当且仅当(u,v)∉E。如果U是G的完全子图,则它也是G'的空子图,反之亦然。因此,G的团与G'的独立集之间存在一一对应的关系。特殊地,U是G的最大团当且仅当U是G'的最大独立集。
- 解向量:x{1,...i}
- 解空间:子集树
- 可行性约束条件:顶点i到已选入的顶点集中每一个顶点都有边相连
- 上界函数:有足够多的可选择顶点使得算法有可能在右子树中找到更大的团
- 部分核心代码:
void Backtrack(MCP &G,int i){ if (i>G.v){ //output()阶段 for (int j=1; j<=G.v; j++) G.bestx[j] = G.x[j]; //记录最优解 G.bestn =G.cnum; return ; } //检查顶点i与当前团的连接 int OK = 1; for (int j=1; j<=i ; j++) if (G.x[j]&& G.a[i][j]==0){ //G.x[j]:顶点j在当前解的最大团内;G.a[i][j]:待考察i顶点与最大团中前i-1个顶点间边的关系 //i不与j相连 OK = 0; break; } if (OK) { //进入左子树 G.x[i] = 1;//把i加入团 G.cnum++; Backtrack(G,i+1); G.x[i]=0; G.cnum-- ; } if (G.cnum+G.v- i>G.bestn){ //进入右子树——剪枝函数 G.x[i] = 0; Backtrack(G,i+1); } }
回溯算法的效率的影响因素
- 产生x[k]的时间;满足显约束的x[k]的个数;计算约束函数的时间;计算上界的时间;满足约束条件和上界函数约束的所有x[k]的个数