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;i<=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;
for(i=0;icolor[i]=0;
k=0;//注意数组下标从0开始
while(k>=0)
{
color[k]=color[k]+1;//取下一种颜色
while(color[k]<=m)
if(Ok(k))break;
elsecolor[k]=color[k]+1;//搜索下一个颜色
if(color[k]<=m&&k==n-1){//求解完毕,输出解
for(i=0;icout<return;
}
if(color[k]<=m&&kk=k+1;//处理下一个顶点
else
color[k--]=0;//回溯
}
}
intOk(intk)//判断顶点k的着色是否发生冲突
{
for(inti=0;iif(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;k=1;
3.while(k>=1)
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)
{
inti,k;
intvisited[10];//假设图最多有10个顶点
for(i=0;i{
x[i]=0;visited[i]=0;
}
x[0]=0;visited[0]=1;//从顶点0出发
k=1;
while(k>=1)
{
x[k]=x[k]+1;//搜索下一顶点
while(x[k]if(visited[x[k]]==0&&arc[x[k-1]][x[k]]==1)break;
elsex[k]=x[k]+1;
if(x[k]for(k=0;kcout<return;
}
if(x[k]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摆放到它所在行的第一个位置,也就是