广度优先搜索(BFS)解决华容道问题

    xiaoxiao2024-12-29  56

    小时候玩华容道一直玩不过,即使到了现在还是不知道这游戏的玩法(囧),这几天心血来潮想看看网上有没有现成的华容道解法,结果居然没找到。那就只能自己写一个了。

    先看一下华容道经典关卡“横刀立马”。

    我们可以把这个图看成一个4*5的二维数组,用不同数字代表不同的角色。这里我用0代表空位,1代表小兵,23456分别表示张飞黄忠赵云马超关羽这四个武将,7代表曹操,那么整个图的信息就表示为右图中的数组了,注意右图中的一行是左图中的一列,所以看着是翻转过。

    bfs的基础的不再提了。重点是哈希和遍历的方法。图中一共有8种角色,20个位置,因此总共的组合数有8^20=2^60<2^64,所以用long型来表示哈希值。至于遍历的话,如果移动角色的话感觉有点麻烦,我用的方法是对每个空位尝试进行上下左右移动来得到新的结果,也是另一种意义的移动角色。因此我们需要byte[4]来表示当前图中空位的位置(自己在图中搜索自然也是可以的,但那样比较耗时),以及上一步的方向(来提高筛选的效率),以及完整的路径。

    我把上下左右的操作用0,1,2,3表示,dir%2计算移动的方向,dir/2计算移动的距离。移动即对pos0操作,完成以后把新的pos0置0,把旧的pos0置为对应的人物就完成了一次移动。现在来看一下代码。

    using System; using System.Collections.Generic; class Node { public byte[,] data;//当前的位置图 public ulong hash;//哈希值 public byte[] path;//完整的路径信息 public byte[] pos0;//byte[4],分别是第一个空位的横纵坐标和第二个空位的横纵坐标 public byte lastDir;//上一步的方向,防止路线返回去 } class Program { string[] dirs = new string[] { "左", "上", "右", "下" }; string[] peoples = new string[] { "", "小兵", "黄忠", "赵云", "马超", "张飞", "关羽","曹操", "小兵2", "小兵3", "小兵4" }; Queue<Node> nodeQuene = new Queue<Node>(); HashSet<ulong> nodeSet = new HashSet<ulong>(); Node startNode = new Node(); static void Main(string[] args) { Program p = new Program(); Node startNode = p.startNode; startNode.data = new byte[4, 5] { { 2,2,3,3,1 }, { 7,7,6,1,0 }, { 7,7,6,1,0 }, { 4,4,5,5,1 } }; startNode.pos0 = new byte[] { 1, 4, 2, 4 };//横刀立马 startNode.hash = p.GetHash(startNode.data); startNode.path = new byte[] { }; startNode.lastDir = 255; if (!p.bsf(startNode)) Console.WriteLine("fail"); Console.ReadLine(); } bool bsf(Node startNode) { nodeSet.Add(startNode.hash); nodeQuene.Enqueue(startNode); while (nodeQuene.Count > 0) { Node node = nodeQuene.Dequeue(); if (node.path.Length > 150) { Console.WriteLine("beyond"); Console.ReadLine(); return true; } //遍历两个空格的四个方向 for (int i = 0; i < 2; i++) for (int j = 0; j < 4; j++) if(Move(node, i * 2, j)) return true; } return false; } ulong GetHash(byte[,] data) { //计算哈希值,如果不区分小兵的话,一共有8^20=2^60<2^64所以long型是足够的 ulong ret = 0; for (int i = 0; i < 4; ++i) for (int j = 0; j < 5; ++j) ret = ret * 8 + data[i, j]; return ret; } bool FindTarget(Node node) { //曹操在中间最下就是成功了,然后打印路径信息 if (node.data[1, 3] == 7 && node.data[1, 4] == 7 && node.data[2, 3] == 7 && node.data[2, 4] == 7) { nodeQuene.Clear(); nodeSet.Clear(); Console.WriteLine(node.path.Length); /*区分小兵的data startNode.data = new byte[4, 5] { { 2,2,3,3,1 }, { 7,7,6,8,0 }, { 7,7,6,9,0 }, { 4,4,5,5,10 } };*/ for (int i = 0; i < node.path.Length; i++) { //根据路径推算整个移动过程 byte step = node.path[i]; int[] ij = new int[] { step % 8, step % 64 / 8 }; int off = 0; if (startNode.pos0[2] == ij[0] && startNode.pos0[3] == ij[1]) off = 2; int dir = step / 64; ij[dir % 2] += dir / 2 == 0 ? 1 : -1; Console.WriteLine("把{0}({1},{2})往{3}移动 {4}", peoples[startNode.data[ij[0], ij[1]]], ij[0], ij[1], dirs[dir], i); Move(startNode, off, dir, true); Console.ReadLine(); } Console.ReadLine(); return true; } return false; } /// <summary> /// 移动节点并加入队列 /// </summary> /// <param name="node">上一个节点</param> /// <param name="off">移动的空位的偏移.0为第一个空位,2为第二个空位</param> /// <param name="dir">移动的方向</param> /// <param name="show">是否是为了展示路径信息</param> bool Move(Node node, int off, int dir, bool show = false) { if (dir - node.lastDir == 2 || dir - node.lastDir == -2) return false; Node node2 = new Node(); node2.lastDir = (byte)dir; int pos = off + dir % 2;//空位改变的坐标在pos0里的位置 int otherPos = 2 - off + dir % 2;//空位不改变的坐标在pos0里的位置 node2.pos0 = (byte[])node.pos0.Clone(); int offset = (int)(dir / 2 == 0 ? 1 : -1);//根据dir判断移动的方向 node2.pos0[pos] += (byte)offset;//将空位的位置根据dir移动offset个位置 if (node2.pos0[pos] >= 4 + pos % 2)//判断是否超出地图 return false; bool moveTwo = false; byte people = node.data[node2.pos0[off], node2.pos0[off + 1]];//判断空位要移动到的位置是哪个角色 switch (people) { case 0: return false;//交换空位没有意义 case 1: case 8: case 9: case 10: //移动小兵一定会成功 break; case 2: case 3: case 4: case 5: case 6: if (dir % 2 != people / 6) { //如果移动的方向和武将的方向一致则一定会成功,重新设置data和pos0 node2.pos0[pos] += (byte)offset;//实际0将要移动到的位置跟这个位置相差1 } else { //如果移动的方向和武将的方向不同则需要判断另一个空位的位置是否支持这个操作 //对纵向武将来说如果两个空位的横坐标不同或者另一个空位旁边的人物跟people不同 node2.pos0[otherPos] += (byte)offset; if (node.pos0[dir % 2] != node.pos0[dir % 2 + 2] ||//在这个条件成立的前提下一定不会越界 node.data[node2.pos0[2 - off], node2.pos0[2 - off + 1]] != people) return false; moveTwo = true; } break; case 7: node2.pos0[otherPos] += (byte)offset; if (node.pos0[dir % 2] != node.pos0[dir % 2 + 2] || node.data[node2.pos0[2 - off], node2.pos0[2 - off + 1]] != people) return false; node2.pos0[pos] += (byte)offset; node2.pos0[otherPos] += (byte)offset; moveTwo = true; break; default: return false; } node2.data = (byte[,])node.data.Clone(); node2.data[node.pos0[off], node.pos0[off + 1]] = people; node2.data[node2.pos0[off], node2.pos0[off + 1]] = 0; if(moveTwo) { node2.data[node.pos0[2 - off], node.pos0[2 - off + 1]] = people; node2.data[node2.pos0[2 - off], node2.pos0[2 - off + 1]] = 0; } if (show) { node.data = node2.data; node.hash = node2.hash; node.pos0 = node2.pos0; node.lastDir = node2.lastDir; return false; } node2.hash = GetHash(node2.data); if (!nodeSet.Contains(node2.hash)) { //记录这一步移动的坐标和方向信息 node2.path = new byte[node.path.Length + 1]; node.path.CopyTo(node2.path, 0); node2.path[node.path.Length] =((byte)(node.pos0[off] + node.pos0[off + 1] * 8 + dir * 64)); if (FindTarget(node2)) { return true; } nodeSet.Add(node2.hash); nodeQuene.Enqueue(node2); } return false; } }

    上一下运行结果。每次打印一步操作,然后回车进行下一步操作。顺便再贴个手机和电脑华容道的游戏链接。

    https://shouji.baidu.com/game/7547918.html               http://www.4399.com/flash/14016_3.htm

    还有一件很囧的事。我写完程序以后网上搜到了这个帖子 https://tieba.baidu.com/p/5354792142,发现他的步数比我少,于是觉得自己代码有问题,找了好久都没发现问题,最后发现他的步数计算方法跟我不一样(比如小兵连移三格认为是一步操作)。?猜测他遍历的方法是尝试移动每一个方块对所有可能的结果进行遍历。

    最新回复(0)