一、实验名称:
实验C:回溯算法
二、实验目的:
1、掌握回溯算法的概念及适应范围
2、熟悉回溯算法的设计原理
三、实验器材:
1、计算机
四、实验内容:
回溯算法也叫试探法,它是一种系统地搜索问题解的方法。
如下例:
回溯算法的基本思想是:从一条路往前走,能进则进,不能进则退回来,换一条路再试。
回溯法是一个既带有系统性又带有跳跃性的搜索算法。它在包含问题的所有解的解空间树中,按照深度优先的策略,从根结点出发搜索解的空间树。算法搜索至解的空间树的任一结点时,总是先判断该结点是否肯定不包含问题的解。如果肯定不包含,则跳过对以该结点为根的子树的系统搜索,逐层向其祖先结点回溯。否则,进入该子树,继续按深度优先的策略进行搜索。回溯法在用来求问题的所有解时,要回溯到根,且根结点的所有子树都已被搜索遍才结束。而回溯法在用来求问题的任一解时,只要搜索到问题的一个解就可以结束。这种以深度优先的方式系统地搜索问题的解的算法称为回溯法,它适用于解一些组合数较大的问题。
算法框架:
1、问题的解空间:应用回溯法解问题时,首先应明确定义问题的解空间。问题的解空间应至少包含一个(最优)解。
2、回溯法的基本思想:确定了解空间的组织结构后,回溯法就从开始结点(根结点)出发,以深度优先的方式搜索整个解空间,这个开始结点就成为一个活结点,同时也成为当前的扩展结点。在当前的扩展结点处,搜索向纵深方向移至一个新结点,这个新结点就成为一个新的活结点,并成为当前扩展结点。如果在当前的扩展结点处不能再向纵深方向移动,则当前扩展结点就成为死结点。换句话说,这个结点不再是一个活结点。此时,应往回移动(回溯)至最近的一个活结点处,并使这个活结点成为当前的扩展结点。回溯法即以这种工作方式递归地在解空间中搜索,直至找到所要求的解或解空间中已没有活结点时为止。
运用回溯法解题通常包含以下三个步骤:
(1)针对所给问题,定义问题的解空间;
(2)确定易于搜索的解空间结构;
(3)以深度优先的方式搜索解空间,并且在搜索过程中用剪枝函数避免无效搜索。
3、递归回溯:由于回溯法是对解的空间的深度优先搜索,因此在一般情况下可用递归函数来实现回溯法如下:
Try(int i)
var
{
if i>n then输出结果
else for j:=下界to上界do
{
x:=h[j];
if可行{满足限界函数和约束条件}
{
置值;
try(i+1);
}
}
}
说明:
i是递归深度;
n是深度控制,即解空间树的高度;
可行性判断有两方面的内容:
①不满约束条件则剪去相应子树;
②若限界函数越界,也剪去相应子树;
③两者均满足则进入下一层;
搜索:全面访问所有可能的情况,分为两种:不考虑给定问题的特有性质,按事先设好的顺序,依次运用规则,即盲目搜索的方法;另一种则考虑问题给定的特有性质,选用合适的规则,提高搜索的效率,即启发式的搜索。
1、骑士游历:设有一个 m ×n的棋盘 (2<=n<=50,2<=m<=50), 在棋盘上任一点有一个中国象棋马。
马走的规则为:马走日字,即如右图所示:
任务①:当m,n输入之后,找出一条走遍所有点的路径。
任务②:当m,n给出之后,找出走遍所有点的全部路径及数目。
分析:为了解决这个问题,我们将棋盘的横坐标规定为x,纵坐标规定为y,对于一个m×n的棋盘,x的值从1到m,y的值从1到n。棋盘上的每一个点,可以表示为:(x坐标值,y坐标值),即用它所在的行号和列号来表示,比如(3,5)表示第3行和第5列相交的点。
要寻找从起点到终点的路径,我们可以使用回溯算法的思想。首先将起点作为当前位置。按照象棋马的移动规则,搜索有没有可以移动的相邻位置。如果有可以移动的相邻位置,则移动到其中的一个相邻位置,并将这个相邻位置作为新的当前位置,按同样的方法继续搜索通往终点的路径。如果搜索不成功,则换另外一个相邻位置,并以它作为新的当前位置继续搜索通往终点的路径。以4×4的棋盘为例:
首先将起点(1,1)作为当前位置,按照象棋马的移动规则,可以移动到(2,3)和(3,2)。假如移动到(2,3),以(2,3)作为新的当前位置,又可以移动到(4,4)、(4,2)和(3,1)。继续移动,假如移动到(4,4),将(4,4)作为新的当前位置,这时候已经没有可以移动的相邻位置了。(4,4)已经是终点,对于任务一,我们已经找到了一条从起点到终点的路径,完成了任务,可以结束搜索过程。但对于任务二,我们还不能结束搜索过程。从当前位置(4,4)回溯到(2,3),(2,3)再次成为当前位置。从(2,3)开始,换另外一个相邻位置移动,移动到(4,2),……然后是(3,1)。(2,3)的所有相邻位置都已经搜索过。从(2,3)回溯到(1,1),(1,1)再次成为当前位置。从(1,1)开始,还可以移动到(3,2),从(3,2)继续移动,可以移动到(4,4),这时候,所有可能的路径都已经试探完毕,搜索过程结束。
如果用树形结构来组织问题的解空间(如右图),那么寻找从起点到终点的路径的过程 , 实际上就是从根结点开始,使用深度优先方法对这棵树的一次搜索过程。
还存在这样一个问题:怎样从当前位置移动到它的相邻位置?象棋马有八种移动方法,如下图:
我们分别给八种移动方法编号1、2、3、4、5、6、7、8,每种移动方法,可以用横坐标和纵坐标从起点到终点的偏移值来表示,如下表:
编号(i) |
x偏移值 |
y偏移值 |
方向 |
1 |
-2 |
1 |
左下 |
2 |
-2 |
-1 |
左上 |
3 |
-1 |
2 |
左下 |
4 |
-1 |
-2 |
左上 |
5 |
1 |
2 |
右上 |
6 |
1 |
-2 |
右上 |
7 |
2 |
1 |
右下 |
8 |
2 |
-1 |
右上 |
从当前位置搜索它的相邻位置的时候,为了便于程序的实现,我们可以按照移动编号的固定顺序来进行,比如,首先尝试第1种移动方法,其次尝试第2种移动方法,再次尝试第3种移动方法,后尝试第4种移动方法……。
具体实现过程如下:
1、棋盘的表示:用一个二维数组int position[M+1][N+1]来表示二维棋盘,为了符合人们的一般习惯,其中0行及0列没有用,数组元素的初始值都为0,表示还没有被走过,若被走过则在相应位置填入走过的顺序号;
2、马走的步骤的控制:设一个变量stepCount记录走的步骤数;
3、马位置的表示:用坐标x及y来表示马在棋盘中的横及纵坐标:
4、马走的方向的控制:
编号(i) |
x偏移值 |
y偏移值 |
方向 |
1 |
-2 |
1 |
左下 |
2 |
-2 |
-1 |
左上 |
3 |
-1 |
2 |
左下 |
4 |
-1 |
-2 |
左上 |
5 |
1 |
2 |
右上 |
6 |
1 |
-2 |
右上 |
7 |
2 |
1 |
右下 |
8 |
2 |
-1 |
右上 |
5、某一个方向的马跳步骤是否可行:没有走出棋盘且对应位置的值为0则可行,否则不可行;
6、此问题可能有多个解,设一个变量int pathCount来记录方案个数,初值为0。
则此问题的回溯算法描述大体如下:
void horse(int x,int y)
{
int xBuf=x; //暂存当前位置以备将来返回时用
int yBuf=y;
for(int i=1;i<=8;i++)
{
x=xBuf;
y=yBuf; //恢复当前位置,为下一步做准备
nextStep(i); //找下一个新的位置,注意:不一定可行
if (stepSafe()) //若新位置可行则保存相关信息
{
stepSave();
if(stepCount==M*N) //已跳遍则输出本方案并回退一步再换方向跳
{
output();
stepBack(x,y);
}
else //若若没有跳遍则以新位置为起点继续跳
horse();
}
}
stepBack(xBuf,yBuf); //回退到前一步再换方向跳以找出所有方案
}
参考程序如下:
#include
#include
#define M 5
#define N 5
int position[M+1][N+1];
int stepCount=0,pathCount=0;
static int x=1,y=1;
void init()
{
int i,j;
for(i=0;i<=M;i++)
for(j=0;j<=N;j++)
position[i][j]=0;
}
void nextStep(int n)
{
if(n==1)
{
x=x-2;y=y+1;
}
else
if(n==2)
{
x=x-2;y=y-1;
}
else
if(n==3)
{
x=x-1;y=y+2;
}
else
if(n==4)
{
x=x-1;y=y-2;
}
else
if(n==5)
{
x=x+1;y=y+2;
}
else
if(n==6)
{
x=x+1;y=y-2;
}
else
if(n==7)
{
x=x+2;y=y+1;
}
else
if(n==8)
{
x=x+2;y=y-1;
}
}
int stepSafe() //1--可行,0--不可行
{
if((x<1)||(x>M)||(y<1)||(y>N)||(position[x][y]!=0))
return 0;
else
return 1;
}
void stepSave()
{
stepCount++;
position[x][y]=stepCount;
}
void stepBack(int x,int y)
{
stepCount--;
position[x][y]=0;
}
void output()
{
int i,j;
pathCount++;
printf(\"\\n---------第%d种方案--------\\n\",pathCount);
for(i=1;i<=M;i++)
{
for(j=1;j<=N;j++)
printf(\"%4d\",position[i][j]);
printf(\"\\n\");
}
printf(\"--------------------------------------\\n\");
//system(\"pause\");
}
void horse()
{
int xBuf=x,yBuf=y,i;
for(i=1;i<=8;i++)
{
x=xBuf;
y=yBuf;
nextStep(i);
if(stepSafe())
{
stepSave();
if(stepCount==M*N)
{
output();
stepBack(x,y);
}
else
horse();
}
}
stepBack(xBuf,yBuf);
}
void main()
{
stepSave();
horse();
}
上例中的变量基本上都是全局变量。全局变量会导致各模块的独立性降低,为此,现将此程序改为局部变量,则相应程序如下:
#include
#include
#define MAXM 5
#define MAXN 5
void nextStep(int n,int *x,int *y); //求马的下一步的位置
bool safe(int position[MAXM][MAXN],int x,int y); //判断此位置是否可以跳
void stepSave(int position[MAXM][MAXN],int x,int y,int *stepCount); //保存当前的步数
void back(int position[MAXM][MAXN],int x,int y,int *stepCount); //退一步
void horse(int position[MAXM][MAXN],int x,int y,int *pathCount,int *stepCount); //求解程序
void print(int position[MAXM][MAXN],int *pathCount); //打印结果
void main(void)
{
int stepCount=1; //步数记载
int position[MAXM][MAXN]; //棋盘
int x,y; //马当前的位置
int pathCount=0; //解计数
for(x=0;x
for(y=0;y
position[x][y]=0;
x=y=0;
stepSave(position,x,y,&stepCount);
horse(position,x,y,&pathCount,&stepCount);
}
void horse(int position[MAXM][MAXN],int x,int y,int *pathCount,int *stepCount)
{
int xBuf=x; //暂存当前位置
int yBuf=y;
for(int i=1;i<=8;i++)
{
x=xBuf;
y=yBuf; //恢复当前位置,为下一步做准备
nextStep(i,&x,&y);
if(safe(position,x,y))
{
stepSave(position,x,y,stepCount);
if(*stepCount
horse(position,x,y,pathCount,stepCount);
else
{
print(position,pathCount);
back(position,x,y,stepCount);
}
}
}
back(position,xBuf,yBuf,stepCount);
}
void nextStep(int n,int *x,int *y)
{
switch(n)
{
case 1:{*x=*x-2;*y=*y+1;break;}
case 2:{*x=*x-2;*y=*y-1;break;}
case 3:{*x=*x-1;*y=*y+2;break;}
case 4:{*x=*x-1;*y=*y-2;break;}
case 5:{*x=*x+1;*y=*y+2;break;}
case 6:{*x=*x+1;*y=*y-2;break;}
case 7:{*x=*x+2;*y=*y+1;break;}
case 8:{*x=*x+2;*y=*y-1;break;}
}
}
void back(int position[MAXM][MAXN],int x,int y,int *stepCount)
{
position[x][y]=0;
*stepCount=*stepCount-1;
}
void stepSave(int position[MAXM][MAXN],int x,int y,int *stepCount)
{
position[x][y]=*stepCount;
*stepCount=*stepCount+1;
}
bool safe(int position[MAXM][MAXN],int x,int y)
{
if((x<0)||(x>MAXM-1)||(y<0)||(y>MAXN-1)||(position[x][y]!=0))
return false;
else
return true;
}
void print(int position[MAXM][MAXN],int *pathCount)
{
int j,k;
printf(\"path:%d\\n\",++*pathCount);
printf(\" * \");
for(k=1;k<=MAXM;k++)
printf(\"%3d\",k);
printf(\"\\n\\n\");
for(k=0;k
{
printf(\"%4d \",k+1);
for(j=0;j
printf(\"%3d\",position[k][j]);
printf(\"\\n\");
}
printf(\"--------------------------\\n\");
}
2、八皇后问题:要在国际象棋棋盘中放八个皇后,使任意两个皇后都不能互相吃。(提示:皇后能吃同一行、同一列、同一对角线的任意棋子。)
#include
#include
#include
//判断第n行是否可以放置皇后
bool SignPoint(int n,int *position)
{
for (int i=0;i
if((*(position+i)==*(position+n))||((abs(*(position+i)-*(position+n))==n-i)))
return false;
return true;
}
//设置皇后
void SetQueen(int n,int queen,int *position,int *count)
{
if (queen==n)
{
*count=*count+1;
printf(\"NO.%d:\\n\",*count);
for (int i=0;i
{
for (int j=0;j
{
if (j==position[i])
printf(\"* \");
else
printf(\"0 \");
}
printf(\"\\n\");
}
printf(\"\\n\");
}
else
{
for (int i=0;i
{
position[n]=i;
if(SignPoint(n,position))//如果该位置放置皇后正确的话,则到下一行
SetQueen(n+1,queen,position,count);
}
}
}
void main(int argc, char *argv[])
{
int queen,count,*position;
printf(\"请输入皇后的总数:\");
scanf(\"%d\",&queen);
count=0;
position=(int*)malloc(sizeof(int));
SetQueen(0,queen,position,&count);
printf(\"\\n结束!\\n\");
system(\"pause\");
}
3、素数环: 把从1到20这20个数摆成一个环,要求相邻的两个数的和是一个素数。
非常明显,这是一道回溯的题目。从1开始,每个空位最多有20种可能,只要填进去的数合法:
①与前面的数不相同;②与左边相邻的数的和是一个素数;③第20个数还要判断和第1个数的和是否素数。
算法流程:
①数据初始化;
②递归填数:
判断第J种可能是否合法:
A、如果合法:填数;判断是否到达目标(20个已填完):是,打印结果;不是,递归填下一个;
B、如果不合法:选择下一种可能。
参考程序:
#include
#include
#include
#define N 20
int count=0;
bool pd1(int j,int i,int a[])//判断j在前i-1个数中是否已被选
{
bool sf=true;
int k;
for(k=1;(k<=i-1)&&sf;k++)
if(a[k]==j)
sf=false;
return sf;
}
bool pd2(int x)//判断x是否为素数
{
bool sf=true;
int k;
for(k=2;sf&&(k<=(int)sqrt(x));k++)
if(x%k==0)
sf=false;
return sf;
}
bool pd3(int j,int i,int a[])//判断相邻两数之和是否为素数
{
if(i<20)
return(pd2(j+a[i-1]));
else
return(pd2(j+a[i-1])&&pd2(j+1));
}
void print(int a[])//输出结果
{
int k;
for(k=1;k<=20;k++)
printf(\"%4d\",a[k]);
printf(\"\\n\");
}
void tryit(int i,int a[])
{
int j;
for(j=2;j<=20;j++)
if(pd1(j,i,a)&&pd3(j,i,a))
{
a[i]=j;
if(i==20)
{
//print(a);
count++;
}
else
tryit(i+1,a);
a[i]=0;
}
}
void main()
{
int k,a[N+1];
for(k=1;k<=20;k++)
a[k]=0;
a[1]=1;
tryit(2,a);
printf(\"\\n共有%d种方法!\\n\",count);
system(\"pause\");
}
五、实验要求:
1、写出所有的程序,填在后面,不够加附页。
2、总结回溯算法的设计思路、技巧。
3、记录上机过程中所出现的相关英文信息如菜单项名称、错误提示等,查出其中文含义,并写在实验报告后面。