回溯法:深度优先搜索,对一个包括有很多个结点,每个结点有很多个搜索分支的问题,把原问题分解为若干个子问题求解的算法;当搜索到某个结点发现无法在继续搜索时,就让搜索过程回溯(回退)到该结点的前一个结点,继续搜索该结点外的其他尚未搜索到的分支;如果发现该结点无法在搜索下去,就让搜索过程回溯到这个结点的前一结点继续这样的搜索过程;这样的搜索过程一直持续到问题的解或者搜索完了全部可搜索分支没有解为止。
相信我们很多人都玩过迷宫探险的游戏,今天我们实现基础版本的,只有一个出口。在开始之前,首先,我们得有一张迷宫地图 :
我们用数字来表示一些特定的事物
0 —- 墙 ,表示不能走
1 —- 路,表示可以走
2 —- 走过的路
出口和入口不是一个,用边界点来表示。
思路:每走一步判断当前点能不能落脚,如果能落脚,标记当前点并把它压栈。然后判断是不是出口,如果是出口,说明找到了一条路;
如果不是,寻找下一个落脚点,按顺时针方向探测它相邻的四个点。如果四个点都不能落脚,就出栈回到上一个点,进行回溯。
准备工作:定义一个迷宫结构,初始化并且打印它
#define MAX_ROW 6
#define MAX_COL 6
typedef struct Maze
{
int map[MAX_ROW][MAX_COL];
}Maze;
void MazeInit(Maze* maze)
{
int map[MAX_ROW][MAX_COL] = {
{0,1,0,0,0,0},
{0,1,1,1,0,0},
{0,1,0,1,0,0},
{0,1,0,1,1,0},
{0,1,1,0,0,0},
{0,0,1,0,0,0},
};
size_t i = 0;
for( ;i < MAX_ROW;i++)
{
size_t j = 0;
for( ;j < MAX_COL;j++)
{
maze->map[i][j] = map[i][j];
}
}
return;
}
void MazePrint(Maze* maze)
{
size_t i = 0;
for( ;i < MAX_ROW;i++)
{
size_t j = 0;
for( ;j < MAX_COL;j++)
{
printf("%2d ",maze->map[i][j]);
}
printf("\n");
}
}
效果如下图:
下面我们用两种方法来实现:
一、递归实现
借助函数的调用栈来进行回溯
void GetPath(Maze* maze,Point entry)
{
if(maze == NULL)
{
return;//非法输入
}
//使用下面的函数辅助我们完成递归
_GetPath(maze,entry,entry);
}
//每次走到下一个点,都会递归的调用下面这个函数
void _GetPath(Maze* maze,Point cur,Point entry)
{
printf("cur:%d,%d\n",cur.ROW,cur.COL);
if(maze == NULL)
{
return;//非法输入
}
//1.判定当前点,是否能落脚
if(!CanStay(maze,cur))
{
return;//不能落脚
}
//2.如果能落脚,就给当前位置做一个标记
Mark(maze,cur);
//3.如果当前点是出口,说明找到了一跳出路,探测结束
if(IsExit(maze,cur,entry))
{
//找到了出口点
printf("找到了一条路径\n");
return;
}
//4.如果当前点不是出口,寻找下一个落脚点,按顺时针探测四个相邻的点
// 递归的调用函数自身,递归时,更新cur点(每次递归的时候,点都是下一次要走的点,这个点能不能走,交给递归函数作判断)
Point up = cur;
up.ROW -= 1;
_GetPath(maze,up,entry);
Point right = cur;
right.COL += 1;
_GetPath(maze,right,entry);
Point down = cur;
down.ROW += 1;
_GetPath(maze,down,entry);
Point left = cur;
left.COL -= 1;
_GetPath(maze,left,entry);
}
//判断pt这个点是否能落脚
//如果能落脚,返回1,不能落脚,返回0
int CanStay(Maze* maze,Point pt)
{
//1.如果pt这个点在地图外,肯定不能落脚
if(pt.ROW < 0||pt.COL < 0||pt.ROW >= MAX_ROW||pt.COL >= MAX_COL)
{
return 0;//不能落脚
}
//2.如果这个点在地图内,如果这个位置的值是1,就能落脚,如果是2或者0,就不能落脚
int value = maze->map[pt.ROW][pt.COL];
if(value == 1)
{
return 1;//能落脚,返回1
}
return 0;
}
//将当前能落脚的点赋值为2
void Mark(Maze* maze,Point cur)
{
maze->map[cur.ROW][cur.COL] = 2;
}
//判断当前点是否是出口,是,返回1,否则,返回0
int IsExit(Maze* maze,Point cur,Point entry)
{
(void)maze;
//1.当前点如果是入口,肯定不是出口
if(cur.ROW == entry.ROW && cur.COL == entry.COL)
{
return 0;
}
if(cur.ROW == 0||cur.COL == 0||cur.ROW == MAX_ROW-1||cur.COL == MAX_COL-1)
{
return 1;//当前点是边界点
}
return 0;
}
结果如下图:
我们打印出了找出路时探测结点的所有顺序,发现它如果将自身四周都遍历过还是不能发现一个落脚点,就出栈回溯。
这块大家可以对照着结点坐标和地图来看。
二、循环实现
这里思路同上面一样,只是我们要手动的去维护一个栈,通过循环来实现,栈为空说明回溯结束。
//通过循环获得路径
void GetPathByLoop(Maze* maze,Point entry)
{
//1.创建一个栈,并且初始化,这个栈保存走过的路径
SeqStack stack;
SeqStackInit(&stack);
//2.判定入口能不能落脚,如果不能,说明参数非法
if(!CanStay(maze,entry))
{
return;
}
//3.标记入口点,并且将入口点入栈
Mark(maze,entry);
SeqStackPush(&stack,entry);
//4.进入循环,获取当前的栈顶元素(栈顶元素一定能落脚)
while(1)
{
SeqStackType cur;
int ret = SeqStackTop(&stack,&cur);
if(ret == 0)
{
return;//栈为空说明回溯结束
}
//5.判定这个点是不是出口,是直接将函数返回
if(IsExit(maze,cur,entry))
{
printf("找到了一条路径\n");
return;
}
//6.不是按照顺时针方向取相邻点,判断相邻点能否落脚,
// 如果能,标记并入栈,立刻进入下一次循环
Point up = cur;
up.ROW -= 1;
if(CanStay(maze,up))
{
Mark(maze,up);
SeqStackPush(&stack,up);
continue;
}
Point right = cur;
right.COL += 1;
if(CanStay(maze,right))
{
Mark(maze,right);
SeqStackPush(&stack,right);
continue;
}
Point down = cur;
down.ROW += 1;
if(CanStay(maze,down))
{
Mark(maze,down);
SeqStackPush(&stack,down);
continue;
}
Point left = cur;
left.COL -= 1;
if(CanStay(maze,left))
{
Mark(maze,left);
SeqStackPush(&stack,left);
continue;
}
//7.如果四个相邻点都不能落脚,就出栈当前点,相当于进行回溯
SeqStackPop(&stack);
}
return;
}
结果如下: