第8章 回溯法Word文档格式.docx
《第8章 回溯法Word文档格式.docx》由会员分享,可在线阅读,更多相关《第8章 回溯法Word文档格式.docx(26页珍藏版)》请在冰豆网上搜索。
![第8章 回溯法Word文档格式.docx](https://file1.bdocx.com/fileroot1/2023-2/4/682ff955-a6ab-4b2e-90a8-026bf210d4d2/682ff955-a6ab-4b2e-90a8-026bf210d4d21.gif)
回溯法的时间性能
图着色问题
哈密顿回路问题
八皇后问题
批处理作业调度问题
8.1概述
回溯法(backtrackmethod)在包含问题的所有可能解的解空间树中,从根结点出发,按照深度优先的策略进行搜索,对于解空间树的某个结点,如果该结点满足问题的约束条件,则进入该子树继续进行搜索,否则将以该结点为根结点的子树进行剪枝。
回溯法常常可以避免搜索所有的可能解,所以,适用于求解组合数较大的问题。
8.1.1问题的解空间树
复杂问题常常有很多的可能解,这些可能解构成了问题的解空间(solutionspace),并且可能解的表示方式隐含了解空间及其大小。
用回溯法求解一个具有n个输入的问题,一般情况下,将问题的可能解表示为满足某个约束条件的等长向量X=(x1,x2,…,xn),其中分量xi(1≤i≤n)的取值范围是某个有限集合Si={ai,1,ai,2,…,ai,ri},所有可能的解向量构成了问题的解空间。
例如,对于有n个物品的0/1背包问题,其可能解由一个等长向量{x1,x2,…,xn}组成,其中xi=1(1≤i≤n)表示物品i装入背包,xi=0表示物品i没有装入背包,则解空间由长度为n的0/1向量组成。
当n=3时,其解空间是:
{(0,0,0),(0,0,1),(0,1,0),(1,0,0),(0,1,1),(1,0,1),(1,1,0),(1,1,1)}
问题的解空间一般用解空间树(solutionspacetree,也称状态空间树)的方式组织,树的根结点位于第1层,表示搜索的初始状态,第2层的结点表示对解向量的第一个分量做出选择后到达的状态,第1层到第2层的边上标出对第一个分量选择的结果,依此类推,从树的根结点到叶子结点的路径就构成了解空间的一个可能解。
例如,对于n=3的0/1背包问题,其解空间树如图8.1所示,树中第i层与第i+1层(1≤i≤n)结点之间的边上给出了对物品i的选择结果,左子树表示该物品被装入了背包,右子树表示该物品没有被装入背包。
树中的8个叶子结点分别代表该问题的8个可能解,例如结点8代表一个可能解(1,0,0)。
8.1.2回溯法的设计思想
回溯法从解空间树的根结点出发,按照深度优先策略搜索满足约束条件的解。
在搜索至树中某结点时,先判断该结点对应的部分解是否满足约束条件,也就是判断该结点是否包含问题的(最优)解,如果肯定不包含,则跳过以该结点为根的子树,即所谓剪枝(pruning);
否则,进入以该结点为根的子树,继续按照深度优先策略搜索。
需要强调的是,问题的解空间树是虚拟的,并不需要在算法运行时构造一棵真正的树结构。
由于问题的解向量X=(x1,x2,…,xn)中的每个分量xi(1≤i≤n)都属于一个有限集合Si={ai,1,ai,2,…,ai,ri},因此,回溯法可以按某种顺序(例如字典序)依次考察笛卡儿积S1×
S2×
…×
Sn中的元素。
初始时,令解向量X为空,然后,从根结点出发,选择S1的第一个元素作为解向量X的第一个分量,即x1=a1,1,如果X=(x1)是问题的部分解,则继续扩展解向量X,选择S2的第一个元素作为解向量X的第2个分量;
否则,选择S1的下一个元素作为解向量X的第一个分量,即x1=a1,2。
依此类推,一般情况下,如果X=(x1,x2,…,xi)是问题的部分解,则选择Si+1的第一个元素作为解向量X的第i+1个分量时,有下面三种情况:
(1)如果X=(x1,x2,…,xi+1)是问题的最终解,则输出这个解。
如果问题只希望得到一个解,则结束搜索,否则继续搜索其他解;
(2)如果X=(x1,x2,…,xi+1)是问题的部分解,则继续构造解向量的下一个分量;
(3)如果X=(x1,x2,…,xi+1)既不是问题的部分解也不是问题的最终解,则存在下面两种情况:
①如果xi+1=ai+1,k不是集合Si+1的最后一个元素,则令xi+1=ai+1,k+1,即选择Si+1的下一个元素作为解向量X的第i+1个分量;
②如果xi+1=ai+1,k是集合Si+1的最后一个元素,就回溯到X=(x1,x2,…,xi),选择Si的下一个元素作为解向量X的第i个分量,假设xi=ai,k,如果ai,k不是集合Si的最后一个元素,则令xi=ai,k+1;
否则,就继续回溯到X=(x1,x2,…,xi-1)。
例如,对于n=3的0/1背包问题,三个物品的重量为{20,15,10},价值为{20,30,25},背包容量为25,从图8.1所示的解空间树的根结点开始搜索,搜索过程如下:
(1)从结点1选择左子树到达结点2,由于选取了物品1,故在结点2处背包剩余容量是5,获得的价值为20;
(2)从结点2选择左子树到达结点3,由于结点3需要背包容量为15,而现在背包仅有容量5,因此结点3导致不可行解,对以结点3为根的子树实行剪枝;
(3)从结点3回溯到结点2,从结点2选择右子树到达结点6,结点6不需要背包容量,获得的价值仍为20;
(4)从结点6选择左子树到达结点7,由于结点7需要背包容量为10,而现在背包仅有容量5,因此结点7导致不可行解,对以结点7为根的子树实行剪枝;
(5)从结点7回溯到结点6,在结点6选择右子树到达叶子结点8,而结点8不需要容量,构成问题的一个可行解(1,0,0),背包获得价值20;
按此方式继续搜索,得到的搜索空间如图8.2所示。
8.1.3回溯法的时间性能
一般情况下,在问题的解向量X=(x1,x2,…,xn)中,分量xi(1≤i≤n)的取值范围为某个有限集合Si={ai,1,ai,2,…,ai,ri},因此,问题的解空间由笛卡儿积A=S1×
Sn构成,并且第1层的根结点有|S1|棵子树,则第2层共有|S1|个结点,第2层的每个结点有|S2|棵子树,则第3层共有|S1|×
|S2|个结点,依此类推,第n+1层共有|S1|×
|S2|×
|Sn|个结点,他们都是叶子结点,代表问题的所有可能解。
回溯法实际上属于蛮力穷举法,当然不能指望它有很好的最坏时间复杂性,遍历具有指数阶个结点的解空间树,在最坏情况下,时间代价肯定为指数阶。
然而,从本章介绍的几个算法来看,他们都有很好的平均时间性能。
回溯法的有效性往往体现在当问题规模n很大时,在搜索过程中对问题的解空间树实行大量剪枝。
但是,对于具体的问题实例,很难预测回溯法的搜索行为,特别是很难估计出在搜索过程中所产生的结点数,这是分析回溯法的时间性能的主要困难。
8.1.4一个简单的例子——素数环问题
【问题】把整数{1,2,…,20}填写到一个环中,要求每个整数只填写一次,并且相邻的两个整数之和是一个素数。
例如,图8.3所示就是整数{1,2,3,4}对应的一个素数环。
【想法】这个素数环有20个位置,每个位置可以填写的整数有1~20共20种可能,可以对每个位置从1开始进行试探,约束条件是正在试探的数满足如下条件:
(1)与已经填写到素数环中的整数不重复;
(2)与前面相邻的整数之和是一个素数;
(3)最后一个填写到素数环中的整数与第一个填写的整数之和是一个素数。
在填写第k个位置时,如果满足上述约束条件,则继续填写第k+1个位置;
如果1~20个数都无法填写到第k个位置,则取消对第k个位置的填写,回溯到第k-1个位置。
【算法实现】设数组a[n]表示素数环,为了和C++语言的数组下标一致,素数环的位置为0~n-1,算法用C++语言描述如下:
voidPrimeCircle(intn)//填写1~n共n个整数
{
inti,k;
for(i=0;
i<
n;
i++)//将数组a[n]初始化为0
a[i]=0;
a[0]=1;
k=1;
//指定第0个位置填写1
while(k>
=1)
{
a[k]=a[k]+1;
while(a[k]<
=n)
if(Check(k)==1)break;
//位置k可以填写整数a[k]
elsea[k]=a[k]+1;
//试探下一个数
if(a[k]<
=n&
&
k==n-1){//求解完毕,输出解
for(i=0;
i++)
cout<
<
a[i]<
"
"
;
return;
}
k<
n-1)
k=k+1;
//填写下一个位置
else
a[k--]=0;
//回溯
}
}
intCheck(intk)//判断位置k的填写是否满足约束条件
{
intflag=0;
for(inti=0;
k;
i++)//判断是否重复
if(a[i]==a[k])return0;
flag=Prime(a[k]+a[k-1]);
//判断相邻数之和是否素数
if(flag==1&
k==n-1)//判断第一个和最后一个是否素数
flag=Prime(a[k]+a[0]);
returnflag;
intPrime(intx)//判断整数x是否素数
inti,n;
n=(int)sqrt(x);
for(i=2;
=n;
i++)
if(x%i==0)return0;
return1;
【算法分析】设要填写1~n共n个整数,由于每个位置可以填写的情况有n种,因此,素数环问题的解空间树是一棵完全n叉树,且树的深度为n+1,因此,最坏情况下的时间性能为O(nn)。
8.2图问题中的回溯法
8.2.1图着色问题
【问题】给定无向连通图G=(V,E),图着色问题(graphcoloringproblem)求最小的整数m,用m种颜色对G中的顶点着色,使得任意两个相邻顶点着色不同。
应用实例
机场停机位分配是指根据航班和机型等属性,为每个航班指定一个具体的停机位。
具体分配停机位时,必须满足下列约束条件:
(1)每个航班必须被分配且仅能被分配1个停机位;
(2)同一时刻同一个停机位不能分配1个以上的航班;
(3)应满足航站衔接以及过站时间衔接要求;
(4)机位与所使用机位的航班应该相互匹配。
对飞机进行停机位分配时,假设班机时刻表为已知,并且按照“先到先服务”的原则对航班进行机位分配,这样就可以将停机位分配转化为图着色问题。
【想法】用m种颜色为无向图G=(V,E)着色,其中,V的顶点个数为n,可以用一个n元组C=(c1,c2,…,cn)来描述图的一种可能着色,其中,ci∈{1,2,…,m}(1≤i≤n)表示赋予顶点i的颜色。
例如,5元组(1,2,2,3,1)表示对具有5个顶点的无向图的一种着色,顶点1着颜色1,顶点2着颜色2,顶点3着颜色2,等等。
如果在n元组C中,所有相邻顶点都不会着相同颜色,则称此n元组为可行解,否则为无效解。
回溯法求解图着色问题,首先把所有顶点的颜色初始化为0,然后依次为每个顶点着色。
在图着色问题的解空间树中,如果从根结点到当前结点对应一个部分解,也就是所有的颜色指派都没有冲突,则在当前结点处选择第一棵子树继续搜索,也就是为下一个顶点着颜色1,否则,对当前子树的兄弟子树继续搜索,也就是为当前顶点着下一个颜色,如果所有m种颜色都已尝试过并且都发生冲突,则回溯到当前结点的父结点处,上一个顶点的颜色被改变,依此类推。
例如,在图8.4(a)所示的无向图中求解3着色问题,在解空间树中的搜索过程如图8.4(b)所示,具体过程如下:
在解空间树中,从根结点出发,搜索第1棵子树,即为顶点A着颜色1;
再搜索结点2的第1棵子树,即为顶点B着颜色1,这导致一个不可行解,回溯到结点2,选择结点2的第2棵子树,即为顶点B着颜色2;
在为顶点C着色时,经过着颜色1和颜色2的失败的尝试后,选择结点4的第3棵子树,即为顶点C着颜色3;
在结点7选择第1棵子树,即为顶点D着颜色1,但是在为顶点E着色时,顶点E无论着3种颜色的哪一种均发生冲突,于是导致回溯,在结点7选择第2棵子树也会发生冲突,于是,选择结点7的第3棵子树,即顶点D着颜色3;
在结点10选择第1棵子树,即为顶点E着颜色1,得到了一个可行解(1,2,3,3,1)。
【算法】在回溯法的搜索过程中只需要保存已处理顶点的着色情况,设数组color[n]表示顶点的着色情况,回溯法求解m着色问题的算法用伪代码描述如下:
算法8.1:
回溯法求解图着色问题
输入:
图G=(V,E),m种颜色
输出:
n个顶点的涂色情况color[n]
1.将数组color[n]初始化为0;
2.k=1;
3.while(k>
=1)
3.1依次考察每一种颜色,若顶点k的着色与其他顶点的着色不发生冲突,则转步骤3.2;
否则,搜索下一个颜色;
3.2若顶点已全部着色,则输出数组color[n],返回;
3.3若顶点k是一个合法着色,则k=k+1,转步骤3处理下一个顶点;
3.4否则,重置顶点k的着色情况,k=k-1,转步骤3回溯;
【算法分析】用m种颜色为一个具有n个顶点的无向图着色,共有mn种可能的着色组合,因此,解空间树是一棵完全m叉树,树中每一个结点都有m棵子树,最后一层有mn个叶子结点,每个叶子结点代表一种可能着色,最坏情况下的时间性能是O(mn)。
对于图8.4所示无向图,解空间树中共有243个结点,而回溯法只搜索了其中的14个结点后就找到了问题的解。
【算法实现】假设n个顶点的无向图采用邻接矩阵存储,数组arc[n][n]存储顶点之间边的情况,color[n]存储n个顶点的着色情况,为避免在函数间传递参数,将数组arc和color设为全局变量,回溯法求解m着色问题的算法用C++语言描述如下:
voidGraphColor(intm)
inti,k;
i++)//将数组color[n]初始化为0
color[i]=0;
k=0;
//注意数组下标从0开始
=0)
color[k]=color[k]+1;
//取下一种颜色
while(color[k]<
=m)
if(Ok(k))break;
elsecolor[k]=color[k]+1;
//搜索下一个颜色
if(color[k]<
=m&
for(i=0;
cout<
color[i]<
return;
k=k+1;
//处理下一个顶点
else
color[k--]=0;
//回溯
intOk(intk)//判断顶点k的着色是否发生冲突
for(inti=0;
if(arc[k][i]==1&
color[i]==color[k])return0;
return1;
8.2.2哈密顿回路问题
【问题】爱尔兰数学家哈密顿(WilliamHamilton,1805—1865)提出了著名的周游世界问题。
设正十二面体的20个顶点代表20个城市,哈密顿回路问题(Hamiltoncycleproblem)要求从一个城市出发,经过每个城市恰好一次,然后回到出发城市。
【想法】假定图G=(V,E)的顶点集为V={1,2,…,n},则哈密顿回路的可能解表示为n元组X=(x1,x2,…,xn),其中,xi∈{1,2,…,n}。
根据题意,有如下约束条件:
回溯法求解哈密顿回路问题,首先把所有顶点的访问标志初始化为0,然后在解空间树中从根结点开始搜索,如果从根结点到当前结点对应一个部分解,即满足上述约束条件,则在当前结点处选择第一棵子树继续搜索,否则,对当前子树的兄弟结点进行搜索,如果当前结点的所有子树都已尝试过并且发生冲突,则回溯到当前结点的父结点。
例如,对于图8.5(a)所示无向图,求解哈密顿回路在解空间树中的搜索过程如图8.5(b)所示,具体过程如下:
在解空间树中从根结点1开始搜索,首先将x1置为1,到达结点2,表示哈密顿回路从顶点1开始;
然后将x2置为1,到达结点3,但顶点1重复访问,搜索结点2的兄弟子树,将x2置为2,构成哈密顿回路的部分解(1,2);
在经过结点5和结点6的失败的尝试后,将x3置为3,将哈密顿回路的部分解扩展到(1,2,3);
在经过结点8、结点9和结点10的失败的尝试后,将x4置为4,将哈密顿回路的部分解扩展到(1,2,3,4);
在经过结点12、结点13、结点14和结点15的失败的尝试后,将x5置为5,将哈密顿回路的部分解扩展到(1,2,3,4,5),但是在图(a)中从顶点5到顶点1没有边,引起回溯;
将x4置为5,到达结点17,构成哈密顿回路的部分解(1,2,3,5);
在经过结点18、结点19和结点20的失败的尝试后,将x5置为4,将哈密顿回路的部分解扩展到(1,2,3,5,4),而在图(a)中从顶点4到顶点1存在边,所以,找到了一条哈密顿回路(1,2,3,5,4),搜索过程结束。
【算法】用回溯法求解哈密顿回路,首先把n元组(x1,x2,…,xn)的每一个分量初始化为0,然后深度优先搜索解空间树,如果满足约束条件,则继续进行搜索,否则,将引起搜索过程的回溯。
设数组x[n]存储哈密顿回路上的顶点,数组visited[n]存储顶点的访问标志,visited[i]=1表示哈密顿回路经过顶点i,算法用伪代码描述如下:
算法8.2:
回溯法求解哈密顿回路问题
无向图G=(V,E)
哈密顿回路
1.将顶点数组x[n]初始化为0,标志数组visited[n]初始化为0;
2.从顶点0出发构造哈密顿回路:
visited[0]=1;
x[0]=1;
3.while(k>
3.1x[k]=x[k]+1,搜索下一个顶点;
3.2若(n个顶点没有被穷举)执行下列操作
3.2.1若(顶点x[k]不在哈密顿回路上&
(x[k-1],x[k])∈E),转步骤3.3;
3.2.2否则,x[k]=x[k]+1,搜索下一个顶点;
3.3若数组x[n]已形成哈密顿路径,则输出数组x[n],算法结束;
3.4若数组x[n]构成哈密顿路径的部分解,则k=k+1,转步骤3;
3.5否则,重置x[k],k=k-1,取消顶点x[k]的访问标志,转步骤3;
【算法分析】在哈密顿回路的可能解中,考虑到约束条件xi≠xj(1≤i,j≤n,i≠j),则可能解应该是(1,2,…,n)的一个排列,对应的解空间树中有n!
个叶子结点,每个叶子结点代表一种可能解。
对于图8.5所示无向图,解空间树中共有243个结点,而回溯法只搜索了其中的21个结点后就找到了问题的解。
【算法实现】假设图采用邻接矩阵存储,数组arc[n][n]存储顶点之间边的情况,为避免在函数间传递参数,将数组arc设为全局变量,设数组x[n]表示哈密顿回路经过的顶点,算法用C++语言描述如下:
voidHamiton(intx[],intn)
intvisited[10];
//假设图最多有10个顶点
i++)//初始化回路的顶点数组和标志数组
x[i]=0;
visited[i]=0;
x[0]=0;
visited[0]=1;
//从顶点0出发
k=1;
x[k]=x[k]+1;
//搜索下一顶点
while(x[k]<
n)
if(visited[x[k]]==0&
arc[x[k-1]][x[k]]==1)break;
elsex[k]=x[k]+1;
if(x[k]<
n&
k==n-1&
arc[x[k]][0]==1){
for(k=0;
k++)
x[k]+1<
//输出顶点的编号,编号从1开始
n-1){
visited[x[k]]=1;
k=k+1;
else{//回溯
visited[x[k]]=0;
x[k]=0;
k=k-1;
8.3组合问题中的回溯法
8.3.1八皇后问题
【问题】八皇后问题(eightquessproblem)是十九世纪著名的数学家高斯于1850年提出的。
问题是:
在8×
8的棋盘上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上。
可以把八皇后问题扩展到n皇后问题,即在n×
n的棋盘上摆放n个皇后,使任意两个皇后都不能处于同一行、同一列或同一斜线上。
【想法】显然,棋盘的每一行可以并且必须摆放一个皇后,所以,n皇后问题的可能解用一个n元向量(x1,x2,…,xn)表示,即第i个皇后摆放在第i行第xi列的位置(1≤i≤n且1≤xi≤n)。
由于两个皇后不能位于同一列,所以,n皇后问题的解向量必须满足约束条件xi≠xj。
可以将n皇后问题的n×
n棋盘看成是矩阵,设皇后i和皇后j的摆放位置分别是(i,xi)和(j,xj),则在棋盘上斜率为-1的同一条斜线上,满足条件i-xi=j-xj,如图8.6(a)所示;
在棋盘上斜率为1的同一条斜线上,满足条件i+xi=j+xj,如图8.6(b)所示。
综合上述两种情况,n皇后问题的解必须满足约束条件:
|i-j|≠|xi-xj|。
为了简化问题,下面讨论四皇后问题。
回溯法从空棋盘开始,首先把皇后1摆放到它所在行的第一个位置,也就是