回溯法
基本思想
从初始状态出发,搜索其所能到达的所有状态。当一条路走到尽头,再后退一步或若干步,从另外一种状态出发,继续搜索,直到所有的路径都搜索过。
相关概念
- 搜索策略:深度优先为主,也可以采用广度优先,函数优先,广度深度结合等
- 约束函数:在扩展节点处减去不满足约束条件的子树
- 界限函数:在扩展节点处剪去得不到最优解的子树
- 结点状态:白结点(尚未访问)、灰节点(正在访问以该节点为根的子树)、黑结点(以该节点为根的子树遍历完成),白结点和灰节点为活结点,黑结点为死结点
- 子集树:当所给的问题是从n个元素的集合S中找出满足某种性质的子集是,相应的解空间树称为子集树。子集树通常有 m n m^n mn个叶结点(n皇后问题)
- 排列树:当所给的问题是确定n个元素满足某种性质的排列时,相应的解空间树称为排列树。排列树通常有 n ! n! n!个叶结点(货郎担问题)
相关概念 | 描述 |
---|---|
问题解的表示 | 将问题的解表示成一个n元式 ( x 1 , x 2 , . . . , x n ) (x_1,x_2,...,x_n) (x1,x2,...,xn)的形式。如: ( x 1 , x 2 , . . . , x 4 ) (x_1,x_2,...,x_4) (x1,x2,...,x4) |
显示约束 | 对分量 x i x_i xi的取值限定。如: x i ∈ { 1 , 2 , 3 , 4 } x_i \in \{1,2,3,4\} xi∈{1,2,3,4} |
隐示约束 | 为满足问题的解而对不同分量之间施加的约束。如: ( x i , x i + 1 ) ∈ E , ∀ i , k ∈ { 1 , 2 , 3 , 4 } , x i ≠ x k , ( x n , x 1 ) ∈ E (x_i,x_{i+1}) \in E,\forall i,k \in \{1,2,3,4\},x_i \neq x_k,(x_n,x_1) \in E (xi,xi+1)∈E,∀i,k∈{1,2,3,4},xi=xk,(xn,x1)∈E |
解空间 | 解向量满足约束条件的所有多元组,构成了问题的一个解空间。如:满足 x i ∈ { 1 , 2 , 3 , 4 } , ( x i , x i + 1 ) ∈ E , ∀ i , k ∈ { 1 , 2 , 3 , 4 } , x i ≠ x k , ( x n , x 1 ) ∈ E 的 所 有 ( x 1 , x 2 , . . . , x 4 ) x_i \in \{1,2,3,4\},(x_i,x_{i+1}) \in E,\forall i,k \in \{1,2,3,4\},x_i \neq x_k,(x_n,x_1) \in E的所有(x_1,x_2,...,x_4) xi∈{1,2,3,4},(xi,xi+1)∈E,∀i,k∈{1,2,3,4},xi=xk,(xn,x1)∈E的所有(x1,x2,...,x4) |
存储 | 当前路径 |
适用特征
-
适用于搜索问题和优化问题
-
必要条件:多米诺性质
设 P ( x 1 , x 2 , . . . , x i ) P(x_1,x_2,...,x_i) P(x1,x2,...,xi)是关于向量 < x 1 , x 2 , . . . , x i > <x_1,x_2,...,x_i> <x1,x2,...,xi>的某个性质,那么 P ( x 1 , x 2 , . . . , x i + 1 ) P(x_1,x_2,...,x_{i+1}) P(x1,x2,...,xi+1)真蕴含 P ( x 1 , x 2 , . . . , x i ) P(x_1,x_2,...,x_i) P(x1,x2,...,xi)为真,即 P ( x 1 , x 2 , . . . , x i + 1 ) − > P ( x 1 , x 2 , . . . , x i ) ( 0 < i < n ) [ n 为 向 量 维 数 ] P(x_1,x_2,...,x_{i+1})->P(x_1,x_2,...,x_i)(0<i<n)[n为向量维数] P(x1,x2,...,xi+1)−>P(x1,x2,...,xi)(0<i<n)[n为向量维数]
设计要素
- 针对问题定义解空间
- 问题解向量
- 解向量分量取值集合
- 构造解空间树
- 判断问题是否满足多米诺性质
- 搜索解空间树,确定剪枝函数
- 确定存储搜索路径的数据结构
算法框架
- 递归回溯
//t:递归深度
//n:最大递归深度
//x:解向量
//f(n,t),g(n,t):当前扩展结点处未搜索的子树的起始编号和终止编号
//h(i):当前扩展结点x[t]的第i个可选值
//Constraint(t),Bound(t):当前扩展结点的约束函数,界函数
void Backtrack(int t){
if(t>n)//搜索到叶结点
Output(x);
else{
for(int i=f(n,t);i<=g(n,t);i++){
x[n] = h[i];
if(Constraint(t)&&Bound(t))
Backtrack(t+1);
}
}
}
- 迭代回溯
//t:迭代深度
//Solution(t):判断当前扩展结点处是否得到问题的可行解
void IterativeBacktrack(void){
int t = 1;
while(t>0){//退到无路可退,结束
if(f(n,t) <= g(n,t)){
for(int i=f(n,t);i <= g(n,t);i++){
x[t] = h[i];
if(Constrain(t)&&Bound(t)){
if(Solution(t))
Output(x);
else
t++;//等价于 Backtrack(t+1)
}
}
}
else
t--;//走不通了后退
}
}
优点
在搜索过程中动态产生问题的解空间。在任何时刻,算法只保存从根结点到扩展结点的路径。
例题
1.N皇后问题
问题
在 n n nx n n n的棋盘中放置n个皇后,使得任何两个皇后之间不能相互攻击,试给出所有的放置方法
思路
- 解向量:每一行有且只有一个皇后,设第i行皇后所在的列为 x i x_i xi,则问题解向量: ( x 1 , x 2 , . . . , x n ) (x_1,x_2,...,x_n) (x1,x2,...,xn)
- 显约束: x i = 1 , 2 , . . . , n x_i=1,2,...,n xi=1,2,...,n
- 隐约束:
- 不同列: x i ≠ x j x_i \neq x_j xi=xj
- 不处于同一斜线: ∣ i − j ∣ ≠ ∣ x i − x j ∣ |i-j| \neq |x_i-x_j| ∣i−j∣=∣xi−xj∣
代码
- 递归
int n = *,sum=0,x[n];
void Queen::BaCKtrack(int t){
if(t>n)sum++;
else{
for(int i = 1;i<=n;i++){
x[t] = i;
if(check(x,t))Bracktrack(t+1);
}
}
}
check(int x[],int pos){
for(int i=1;i<pos;i++){
if(abs(x[i]-x[pos) == abs(i-pos) or x[i] == x[pos])return 0;
}
return 1;
}
- 迭代
void iterativeBracktrack(int n){
int t = 1;
int[] f;//记录每一层皇后尝试到哪了
while(t>0){
if(f[t]<=n){
for(;f[t]<=n;f[t]++){
x[t] = f[t];
if(check(x,t)){
if(t ==n)output(x);
else{
t++;
f[t] = 0;//开始新的一层
}
}
}
}
else{
t--;
f[t]++;//上一层加1
}
}
}
2.货郎担问题
问题
某售货员要到若干城市去推销商品,已知各城市间的路程耗费,如何选定一条从驻地出发,经过每个城市一遍,最后回到驻地的路线,使得总路程耗费最小
思路
- 解向量:每个城市只出现有且仅有一次,设第 i i i个出现的城市为 x i x_i xi,则问题解向量 ( x 1 , x 2 , . . . , x n ) (x_1,x_2,...,x_n) (x1,x2,...,xn)
- 项约束: x i = 1 , 2 , . . . , n x_i=1,2,...,n xi=1,2,...,n
- 隐约束:
- 有从 x i x_i xi到 x i + 1 x_{i+1} xi+1的边
- 有从 x n x_n xn到 x 1 x_1 x1的边 //能回到出发城市
- x i ≠ x k x_i \neq x_k xi=xk //城市不能重复
代码
a[n][n];//邻接矩阵,
bestx[n];//存储当前最小代价对应的路线
bestc = MaxInt;//存储当前最小代价
cc = 0;//存储当前代价
x[n];//解空间
void Traveling<Type>::Backtrack(int t){
if(t>n){
bestc = cc;
bestx = x;
}
else{
for(int i =t;i<=n;i++){
if(check(x,i,t,a,n)){
swap(x[t],x[i]);
if(t<n&&cc+a[x[t-1]][x[t]]<bestc){//有更优的解才进行搜索,否则剪枝
cc = cc + a[x[t-1]][x[t]];//加入城市后更新cc
backtrack(t+1);
cc = cc- a[x[t-1]][x[t]];//恢复现场
swap(x[t],x[i]);//恢复现场
}
if(t ==n && cc+a[x[t-1]][x[t]] + a[x[n]][x[1]] < betsc){//最后一个路径点需要回到出发位置
cc = cc + a[x[t-1]][x[t]] + a[x[n]][x[1]];
bracktrack(t+1);
cc = cc - a[x[t-1]][x[t]] - a[x[n]][x[1]];//恢复现场
swap(x[t],x[i]);//恢复现场
}
}
}
}
}
3.装载问题
问题
有一批共 n n n个集装箱要装上2艘载重量分别为 c 1 c_1 c1和 c 2 c_2 c2的轮船,其中集装箱 i i i的重量为 w i w_i wi,且 ∑ i = 1 n w i ≤ c 1 + c 2 \sum_{i=1}^nw_i \leq c_1+c_2 ∑i=1nwi≤c1+c2
装载问题要求确定是否有一个合理的装在方案可将这批集装箱装上这两艘轮船。如果有,找出一种装载方案。
思路
容易证明,如果一个给定装载问题有解,则采用下面的策略可得到一种装载方案。首先将第一艘轮船尽可能装满,将剩余的集装箱装上第二艘轮船。
将第一艘轮船尽可能装满等价于选取全体集装箱的一个子集,使该子集中集装箱重量之和最接近第一艘轮船。由此可知,装载问题等价于特殊的0-1背包问题
- 解向量:每个货物要么出现,要不不出现在第一艘轮船中,设第 i i i个货物是否出现表示为 x i x_i xi,则解向量: ( x 1 , x 2 , . . . , x n ) (x_1,x_2,...,x_n) (x1,x2,...,xn)
- 显示约束: x i ∈ { 0 , 1 } x_i \in \{0,1\} xi∈{0,1}
- 隐示约束: x i x_i xi为1的货物总重量小于 c 1 c_1 c1
- 界函数: 当 前 载 重 量 c w + 剩 余 可 选 集 装 箱 的 重 量 和 r > 当 前 最 优 载 重 量 b e s t w 当前载重量cw+剩余可选集装箱的重量和r>当前最优载重量bestw 当前载重量cw+剩余可选集装箱的重量和r>当前最优载重量bestw
代码
r = sum(w);//剩余货物的重量
n;//货物的数量
bestx[];//最优解
bestw;//最优重量
cw;//当前物品的重量
w[n];//货物的重量
c;//轮船1的载重量
void backtrack(int t){//搜索第i层结点
if(i>n){//到达叶结点
bestw = cw;
bextx = x;
return;
}
//搜索子树
r -= w[t];//不管是否选择,在向下回溯的时候,这一货物都不再将作为剩余可选货物考虑,因为在下面已经考虑过了
if(cw+w[t]<=c){//搜索左子树,装入
x[t] = 1;
cw +=w[t];
backtrack(t+1);
//恢复现场
x[t] = 0;
cw -= w[t];
}
if(cw+r>bestw){//搜索右子树,不装入,同时,如果找不到更优的解,剪枝
backtrack(t+1);
}
r +=w[t];//向上回溯的时候加上
}
4.批作业调度问题
问题
给定n个作业的集合 { J 1 , J 2 , . . . , J n } \{J_1,J_2,...,J_n\} {J1,J2,...,Jn}。每个作业必须先由机器1处理,然后由机器2处理。所有作业在机器2上完成处理的时间和称为该作业调度的完成时间和。
批处理作业调度问题要求对于给定的n个作业,制定最佳作业调度方案(给出作业的加工顺序),使其完成时间和达到最小。
思路
- 每个工作出现且只出现一次,设第 i i i个出现的工作序号为 x i x_i xi,则问题解向量: ( x 1 , x 2 , . . . , x n ) (x_1,x_2,...,x_n) (x1,x2,...,xn)
- 显约束: x i ∈ { 1 , 2 , . . . , n } x_i \in \{1,2,...,n\} xi∈{1,2,...,n}
- 隐约束: x i ≠ x k x_i \neq x_k xi=xk
- 界函数:当前工作的完成时间和小于当前找到的最优调度的完成时间和
代码
F[2][n];//任务加工时间矩阵
bextx[n];//存储完成时间最短的加工顺序
bestf = MaxInt;//存储当前最短完工时间和
cf = 0;//存储当前完成时间和
M1 = 0;//机器1的完成时间
M2[n+1] = 0;//机器2的完成时间
void Scheduling<Type>::Backtrack(int t){
if(t>n){
bestf = cf;
bestx = x;
}
else{
for(int i =t;i<=n;i++){
swap(x[t],x[i]);
M1 += F[1][x[t]];
M2[t] = max(M2[t-1],M1)+F2[2][x[t]];
cf +=M2[t];
if(cf<bestf){//检查bound
backtrack(t+1);
}
//恢复现场
M1 -= F[1][x[t]];
cf -= M2[t];
swap(x[t],x[i]);
}
}
}
5.0-1背包问题
问题
思路
- 解空间:子集树
- 可行性约束函数: ∑ i = 1 n w i x i ≤ c 1 \sum_{i=1}^nw_ix_i \leq c_1 ∑i=1nwixi≤c1
- 上界函数: 当 前 价 值 c v + 剩 余 容 量 可 容 纳 的 最 大 价 值 > 当 前 最 优 价 值 b e s t p 当前价值cv+剩余容量可容纳的最大价值>当前最优价值bestp 当前价值cv+剩余容量可容纳的最大价值>当前最优价值bestp
代码
Typew c ;//背包容量
int n;//物品数量
Typew w[n];//物品重量数组
Typep p[n];//物品价值数组
Typew cw;//当前重量
Typep cp;//当前价值
Typep bestp;//当前最优值
template<class Typew,class Typep>
Typep Knap<Typew,Typep>::Backtrack(int i){
if(i>n){//到达叶结点
bestp = cp;
return;
}
if(cw+w[i]<=c){//进入左子树
cw += w[i];
cp += p[i];
Backtrack(i+1);
cw -= w[i];
cp -= p[i];
}
if(Bound(i+1)> bestp){
bcktrack(i+1);//进入右子树
}
}
template<class Typew,class Typep>
Typep Knap<Typew,Typep>::Bound(int i){//计算上界
Typew cleft = c-cw;//剩余容量
Typep b = cv;//当前容量
while(i<=n && w[i]<=cleft){//以剩余物品单位重量价值递减序装入物品
clef -= w[i];//重量减少
b += p[i];//价值增加
i++;
}
if(i<=n)b += p[i]/w[i] *cleft;//装满背包
return b;
}
6.最大团问题
问题
给定无向图 G = ( V , E ) G=(V,E) G=(V,E)。如果 U ⊆ V U \subseteq V U⊆V,且对任意 u , v ∈ U u,v \in U u,v∈U有 ( u , v ) ∈ E (u,v) \in E (u,v)∈E,则称U是G的完全子图。G的完全子图U是G的团:当且仅当U不包含在G的更大的完全子图中。G的最大团是指G中所含顶点数量最多的团。
如果 U ⊆ V U \subseteq V U⊆V且对于任意 u , v ∈ U u,v \in U u,v∈U有 ( u , v ) ∉ E (u,v) \notin E (u,v)∈/E,则称U是G的空子图。G的空子图U是G的独立集当且仅当U不包含在G的更大的空子图中。G的最大独立集是G中所含顶点数最多的独立集。
对于任一无向图 G = ( V , E ) G=(V,E) G=(V,E),其补图 G ‾ = ( V 1 , E 1 ) \overline{G}=(V_1,E_1) G=(V1,E1)定义为: V 1 = V V_1 = V V1=V,且 ( u , v ) ∈ E 1 (u,v) \in E_1 (u,v)∈E1当且仅当 ( u , v ) ∉ E (u,v) \notin E (u,v)∈/E
U是G的最大团当且仅当 U U U是 G ‾ \overline{G} G的最大独立集
思路
无向图G的最大团和最大独立集问题可以看做是图G的顶点集V的子集选取问题
- 解向量:每个顶点要么出现,要么不出现,设第 i i i个顶点是否出现表示为 x i x_i xi,则问题解向量: ( x 1 , x 2 , . . . , x n ) (x_1,x_2,...,x_n) (x1,x2,...,xn)
- 显约束: x i = 0 o r 1 x_i=0 or 1 xi=0or1
- 隐约束:所有 x i = 1 x_i=1 xi=1的顶点都互相连接
- 界函数:有足够多的可选择顶点使得算法有可能在右子树中找到更大的团
代码
cn = 0;//当前节点数
bestx[];//最大团的选择方案
void Clique::Backtrack(int t){//计算最大团
if(t>n){//到达叶结点
bestx = x;
bestn = cn;
return;
}
//检查顶点t与当前团的连接
if(check(t)){//进入左子树,只check不bound
x[t] = 1;
cn++;
backreack(t+1);
x[t] = 0;
cn--;
}
if(cn + n-t> bestn){//进入右子树,先判断界值
x[t] = 0;
backtrack(t+1);
}
}
7.图的m着色问题
问题
给定无相连通图G和m种不同的颜色。用这些颜色为图G的各顶点着色,每个顶点着一种颜色。是否有一种着色法使G中每条边的2个顶点着不同颜色。
这个问题是图的m可着色判定问题。若一个图最少需要m种颜色才能使图中每条边连接的2个顶点着不同颜色,则称这个数为该图的色数。
求一个图的色数m的问题称为图的m可着色优化问题。
思路
- 解向量: ( x 1 , x 2 , . . . , x n ) (x_1,x_2,...,x_n) (x1,x2,...,xn), x i x_i xi表示顶点 i i i所着颜色
- 可行性约束函数:顶点 i i i与已着色的相邻顶点颜色不重复
代码
int n;//图的顶点数
int m;//可用颜色数
a[n][n];//图的邻接矩阵
int x[n];//当前解
int sum;//当前已找到的可m着色方案
bool Color::Ok(int t){//检查颜色是否重复
for(int j=1;j<=n;j++){
if((a[t][j] == 1)&& (x[j]==x[t]))return false;
}
return true;
}
void Color::Backtrack(int t){//第t个点着色
if(t>n){
sum++;
for(int i=1;i<=n;i++)cout<<x[i]<<" ";
cout<<endl;
}
else{
for(int i=1;i<=m;i++){
x[t] = i;
if(Ok(t))backtrack(t+1);
x[t] = 0;
}
}
}