说到迷宫,首先一定要有一个迷宫的地图,让后还要规定寻找迷宫出口的一些规则
关于迷宫的地图,在这里我创建了一个二维数组来表示迷宫地图,并且约定
(1)位置0为墙(即不能走的地方),位置1为路
(2)走到边界点的位置,就为出路
(3)按照上、右、下、左(顺时针)的顺序来探测下一步出路,若下一步路,就往下走
(4)若当前点可以走,就将当前点标记为2,为了区分位置1,防止不知道自己走了重复的路
首先先来创建迷宫地图并打印迷宫地图
在这里通过使用二维数组的方法来表示迷宫的地图,设计出的地图如图所示
4 #define Max_ROW 6 5 #define Max_COL 6 6 typedef struct Maze 7 { 8 int map[Max_ROW][Max_COL]; 9 }Maze; 10 11 //初始化 12 void MazeInit(Maze* maze) 13 { 14 int map[Max_ROW][Max_COL]={{0,1,0,0,0,0}, 15 {0,1,1,1,0,0}, 16 {0,1,0,1,0,0}, 17 {0,1,0,1,0,0}, 18 {0,1,1,0,0,0}, 19 {0,0,1,0,0,0}}; 20 size_t i=0; 21 for(;i<Max_ROW;++i) 22 { 23 size_t j=0; 24 for(;j<Max_COL;++j) 25 { 26 maze->map[i][j]=map[i][j]; 27 } 28 } 32 //打印迷宫 33 void MazePrint(Maze* maze) 34 { 35 size_t i=0; 36 for(;i<Max_ROW;++i) 37 { 38 size_t j=0; 39 for(;j<Max_COL;++j) 40 { 41 printf("%2d ",maze->map[i][j]); 42 } 43 printf("\n"); 44 } 45 } 46
方法一:使用递归的方法来解决迷宫问题,为迷宫找出路
使用递归的方法来解决迷宫问题本质上还是使用栈,只不过这个栈是由操作系统提供的,即函数调用栈
创建函数GetPath,传入地图,和入口点,然后通过_GetPath函数来辅助完成递归的操作
134 void GetPath(Maze* maze,Point entry) 135 { 136 if(maze == NULL) 137 { 138 //非法操作 139 return; 140 } 141 //用_GetPath来辅助完成递归 142 _GetPath(maze,entry,entry);//第一个entry为当前走到的点,第二个entry为真正的入口点 143 } 144
在使用递归的方法来实现迷宫问题时,首先要清楚,当每个坐标入栈的时候,都会调用一次函数,当出栈的时候,函数调用结束,而每当走到下一个点的时候又会递归地调用下一个函数
下面是实现
_GetPath函数,需要传入地图和当前到的点和迷宫入口点
1.首先要判断当前点是否能落脚,即当前点是否为1(在刚开始的时候,就已经约定0为墙,1为路)
需要引用一个函数CanStay来判断是否能落脚
2.若能落脚,就将当前的位置标记为2,表示当前点已走
需要引用一个函数Mark来将走过的路来标记成2
3.若当前点为出口(即地图边缘的点),则说明寻找路径结束,则此时打印一条语句“找到出路”结束探测
需要引用一个函数IsExit来判断当前点是否为出口点
4.若不是出口位置,则需要按照上、右、下、走(顺时针)的顺序来探测当前点的四个方向的点是否能够落脚,此时就是要递归地调用函数
_GetPath函数自身,每次递归的时候递归的点都是下次要走的点,而此点是否能走,就要交给函数来判断
注:递归的调用函数就是将下一个点入栈的操作,
当每个坐标入栈的时候,都会调用一次函数,当出栈的时候,函数调用结束,而每当走到下一个点的时候又会递归地调用下一个函数
详细过程见程序
95 void _GetPath(Maze* maze,Point cur,Point entry) 96 { 97 98 printf("cur:%d,%d\n",cur.row,cur.col); 99 //1.判定当前点是否能落脚 100 if(!CanStay(maze,cur)) 101 { 102 //不能落脚 103 return; 104 } 105 //2.若能落脚,就给当前位置坐标2,2表示已经走过的路 106 Mark(maze,cur); 107 //3.若当前点是出口,则说明找到了一条出路,探测结束 108 if(IsExit(maze,cur,entry)) 109 { 110 //找到了出路 111 printf("找到了一条路径\n"); 112 return; 113 } 114 //4.若当前点不是出口,则按顺时针探测四个相邻的点,递归地调用函数自身,递归时更新cur 115 //(每次递归的时候,点都是下一次要走的点,这个点究竟能不能走,交给递归函数来判断) 116 Point up=cur; 117 up.row-=1;//上方的点 118 _GetPath(maze,up,entry); 119 120 Point right=cur; 121 right.col+=1;//右 122 _GetPath(maze,right,entry); 123 124 Point down=cur; 125 down.row+=1;//下 126 _GetPath(maze,down,entry); 127 128 Point left=cur; 129 left.col-=1;//左 130 _GetPath(maze,left,entry); 131 132 return; 133 } 134
函数需要引入地图和要判断的点pt,有两种情况,一种为在迷宫外部,一种为在迷宫内部
(1)若pt点在迷宫地图的外部,则一定不能落脚
(2)若pt点在迷宫内部,若pt点的值为1,则说明能落脚,否则(pt的值为0或者为2)就不能落脚
54 //判断点是否能落脚 55 int CanStay(Maze* maze,Point pt) 56 { 57 //1.若pt点在迷宫圈外,则不能落脚 58 if(pt.row < 0 || pt.row >= Max_ROW || pt.col < 0 || pt.col >= Max_COL) 59 { 60 return 0; 61 } 62 //2.若pt地图内部,若位置为1,则能落脚,若位置为0或者为2,则不能落脚 63 int value=maze->map[pt.row][pt.col]; 64 if(value == 1) 65 { 66 return 1; 67 } 68 return 0; 69 } 70
下面是实现
Mark函数,来将走过的点标记为2
71 //将走过的点做标记2 72 void Mark(Maze* maze,Point cur) 73 { 74 maze->map[cur.row][cur.col]=2; 75 }
下面是实现
IsExit函数,来判断当前点是否为出口点
在刚开始的时候,约定在地图边缘的点都可以为出口点,但是迷宫的入口点同样也是在边缘上,所以,要先来判断当前点是否为入口点,若为入口点,则一定不是出口,然后再判断当前点是否为地图的边缘点
77 //判断是否为出口 78 int IsExit(Maze* maze,Point cur,Point entry) 79 { 80 (void)maze; 81 //1.看当前点是否为入口,若为入口,则一定不是出口 82 if(cur.row == entry.row && cur.col == entry.col) 83 { 84 //为入口点 85 return 0; 86 } 87 //2.若点在地图边上,则说明是出口 88 if(cur.row == 0 || cur.row == Max_ROW-1 || cur.col == 0 || cur.col == Max_COL-1) 89 { 90 return 1; 91 } 92 return 0; 93 }
方法二:使用非递归的方式来解决迷宫问题
使用非递归的方式来解决迷宫问题就是使用循环来解决
创建GetPathByLoop函数,传入地图和入口点,由于这个方法需要引用之前栈的一些操作
1.首先要创建,并初始化一个栈,这个栈保存着我们走过的路径
2.要判断当前点是否能落脚,调用方法一中写过的CanStay函数
3.若能落脚,就将入口点标记为2,并将入口点入栈
4.然后进入循环,获取当前的栈顶元素,因为栈顶元素一定能落脚
5.判断这个栈顶元素是否为迷宫的出口,若是出口,则直接返回“找到了一条路径”
6.若不是出口则需要按照上、右、下、走(顺时针)的顺序来探测当前点的四个方向的点是否能够落脚,若能落脚,就将点标记为2,然后入栈,并进入到下一轮循环
7.若四个相邻的点都不能落脚,就出栈(相当于是回溯)
153 void GetPathByLoop(Maze* maze,Point entry) 154 { 155 156 //1.创建一个栈,并初始化,这个栈保存着我们走过的路径 157 SeqStact stack; 158 SeqStactInit(&stack); 159 //2.判定入口是否合法,若不能则表示参数非法 160 if(!CanStay(maze,entry)) 161 { 162 //入口非法 163 return ; 164 } 165 //3.标记入口点,将入口点入栈 166 Mark(maze,entry); 167 SeqStactPush(&stack,entry); 168 while(1) 169 { 170 //4.进入循环,获取当前栈的栈顶元素,栈顶元素一定能落脚 171 Point cur; 172 int ret = SeqStactTop(&stack,&cur); 173 if(ret == 0) 174 { 175 //栈为空 176 return ; 177 } 178 //5.判定这个点是否为出口,若是出口,函数返回 179 if(IsExit(maze,cur,entry)) 180 { 181 printf("找到了一条路径\n"); 182 return; 183 } 184 185 //6.按照顺时针方向取相邻点,判断相邻点是否能落脚,若能就标记,并入栈,立刻进入到下一轮循环 186 Point up=cur; 187 up.row-=1; 188 if(CanStay(maze,up)) 189 { 190 Mark(maze,up); 191 SeqStactPush(&stack,up); 192 continue; 193 } 194 195 Point right=cur; 196 right.col+=1; 197 if(CanStay(maze,right)) 198 { 199 Mark(maze,right); 200 SeqStactPush(&stack,right); 201 continue; 202 } 203 204 Point down=cur; 205 down.row+=1; 206 if(CanStay(maze,down)) 207 { 208 Mark(maze,down); 209 SeqStactPush(&stack,down); 210 continue; 211 } 212 213 Point left=cur; 214 left.col-=1; 215 if(CanStay(maze,left)) 216 { 217 Mark(maze,left); 218 SeqStactPush(&stack,left); 219 continue; 220 } 221 //7.若四个相邻点都不能落脚,就出栈当前点,相当于回溯 222 SeqStactPop(&stack); 223 } 224 return; 225 }
方法三:迷宫有多条出路,寻找迷宫的最短路径
迷宫地图为
先将迷宫的地图初始化
233 void MaseInitShortPath(Maze* maze) 234 { 235 int map[Max_ROW][Max_COL]={{0,1,0,0,0,0}, 236 {0,1,1,1,0,0}, 237 {0,1,0,1,1,1}, 238 {1,1,1,0,0,0}, 239 {0,0,1,0,0,0}, 240 {0,0,1,0,0,0} 241 }; 242 size_t i=0; 243 for(;i<Max_ROW;++i) 244 { 245 size_t j=0; 246 for(;j<Max_COL;++j) 247 { 248 maze->map[i][j]=map[i][j]; 249 } 250 } 251 return ; 252 } 253
寻找最短路径的方法,是基于之前写过的栈的一些操作,需要创建两个栈分别为cur_path和shaor_path栈,来存放当前路径和存放当前走过的这些路径的较短的路径
创建GetShortPath,传入地图,和入口点,然后引用函数_GetShortPath来辅助完成寻找寻找最短路径的操作
309 void GetShortPath(Maze* maze,Point entry) 310 { 311 SeqStact cur_path;//保存当前找到对路经 312 SeqStact short_path;//当前最短路径 313 SeqStactInit(&cur_path); 314 SeqStactInit(&short_path); 315 _GetShortPath(maze,entry,entry,&cur_path,&short_path);//第一个entry为当前走到的点,第二个entry为真正的入口点 316 //打印棧中对内容 317 SeqStactDebugPrint(&short_path,"最短路径为:\n"); 318 } 319
下面是实现_GetShortPath函数,需要引入当前位置的点和入口点和当前路径和当前的较短路径
1.首先要使用之前的函数CanStay,来判断当前点是否能够落脚
2.若能落脚,就使用Mark函数将该点标记为2,表示该点不能再走了,然后将该点入栈到当前路径cur_path的栈中
3.标记之后,要判断当前点是否为出口
1)若为出口,就说明找到了一条出路,并将当前路径cur_path与当前的最短路径short_path进行比较,若cur_path的长度要小于当前最小路径short_path的长度,或者当前最小路径short_path为空,则将用SeqStackAssgin函数将cur_path栈中的内容赋给short_path栈,同时还要回溯,来寻找下一条路径
2)若
cur_path的长度要大于等于当前最小路径short_path的长度,就进行回溯,来寻找其他路径,在回溯之前要将当前栈cur_path栈的栈顶元素,出栈,因为此时栈中存放的路径不是最小路径,就没有用处了,表示这条路就没有用处了
4.若当前点不是出口,就需要按照上、右、下、走(顺时针)的顺序来探测当前点的四个方向的点是否能够落脚,此时就要递归地调用_GetShortPath函数自身,每次递归的时候
递归的点都是下次要走的点,而此点是否能走,就要交给函数来判断
5.若四个方向都探测过了,并且都不能落脚,就进行出栈
详细见程序
254 void _GetShortPath(Maze* maze,Point cur,Point entry,SeqStact* cur_path,SeqStact* short_path) 255 { 256 257 printf("cur:%d,%d\n",cur.row,cur.col); 258 //1.判断当前点是否能落脚 259 if(!CanStay(maze,cur)) 260 { 261 //在地图之外,不能落脚 262 return ; 263 } 264 //2.若能落脚,将当前点进行标记,并插入到当前找到对路径cur_path中 265 Mark(maze,cur); 266 SeqStactPush(cur_path,cur); 267 //3.若标记完,判断当前点是否为出口 268 if(IsExit(maze,cur,entry)) 269 { 270 271 //a)若为出口,则找到一条路经,将当前路经cur_path与short_path路径进行比较, 272 //若当前路径cur_path比最短路径short_path短或者当前路径short_path为空,则就用当前路径cur_path来代替最短路径short_path 273 // 同时还要回溯,来继续找下一条路径 274 printf("找到了一条出路\n"); 275 if(cur_path->size < short_path->size || short_path->size == 0) 276 { 277 printf("找到了一条较短的出路\n"); 278 //将棧对号赋值 279 SeqStactAssgin(cur_path,short_path); 280 } 281 282 //b)若当前路径cur_path对长度大于等于最短路径short_path的长度,就尝试去找其他路径,进行回溯, 283 // 在回溯前要将cur_path对棧顶元素出棧,因为cur_path保存着当前路路径,若不是这条路经,则表示这条路已经失败 284 SeqStactPop(cur_path); 285 return ; 286 } 287 //4.若当前点不是出口点,则要继续探测其余的四个方向 288 Point up = cur; 289 up.row-=1; 290 _GetShortPath(maze,up,entry,cur_path,short_path); 291 292 Point right = cur; 293 right.col+=1; 294 _GetShortPath(maze,right,entry,cur_path,short_path); 295 296 Point down = cur; 297 down.row+=1; 298 _GetShortPath(maze,down,entry,cur_path,short_path); 299 300 Point left = cur; 301 left.col-=1; 302 _GetShortPath(maze,left,entry,cur_path,short_path); 303 //为了找到其他的路而递归 304 //5.若四个方向都递归地探测过了,就出棧,回溯到上一个点 305 SeqStactPop(cur_path); 306 return ; 307 }
下面是实现SeqStackAssgin函数,来将一个栈中的内容赋值到另一个栈中,将from栈中的内容赋值到to栈中
1.首先先要比较两个栈的长度,若from栈中的元素个数小于等于to栈中元素的个数,则直接循环将from中的元素赋值到to栈中,不会出现内存越界的情况
2.若
from栈中的元素个数大于to栈中元素的个数,就要释放to中原有的内容,然后根据from中元素的个数来确定要申请内存空间的大小,再给to栈申请一个足够的内存空间,然后进行赋值操作
118 //将一个棧对值赋值到另一个棧中 119 //由于棧支持动态内存对增长,因此 120 //1.from中的元素个数<=to中的元素个数,则进行循环赋值 121 //2.若from中的元素个数>to中的元素个数,则必须要保证to中对内存足够赋值,防止内存越界 122 void SeqStactAssgin(SeqStact* from,SeqStact* to) 123 { 124 //为了保证to中的内存能够容纳from中的元素 125 //1.释放to中原有对内容 126 SeqStactDestroy(to); 127 //2.1根据from中对元素个数确定内存申请大小, 128 to->size=from->size; 129 to->capacity=from->capacity; 130 //2.2给to申请一个足够的内存空间 131 to->data=(SeqStactType*)malloc(to->capacity*sizeof(SeqStactType)); 132 //3.赋值 133 size_t i=0; 134 for(;i<from->size;++i) 135 { 136 to->data[i]=from->data[i]; 137 } 138 return ; 139 }
方法四:针对一个复杂迷宫(可能带环、有多个出口)寻找最短路径
迷宫地图如图所示,其中有环和多个出口
迷宫地图如图所示,其中有环和多个出口
针对带环的迷宫问题,就不能再之前那样,将走过的点标记为2,来记录点了,在处理带环问题的迷宫时,就采用一种将将路线标号的方式,在一条路线上的每个点都标上依次增大的标号,若另一条路走到该点的序号要小于该点原本的序号,就将原本的点的序号覆盖,换上新的序号,继续往下标号,若另一条路要标记 的序号,大于或等于原本的序号,则就说明新的这条路就不是最短路径,此路作废,在寻找其他出路,详细见图和程序
先将迷宫地图初始化
328 void MaseInitShortPathWithCycle(Maze* maze) 329 { 330 int map[Max_ROW][Max_COL]={{0,1,0,0,0,0}, 331 {0,1,1,1,0,0}, 332 {0,1,0,1,1,1}, 333 {1,1,1,1,0,0}, 334 {0,0,1,0,0,0}, 335 {0,0,1,0,0,0} 336 }; 337 size_t i=0; 338 for(;i<Max_ROW;++i) 339 { 340 size_t j=0; 341 for(;j<Max_COL;++j) 342 { 343 maze->map[i][j]=map[i][j]; 344 } 345 } 346 return ; 347 }
创建函数GetShortPathWithCycle,传入地图和入口点,找最短路径的方法与方法三着最短路径的方法相同,还是使用两个栈,分别存放着当前路径和最短路径,来实现,该问题与之前方法的区别在于,该问题存在了一个环,而若还按照之前的方法来实现,就将走过的路标记为2,那么走过的路就不能再判断了,那么就会错过最短路径的寻找,
使用_GetShortPathWithCycle函数来辅助完成找最短路径的操作
442 void GetShortPathWithCycle(Maze* maze,Point entry) 443 { 444 SeqStact cur_path; 445 SeqStact short_path; 446 SeqStactInit(&cur_path); 447 SeqStactInit(&short_path); 448 Point pre={(-1,-1)}; 449 _GetShortPathWithCycle(maze,entry,pre,entry,&cur_path,&short_path); 450 SeqStactDebugPrint(&short_path,"找到最短的路经\n"); 451 452 }
下面是实现
_GetShortPathWithCycle函数,需要传入地图,当前点的位置,入口点的位置,和存放当前路径的栈和当前最短路径的栈
1.首先要用CanStayWithCycle函数来判断当前点是否能落脚,在这里判断是否能落脚的判断条件与之前的判断方法不同,
之前是只要是走过的路都标记为2,若下一条路走到2号标记点的时候,就走不了了
现在新的判断路是否能走的函数为CanStayWithCycle,具体判断方式见下面的CanStayWithCycle函数的描述即程序
2.若能落脚,就将当前点进行标记(标记方法也与之前不同),然后将当前点入栈到cur_path栈中
3.然后判断当前点是否为出口(出口的判断方法不变),若是出口,则返回一句话“找到一条出口”,然后判断当前路径的长度与当前最短路径的长度谁的路径更短就将较短的路径赋值给shor_path(方法与之前方法三相同),然后要进行回溯,这是为了找下一条路径
4.若当前点不是出口,就要
按照上、右、下、走(顺时针)的顺序来探测当前点的四个方向的点是否能够落脚,此时就要递归地调用_GetShortPathWithCycle函数自身,每次递归的时候
递归的点都是下次要走的点,而此点是否能走,就要交给函数来判断
5.若四个方向都探测过了,就进行出栈操作,回溯
详细见程序
394 void _GetShortPathWithCycle(Maze* maze,Point cur,Point pre,Point entry,SeqStact* cur_path,SeqStact* short_path) 395 { 396 397 printf("cur:%d,%d\n",cur.row,cur.col); 398 //1.判断当前点是否能落脚(判定规则变了) 399 if(!CanStayWithCycle(maze,cur,pre)) 400 { 401 return ; 402 } 403 //2.若能落脚,标记当前点且当前点插入到cur_path中(标记规则也变了) 404 MarkWithCycle(maze,cur,pre); 405 if(IsExit(maze,cur,entry)) 406 { 407 printf("找到了一条路径\n"); 408 SeqStactPush(cur_path,cur); 409 //3.判定当前点是否为出口(判定规则没变) 410 //若是出口,找到路径,另cur_path与short_path比较 411 if(cur_path->size < short_path->size || short_path->size == 0) 412 { 413 printf("找到了一条较短的出路\n"); 414 SeqStactAssgin(cur_path,short_path); 415 } 416 //进行回溯,不管当前找到对路径是否为最短路径,都要进行回溯 417 SeqStactPop(cur_path); 418 return ; 419 } 420 //若当前点不是出口,以当前点为基准点按照上、右、下、左(顺时针)的顺序判定四个方向 421 //若四个方向都递归地探测过了,就出棧,进行回溯 422 Point up = cur; 423 up.row-=1; 424 _GetShortPathWithCycle(maze,up,cur,entry,cur_path,short_path); 425 426 Point right = cur; 427 right.col+=1; 428 _GetShortPathWithCycle(maze,right,cur,entry,cur_path,short_path); 429 430 Point down = cur; 431 down.row+=1; 432 _GetShortPathWithCycle(maze,down,cur,entry,cur_path,short_path); 433 434 Point left = cur; 435 left.col-=1; 436 _GetShortPathWithCycle(maze,left,cur,entry,cur_path,short_path); 437 438 SeqStactPop(cur_path); 439 return; 440 } 441
下面是实现CanStayWithCycle函数,来判断该点是否能够落脚,
1.首先要判断该点是否在地图上,和是否为墙(0)
2.若该点为1,则可以直接落脚
3.若当前点标记的值大于要标记的点,则也可以直接落脚
349 int CanStayWithCycle(Maze* maze,Point cur,Point pre) 350 { 351 //1.判断当前点是否在地图上 352 if(cur.row<0 || cur.row>=Max_ROW || cur.col<0 || cur.col>=Max_COL) 353 { 354 return 0; 355 } 356 //当前点是不是墙 357 int cur_value=maze->map[cur.row][cur.col]; 358 if(cur_value == 0) 359 { 360 return 0; 361 } 362 //3.若当前点为1,表示可以直接落脚 363 if(cur_value == 1) 364 { 365 return 1; 366 } 367 //4.在取出prev_value之前要判断prev点是否为非法点 368 //if(pre.row == -1 && pre.col == -1) 369 //{ 370 //若pre为非法点就只有这一种情况 371 //但这种情况又不用考虑,因为若pre为非法点,则cur一定为入口点, 372 //判定入口点是否能落脚与pre无关 373 //} 374 int pre_value=maze->map[pre.row][pre.col]; 375 if(cur_value>pre_value+1) 376 { 377 return 1; 378 } 379 return 0; 380 }
下面实现实现MarkWithCycle函数,来将路径上的点进行标记
382 void MarkWithCycle(Maze* maze,Point cur,Point pre) 383 { 384 if(pre.row == -1 && pre.col == -1) 385 { 386 maze->map[cur.row][cur.col]=2; 387 //针对入口点与已被标记的点,此时的pre为非法点,不能用Maze方法进行标记 388 return; 389 } 390 int pre_value=maze->map[pre.row][pre.col]; 391 maze->map[cur.row][cur.col]=pre_value+1; 392 } 393
为了将迷宫的路径从入口进行打印出来,需要对栈进行遍历操作
6 //次函数用于迷宫测试所用 7 //通常棧是不允许进行遍历的,单若进行测试或者调试除外 8 //此时的遍历仅用于测试所用 9 //之所以写这个函数是为了能够从函数对入口开始打印路线 10 void SeqStactDebugPrint(SeqStact* stack,const char* msg) 11 { 12 printf("[%s]\n",msg); 13 size_t i=0; 14 for(;i<stack->size;i++) 15 { 16 printf("(%d,%d)\n",stack->data[i].row,stack->data[i].col); 17 } 18 printf("\n"); 19 } 20