大家好,《蛇棋》游戏旨在寻找最短路径,本文讲解如何使用广度优先遍历算法(BFS,不了解该算法的同学,请参考这篇文章)来解决该问题。
笔者喜欢实用的技术,而图论就是其一。诚然,其他数据结构也应用广泛,但由于图论的特性,使其与我们的生活联系最为紧密。接下来,笔者就用经典游戏《蛇棋》举例,说明如何使用BFS来解决实际生活中的问题。
想必大家小时候一定都玩过《蛇棋》,规则就不多说了。而这里要说的,则是如何通过图论和BFS,找到最短的路径(掷骰子的次数,以及每次掷的点数),抵达终点赢取胜利。下图为蛇棋棋盘,本文将以它来举例说明。
可以看出,我们能以无数种走法抵达终点,但哪个是最优的呢?更重要的是,如何用代码来实现?这正是本文接下来要说到的。现在来想想看,如何用图(顶点和边)来表示这张棋盘?
别太苛求自己,先从简单的开始……只考虑最开始的7个方块,弄清楚什么该用项点来表示,什么该用边来表示,最后在纸上画出来。有了这个思路,很容易想到:棋盘上的数字方块可以用顶点表示,那么,边又是什么呢?按掷出来的点数,我们可以从一个方块走到另一个。现在,先不考虑棋盘上的梯子和蛇,只要把棋盘的一步部分以图的形式画出来,最后我们可以得到下图:
可以看到,按掷的点数,我们有六条路线离开方块1,对于方块2,方块3……同理。现在要明确一个重点,记住,这是有向图!一旦掷出了5点并且走到方块6,就再也无法回头了。现在,我们让问题稍微复杂一些,在上图的方块6中增加一张梯子,想想看边会有什么变化?
如果你在方块1掷出5点,则会直接到达方块27(方块2掷出4点,方块3掷出3点同理,依次类推)。现在从“逻辑”上来说,方块6在图中已经不存在了(如果没有秒懂,就再好好想想)!
无论何时到达了方块6,都无法停留在那里,而是直接跳到方块27。现在,如果用邻接表表示这张图,用顶点1表示方块1,那么顶点1的链表中是有顶点6还是有顶点27?当然是顶点27!因为顶点6即是顶点27呀!
因此,图中的边的箭头都不会停在顶点6,注意到了吗?所以,在邻接表中,顶点6的链接中会有什么呢?什么都没有!因为无论掷出几点,都无法抵达方块6,所以邻接节点链表中凡是到达顶点6的都应为空。这两点很重要,在为棋盘构造邻接表时需要特别注意。
如果方块上不是梯子,而是蛇,处理方式是一样的。逻辑上来那说该方块在链表中是不存在的,与其邻接的边也可以移除。唯一不同于梯子的是,到达蛇头会跳到数值更低的方块。
遇到蛇会增加无意义的路程,所以最优路径不会需到蛇。所以,我们假定一定不会遇到蛇,在求解最优路径时需要避开遇到蛇的情况。为了更好地理解上文中所说走到梯子和蛇的场景,这里给出了邻接表的图例:
上图应该能清晰地解释所有的问题,如果你还有不清楚的,请在评论区提问吧!现在,有了邻接表我们要做什么呢?当然是对该表调用广度优先算法啊!
用图论知识解决蛇棋问题的难点在于构造顶点和边,一旦构造好图,接下来要做的就只是对其调用BFS,就可以获知从顶点1到达顶点100的最短路径了。现在,试着实践一下,投入一点精力,相信你会成功的。但如果你还是没能得到正确答案,我将我的代码放在下面。在看代码前,以下先阐述算法的思路:
首先,不考虑棋盘上的蛇和梯子把所有的边都加上,接着再根据蛇和梯子的情况,将相关的边一一去掉;
若一个顶点n有梯子或蛇,应该按上文图中描述的那样,将能抵达该点的边进行替换。对于顶点n,需将以下顶点的边都替换成新值:顶点(n - 1),(n - 2),(n - 3),(n - 4),(n - 5),(n - 6)。之所以选这些点,是因为只有这些项点的边包含有能抵达顶点n的边。
替换的过程被封装成了
replace()
方法,它获取链表后,搜索包含oldVertext
值的节点,并将其替换成newVertex
。在数组中总是包含有一个无用的元素,占了索引0,使得有效的值是从索引1开始算起。
最短路径所掷骰子次数等于最后一个顶点(顶点100)的层级数,为什么?思考一下,你会明白的!
-
这里构造了一个递归方法
printShortestPath()
,它递归地找查每一个顶点的父节点,直到到达起始节点(节点1),期间会依照其走过的递归栈输出其到达的顶点,倒过来看就是路径。/* ========== ========== ========== ========= */ // Snakes and Ladders // // Quickest Way to Win using // // Breadth First Search // // // // Functions follow Pascal Case // // Convention and Variables // // follow Camel Case Convention // // // // Author - Vamsi Sangam // // Theory of Programming // /* ========== ========== ========== ========== */ #include <stdio.h> #include <stdlib.h> structNode { intval; structNode * next; }; // Adds a new edge, u --> v, to adjacencyList[u] structNode * add(structNode * head, intvertex) { structNode * traverse = (structNode *) malloc(sizeof(structNode)); traverse->val = vertex; traverse->next = head; returntraverse; } // The Breadth First Search Algorithm procedure. Takes empty parent and level // arrays and fills them with corresponding values that we get while applying BFS voidbreadthFirstSearch(structNode * adjList[], intvertices, intparent[], intlevel[]) { structNode * temp; inti, par, lev, flag = 1; lev = 0; level[1] = lev; while(flag) { flag = 0; for(i = 1; i <= vertices; ++i) { if(level[i] == lev) { flag = 1; temp = adjList[i]; par = i; // Exploring all the adjacent vertices while(temp != NULL) { if(parent[temp->val] != 0) { // A level for this is already set temp = temp->next; continue; } level[temp->val] = lev + 1; parent[temp->val] = par; temp = temp->next; } } } ++lev; } } // Replaces the value of an edge (u -->) v to (u --> v') // Traverses the entire list of adjacencyList[u] => O(|E|) operation // Here, "v" is stored as "oldVertex" and "v'" is stored as "newVertex" voidreplace(structNode * head, intoldVertex, intnewVertex) { structNode * traverse = head; // Search for the occurence of 'oldVertex' while(traverse->next != NULL) { if(traverse->val == oldVertex) { break; } traverse = traverse->next; } // replace it with the new value traverse->val = newVertex; } // Prints the Adjacency List from vertex 1 to |V| voidprintAdjacencyList(structNode * adjList[], intvertices) { inti; // Printing Adjacency List printf("\nAdjacency List -\n"); for(i = 1; i <= vertices; ++i) { printf("%d -> ", i); structNode * temp = adjList[i]; while(temp != NULL) { printf("%d -> ", temp->val); temp = temp->next; } printf("NULL\n"); } } // A recursive procedure to print the shortest path. Recursively // looks at the parent of a vertex till the 'startVertex' is reached voidprintShortestPath(intparent[], intcurrentVertex, intstartVertex) { if(currentVertex == startVertex) { printf("%d ", currentVertex); } elseif(parent[currentVertex] == 0) { printShortestPath(parent, startVertex, startVertex); printf("%d ", currentVertex); } else{ printShortestPath(parent, parent[currentVertex], startVertex); printf("%d ", currentVertex); } } intmain() { intvertices, edges, i, j, v1, v2; vertices = 100; // For a 10X10 board // We will make the Adjacency List's size // (|V| + 1) so that we can use it 1-indexed structNode * adjList[vertices + 1]; intparent[vertices + 1]; // Just like 'adjList' -> Size = |V| + 1 intlevel[vertices + 1]; // Just like 'adjList' -> Size = |V| + 1 for(i = 0; i <= vertices; ++i) { // Initialising our arrays adjList[i] = NULL; parent[i] = 0; level[i] = -1; } // Initially we will add edges (a move) from // one location to another location as if // there were no snakes or ladders at all for(i = 1; i <= vertices; ++i) { // From vertex 'i', add a path to // the next 6 locations possible for(j = 1; j <= 6 && j + i <= vertices; ++j) { adjList[i] = add(adjList[i], i + j); ++edges; } } intladderCount, snakeCount; printf("Enter the Number of Ladders - "); scanf("%d", &ladderCount); printf("Enter the Ladder Edges -\n"); // Dealing with Ladder Edges (v1 ---> v2) for(i = 0; i < ladderCount; ++i) { scanf("%d%d", &v1, &v2); // If the ladder can affect any position, it is // the previously 6 possible moves of v1 j = v1 - 6; if(j < 1) { j = 1; } // So we make a correction in those vertices for(; j < v1; ++j) { // Replacing Vertex v1 by v2 replace(adjList[j], v1, v2); } } printf("Enter the Number of Snakes - "); scanf("%d", &snakeCount); printf("Enter the Snake Edges -\n"); // Dealing with Snakes Edges for(i = 0; i < snakeCount; ++i) { scanf("%d%d", &v1, &v2); // If the snake can affect any position, it is // the previously 6 possible moves of v1 j = v1 - 6; if(j < 1) { j = 1; } // So we make a correction in those vertices for(; j < v1; ++j) { // Replacing Vertex v1 by v2 replace(adjList[j], v1, v2); } } printAdjacencyList(adjList, vertices); breadthFirstSearch(adjList, vertices, parent, level); printf("\nNumber of Moves required = %d\n", level[vertices]); if(level[vertices] != -1) { // Printing the shortest path from vertex 1 // to the last vertex 100 (vertices) printShortestPath(parent, vertices, 1); } return0; }
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。