您的位置:首页 > 编程语言 > C语言/C++

回溯算法----C语言 迷宫问题

2013-11-26 09:47 288 查看


一、实验名称:

实验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 <stdio.h>
#include <stdlib.h>
#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<stdio.h>
#include<stdlib.h>
#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<MAXM;x++) //初始化棋盘
for(y=0;y<MAXN;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<MAXM*MAXN+1)
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<MAXM;k++)
{
printf("%4d ",k+1);
for(j=0;j<MAXN;j++)
printf("%3d",position[k][j]);
printf("\n");
}
printf("--------------------------\n");
}
2、八皇后问题:要在国际象棋棋盘中放八个皇后,使任意两个皇后都不能互相吃。(提示:皇后能吃同一行、同一列、同一对角线的任意棋子。)
#include <stdlib.h>
#include <math.h>
#include <stdio.h>

//判断第n行是否可以放置皇后
bool SignPoint(int n,int *position)
{
for (int i=0;i<n;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<queen;i++)
{
for (int j=0;j<queen;j++)
{
if (j==position[i])
printf("* ");
else
printf("0 ");
}
printf("\n");
}
printf("\n");
}
else
{
for (int i=0;i<queen;i++)
{
position
=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 <stdio.h>
#include <stdlib.h>
#include <math.h>
#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、记录上机过程中所出现的相关英文信息如菜单项名称、错误提示等,查出其中文含义,并写在实验报告后面。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: