[XJTUSE 算法设计与分析] 第五章 回溯法
Posted 雨落俊泉
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[XJTUSE 算法设计与分析] 第五章 回溯法相关的知识,希望对你有一定的参考价值。
第五章 回溯法
填空题会有代码填空,大题会手动回溯
学习要点
理解回溯法的深度优先搜索策略。
掌握用回溯法解题的算法框架
(1)递归回溯
(2)迭代回溯
(3)子集树算法框架
(4)排列树算法框架
5.1 回溯法的算法框架
回溯法的基本做法是搜索,或是一种组织得井井有条的,能避免不必要搜索的穷举式搜索法。这种方法适用于解一些组合数相当大的问题。
回溯法在问题的解空间树中,按深度优先策略,从根结点出发搜索解空间树。算法搜索至解空间树的任意一点时,先判断该结点是否包含问题的解。如果肯定不包含,则跳过对该结点为根的子树的搜索,逐层向其祖先结点回溯;否则,进入该子树,继续按深度优先策略搜索。
1、问题的解空间
用回溯法解问题时,应明确定义问题的解空间。
解空间往往用向量集表示。
问题的解向量:回溯法希望一个问题的解能够表示成一个n元式(x1,x2,…,xn)的形式。
显约束:对分量xi的取值限定。
隐约束:为满足问题的解而对不同分量之间施加的约束。
解空间:对于问题的一个实例,解向量满足显式约束条件的所有多元组,构成了该实例的一个解空间。
2、回溯的基本思想
扩展结点:一个正在产生儿子的结点称为扩展结点。
活结点:一个自身已生成但其儿子还没有全部生成的节点称做活结点。
死结点:一个所有儿子已经产生的结点称做死结点。
深度优先的问题状态生成法:如果对一个扩展结点R,一旦产生了它的一个儿子C,就把C当做新的扩展结点。在完成对子树C(以C为根的子树)的穷尽搜索之后,将R重新变成扩展结点,继续生成R的下一个儿子(如果存在)。
宽度优先的问题状态生成法:在一个扩展结点变成死结点之前,它一直是扩展结点。
回溯法从开始结点(根结点)出发,以深度优先方式搜索整个解空间。
基本思想
1️⃣ 针对所给问题,定义问题的解空间;
2️⃣ 确定易于搜索的解空间结构;
3️⃣ 以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索
常用剪枝函数:
用约束函数在扩展结点处剪去不满足约束的子树;
用限界函数剪去得不到最优解的子树。
3、递归回溯——背诵
void Backtrack (int t) //t为递归深度
if (t>n)
Output(x); //记录或输出可靠解x,x为数组
else
for (int i=f(n,t); i<=g(n,t); i++)
//f(n,t)表示在当前扩展结点处未搜索过的子树的起始编号
//g(n,t)为终止编号
x[t]=h(i); //h(i)表示当前扩展结点处x[t]的第i个可选值
if (Constraint(t)&&Bound(t)) //剪枝
Backtrack(t+1);
回溯法对解空间作深度优先搜索,因此,在一般情况下用递归方法实现回溯法。
4、迭代回溯——会填空即可
void IterativeBacktrack ()
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 (Constraint(t)&&Bound(t))
if (solution(t)) //判断是否已得到可行解
Output(x);
else
t++;
else
t--;
f(n,t)表示在当前扩展结点处未搜索过的子树的起始编号
g(n,t)为终止编号
h(i)表示当前扩展结点处x[t]的第i个可选值
5、子集树和排列树(重点)
子集树:当所给的问题是从n个元素的集合S中找出满足某种性质的子集时,相应的解空间树称为子集树。时间复杂度 Ω ( 2 n ) Ω(2^n) Ω(2n)。算法描述如下:
void Backtrack (int t)
if (t>n)
Output(x);
else
for (int i=0; i<=1; i++)
x[t]=i;
if (Constraint(t)&&Bound(t))
Backtrack(t+1);
排列树当所给的问题是确定n个元素满足某种性质的排列时,相应的解空间树称为排列树。时间复杂度 Ω ( n ! ) Ω(n!) Ω(n!)。算法描述如下:
void Backtrack (int t)
if (t>n)
Output(x);
else
for (int i=t; i<=n; i++)
Swap(x[t], x[i]);
if (Constraint(t)&&Bound(t))
Backtrack(t+1);
Swap(x[t], x[i]);
5.2 装载问题
1、问题描述
有一批共n个集装箱要装上2艘载重量分别为c1和c2的轮船,其中集装箱i的重量为wi,且 ∑ i = 1 n w i ≤ c 1 + c 2 \\sum _i=1^n w_i\\le c_1+c_2 ∑i=1nwi≤c1+c2,装载问题要求确定是否有一个合理的装载方案可将这些集装箱装上这2艘轮船。如果有,找出一种装载方案。
例如:
n=3, c1=c2=50,且w=[10,40,40]。
装载方案:
第一艘轮船装集装箱1和2;二艘轮船装集装箱3。
如果一个给定装载问题有解,则采用下面的策略可得到最优装载方案。(1)首先将第一艘轮船尽可能装满;(2)将剩余的集装箱装上第二艘轮船。将第一艘轮船尽可能装满等价于选取全体集装箱的一个子集,使该子集中集装箱重量之和最接近c1。
2、算法分析
解空间:子集树
可行性约束函数(选择当前元素): ∑ i = 1 n w i x i ≤ c 1 x i ∈ 0 , 1 \\sum _i=1^n w_i x_i\\le c_1\\quad x_i\\in\\0,1\\ ∑i=1nwixi≤c1xi∈0,1
template<typename Type>
class Loading
template<typename T>
friend T MaxLoading(T [],T,int);
private:
void Backtrack(int i);
int n; //集装箱数
Type *w; //集装箱重量数组
Type c; //第1艘轮船的载重量
Type cw; //当前载重量
Type bestw; //当前最优载重量
;
template<typename Type>
void Loading<Type>::Backtrack(int i) //搜索第i层结点
if(i>n) //到达叶结点
if(cw>bestw)
bestw=cw;
return;
if(cw+w[i]<=c) //进入左子树,x[i]=1
cw+=w[i];
Backtrack(i+1); //继续搜索下一层
cw-=w[i]; //退出左子树
Backtrack(i+1); //进入右子树,x[i]=0
template<typename Type>
Type MaxLoading(Type w[],Type c,int n) //返回最优载重量
Loading<Type> X;
X.w=w; //初始化X
X.c=c;
X.n=n;
X.bestw=0;
X.cw=0;
X.Backtrack(1); //从第1层开始搜索
return X.bestw;
int main()
const int n=6;
int c=80;
int w[]=0,20,40,40,10,30,20; //下标从1开始
int s=MaxLoading(w,c,n);
cout<<s<<endl;
return 0;
算法在每个结点处花费O(1)时间,子集树中结点个数为O( 2 n 2^n 2n),故算法的计算时间为O( 2 n 2^n 2n)。
3、上界函数
对于上一算法可引入一个上界函数,用于剪去不含最优解的子树。
上界函数(不选择当前元素):当前载重量cw+剩余集装箱的重量r≤当前最优载重量bestw。
template<typename Type>
class Loading
template<typename T>
friend T MaxLoading(T [],T,int);
private:
void Backtrack(int i);
int n; //集装箱数
Type *w; //集装箱重量数组
Type c; //第1艘轮船的载重量
Type cw; //当前载重量
Type bestw; //当前最优载重量
Type r; //剩余集装箱重量
;
template<typename Type>
void Loading<Type>::Backtrack(int i) //搜索第i层结点
if(i>n) //到达叶结点
if(cw>bestw)
bestw=cw;
return;
r-=w[i]; //剩余集装箱重量
if(cw+w[i]<=c) //进入左子树,x[i]=1
cw+=w[i];
Backtrack(i+1); //继续搜索下一层
cw-=w[i]; //退出左子树
if(cw+r>bestw) //上界函数
//进入右子树,x[i]=0
Backtrack(i+1);
r+=w[i];
4、构造最优解
为构造最优解,需在算法中记录与当前最优值相应的当前最优解。
在类Loading中增加两个私有数据成员:
int* x:用于记录从根至当前结点的路径;
int* bestx:记录当前最优解。
算法搜索到叶结点处,就修正bestx的值。
public class Loading
// 船最大装载问题
private int n;// 集装箱数
private int[] x;// 当前解
private int[] bestx;// 当前最优解
private int[] w;// 集装箱重量数组
private int c;// 第一艘船的载重量
private int cw;// 当前载重量
private int bestw;// 当前最优载重量
private int r;// 剩余集装箱重量
void backTrack(int i)// 搜索第i层结点
if (i > n)
// 到达叶结点
if (cw > bestw)
for (int j = 1; j <= n; j++)
bestx[j] = x[j];
bestw = cw;
return;
r -= w[i];
// 剩余集装箱重量
if (cw + w[i] <= c)
// 进入左子树
x[i] = 1;
// 装第i个集装箱
cw += w[i];
backTrack(i + 1);
// 进入下一层
cw -= w[i];
// 退出左子树
if (cw + r > bestw)
// 进入右子树
x[i] = 0;
// 不装第i个集装箱
backTrack(i + 1);
r += w[i];
public Loading(int[] w, int c, int n, int[] bestx)
this.w = w;
this.c = c;
this.n = n;
this.bestx = bestx;
this.bestw = 0;
this.cw = 0;
for (int i = 1; i <= n; i++)
this.r += w[i];
this.x = new int[n + 1];
// 构造器
public static void main(String[] args)
int n = 5;
int c = 10;
int w[] = 0, 7, 2, 6, 5, 4;// 下标从1开始
int bestx[] = new int[n + 1];
Loading test = new Loading(w, c, n, bestx);
test.backTrack(1);
for (int i = 1; i <= n; i++)
System.out.print(bestx[i] + " ");
System.out.println();
System.out.println(test.bestw);
return;
由于bestx可能被更新O(2n)次,故算法的时间复杂性为O(n2^n)。
5、迭代回溯(填空即可)
由于数组x记录了解空间树中从根到当前扩展结点的路径,利用这些信息,可将上述回溯法表示成非递归的形式。
n=3, c1=c2=50,且w=[10,40,40]
//迭代回溯法,返回最优载重量
template<typename Type>
Type MaxLoading(Type w[],Type c,int n,int bestx[])
//初始化根结点
int i=1;
int *x=new int[n+1];
Type bestw=0;
Type cw=0;
Type r=0;
for(int j=1;j<=n;j++)
r+=w[j];
while(true) //搜索子树
while(i<=n&&cw+w[i]<=c) //进入左子树,条件为真,则一直往左搜索
r-=w[i];
cw+=w[i];
x[i]=1;
i++;
if(i>n) //到达叶结点
for(int j=1;j<=n;j++)
bestx[j]=x[j];
bestw=cw;
else //进入右子树
r-=w[i];
x[i]=0;
i++;
while(cw+r<=bestw) //剪枝回溯
i--;
while(i>0&&!x[i]) //从右子树返回
r+=w[i];
i--;
算法第五章小结