3

原文:Snakes and Ladders Game Code
作者:VAMSI SANGAM

大家好,《蛇棋》游戏旨在寻找最短路径,本文讲解如何使用广度优先遍历算法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;
       }
    

田靖荣
1k 声望25 粉丝

PHPer