【引】回溯算法

一、实验名称:

实验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素数环: 把从12020个数摆成一个环,要求相邻的两个数的和是一个素数。

非常明显,这是一道回溯的题目。从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、记录上机过程中所出现的相关英文信息如菜单项名称、错误提示等,查出其中文含义,并写在实验报告后面。

你可能感兴趣的:(知识点学习)