用Python玩烧脑小游戏《一笔画完》,瞬间闯到100关

开发 后端 开发工具
昨天和朋友出去外面吃饭,吃完饭后朋友打开了一个小程序玩了起来......

昨天和朋友出去外面吃饭,吃完饭后朋友打开了一个小程序玩了起来......

[[247317]]

游戏长这样

大概玩法是:从地图中猫的位置开始出发,并且经过所有的格子就算过关。游戏还算挺有意思的,经过我的不断努力终于过到了 30 来关的样子。

并且随着游戏关卡的增加,游戏难度也变得越来越大,过一关需要非常久的时间。

最近也正好在研究算法,就打算看能不能写个通用的算法来找出每个地图的解。

哥尼斯堡的"七桥问题"

这个游戏的玩法和哥尼斯堡的"七桥问题"有点类似。

哥尼斯堡的"七桥问题":18 世纪著名古典数学问题之一。在哥尼斯堡的一个公园里,有七座桥将普雷格尔河中两个岛及岛与河岸连接起来(如下图)。是否可能从这四块陆地中任一块出发,恰好通过每座桥一次,再回到起点?

当时人们想到的证明方法是把七座桥的走法都列出来一个一个试验,用排列组合的知识很容易得到七座桥所有的走法大概有 7! = 5040 种,如果真的逐一试验,会是个很大的工作量。

但数学家欧拉没有这样想,欧拉把两座岛和河两岸抽象成顶点,七座桥抽象成连接每个顶点的七条边,那么这个问题就能被抽象成下面的图:

假设每座桥都恰好走过一次,那么对于 A、B、C、D 四个顶点中的每一个顶点,需要从某条边进入,同时从另一条边离开,进入和离开顶点的次数是相同的,即每个顶点有多少条进入的边,就有多少条出去的边。

也就是说:每个顶点相连的边是成对出现的,即每个顶点的相连边的数量必须是偶数。

很明显,上图中 A、C、D 三个顶点相连的边都有 3 条,B 顶点的相连边为 5 条,都是奇数。因此这个图无法从一个顶点出发,且恰好走过每座桥一次。

由此次证明,人们又得到了欧拉路关系,要使得一个图形可以一笔画完,必须满足如下两个条件:

  • 图形必须是连通的不能有孤立的点。
  • 图中拥有奇数连接边的点必须是 0 或 2。

对于一个连通图,通常把从某结点出发一笔画成所经过的路线叫做欧拉路。那么这个游戏是不是就是让我们找到一条欧拉路呢?

对游戏进行抽象

按照上面证明七桥问题的方法,我们可以将游戏的地图抽象成这样:

  • 其中 14 号顶点为起点。
  • 顶点和边的关系在程序中可以刻画成一个二维列表。
  1. graph = [ 
  2.     [1, 6],    #0 
  3.     [0, 2],    #1 
  4.     [1, 7, 3], #2 
  5.     ... 
  6.     [24, 19]   #25 

graph 列表的***层表示每一个顶点,第二层则是与当前顶点有边的顶点。

抽象完这张游戏地图后可以很清楚知道,这游戏并不是让我们找到一条欧拉路。

因为找到一条欧拉路,需要的是经过每一座桥,且只经过一次,也就是说每个顶点可以被多次经过。

而这个游戏需要的是经过每一个顶点,并不要求走完每一座桥,且顶点只能被经过一次。

哈密顿通路

在研究了七桥问题发现并不能解决这类问题后,我开始向团队的表哥们请教,其中一个表哥告诉我此类问题叫做哈密顿图 (这里感谢下团队的**@xq17**表哥)。

这里说的哈密顿图,实际上是哈密顿通路的一种特殊情况,指的是:由指定的起点出发,途中经过所有其他顶点且只经过一次 ,***返回起点,称之为哈密顿回路。如果给定的图 G 具有哈密顿回路,则称图 G 为哈密顿图。

那么现在目标明确了,这个游戏的解法就是找到某个给定图的哈密顿通路。

但是问题来了!!!哈密顿通路问题,在上世纪七十年代初,被证明是 NP-hard 问题:

  • 一个复杂问题如果能在多项式时间内解决,那么它便被称为 P 类问题。
  • 一个复杂问题如果不能确定在多项式时间内解决,那么它便被称为 NP 类问题。

什么意思呢?就拿质因数分解来说吧:

  • P 类问题:23x37=?
  • NP 类问题:已知 axb=740914799,且 a 和 b 都是质数,求 a 和 b 的值

让我们来看看这个问题有多复杂:

  • 因为 axb=740914799,且 a 和 b 都是质数
  • 设 P={x|2<=x<740914799/2,x 是质数}
  • 易得 (a,b)∈PxP,即 P 与它自身的笛卡尔积

我们用一种叫做筛法的算法来求一下 P 集合的元素个数:

  1. #! /usr/bin/env python 
  2. # -*- coding: utf-8 -*- 
  3. # Coding with love by Naiquan. 
  4. import math 
  5. import time 
  6. start = time.clock() 
  7. number = int(740914799/2) 
  8. marks_list = [True] * (number + 1) 
  9. marks_list[0] = marks_list[1] = False 
  10. for i in range(2, int(math.sqrt(number)) + 1): 
  11.     j = i 
  12.     t = j 
  13.     # 去掉倍数 
  14.     while j * t <= number: 
  15.         marks_list[j * t] = False 
  16.         t += 1 
  17. elapsed = str(time.clock() - start) 
  18. print marks_list.count(True
  19. print "Time used:" + elapsed 

一共有 19841519 个质数,算了我大概 14 分钟。

PxP 的元素个数一共有 19841519^2 个,要一个个验证是否等于 740914799,无疑又是一项很大的工程,这就是典型的 NP 类问题。NP 类问题虽然难,但是可以很快验证一个给定的答案,是否正确。

比如上面的题,我告诉你答案 a=22229,b=33331,你很快就能验证答案是否正确了。而 NP-hard 问题则是比 NP 问题更难的问题,例如:围棋。

也就是说并不能找到一个友好的算法,来解决哈密顿通路问题。

算法设计

虽然找到一个图的哈密顿通路是 NP 困难的,但是好在游戏中的顶点不算太多,还是可以使用暴力一点的方法实现的,例如:图的深度优先遍历法(DFS) 即递归和回溯法思想。

算法流程:

①将当前顶点压入已访问栈和路径栈中。

②将与当前顶点相通的顶点列出来。

③随机选取一个相通的顶点,并判断此顶点是否在已访问栈中:

  • 在已访问栈中则取另一个相通的顶点。
  • 不在则将这个相通的顶点作为当前顶点。
  • 若所有相通的顶点都在已访问栈中, 则判断路径栈是否包含所有顶点。
  • 路径栈中包含所有顶点,则路径栈为当前图的哈密顿通路。
  • 不包含所有顶点则回到父顶点, 并从已访问栈和路径栈中删除。

④反复执行 1~3。

算法实现

上面说过图的顶点和边的关系可以用一个二维列表来描述:

  1. graph = [ 
  2.     [1, 6],    #0 
  3.     [0, 2],    #1 
  4.     [1, 7, 3], #2 
  5.     ... 
  6.     [24, 19]   #25 

但是要手动输入这些顶点和边的关系还是太麻烦了。仔细想了下,如果每个顶点的上下左右有顶点,就一定与上下左右的顶点有边。

那么这个二维列表就可以简化成

  1. graph = [ 
  2.     [1,1,1,1,1,1], 
  3.     [1,0,1,1,0,1], 
  4.     [1,1,1,1,1,1], 
  5.     [1,0,1,1,0,1], 
  6.     [1,1,1,1,1,1], 
  7.     [0,0,0,0,0,0]    #每个1代表一个顶点 1与上下左右的1都有边 与0则没有 长宽相等易于编写代码 

还可以再简化成一维列表:

  1. graph = [ 
  2.     '111111'
  3.     '101101'
  4.     '111111'
  5.     '101101'
  6.     '111111'
  7.     '000000' 

简直机智如我啊!于是我写了个函数对一维列表进行转换:

  1. def get_index(i, j, G): 
  2.     num = 0 
  3.     for a in xrange(i): 
  4.         num += G[a].count('0'
  5.     for b in xrange(j): 
  6.         if G[i][b] == '0'
  7.             num += 1 
  8.     return i * len(G) + j - num 
  9. def get_graph(G): 
  10.     G = [list(x) for x in G] 
  11.     EG = [] 
  12.     for i in xrange(len(G)): 
  13.         for j in range(len(G[i])): 
  14.             if G[i][j] == '0'
  15.                 continue 
  16.             side_list = [] 
  17.             if j+1 <= len(G[i]) - 1: 
  18.                 if G[i][j+1] == '1'
  19.                     index = get_index(i, j+1, G) 
  20.                     side_list.append(index
  21.             if j-1 >= 0: 
  22.                 if G[i][j-1] == '1'
  23.                     index = get_index(i, j-1, G) 
  24.                     side_list.append(index
  25.             if i+1 <= len(G) - 1: 
  26.                 if G[i+1][j] == '1'
  27.                     index = get_index(i+1, j, G) 
  28.                     side_list.append(index
  29.             if i-1 >= 0: 
  30.                 if G[i-1][j] == '1'
  31.                     index = get_index(i-1, j, G) 
  32.                     side_list.append(index
  33.             EG.append(side_list) 
  34.     return EG 

而算法的实现用图的邻接矩阵则更为方便,因此我写了一个将上列二位列表转换成邻接矩阵形式的函数:

  1. def get_matrix(graph): 
  2.     result = [[0]*len(graph) for _ in xrange(len(graph))]  # 初始化 
  3.     for i in xrange(len(graph)): 
  4.         for j in graph[i]: 
  5.             result[i][j] = 1  # 有边则为1 
  6.     return result 

主要的 DFS 算法如下:

  1. # graph为图的邻接矩阵 used为已访问栈 path为路径栈 step为已经遍历的顶点的个数 
  2. def dfs(graph, path, used, step): 
  3.     if step == len(graph): # 判断顶点是否被遍历完毕 
  4.         print path 
  5.         return True 
  6.     else
  7.         for i in xrange(len(graph)): 
  8.             if not used[i] and graph[path[step-1]][i] == 1: 
  9.                 used[i] = True 
  10.                 path[step] = i 
  11.                 if dfs(graph, path, used, step+1): 
  12.                     return True 
  13.                 else
  14.                     used[i] = False  # 回溯 返回父节点 
  15.                     path[step] = -1 
  16.     return False 
  17. def main(graph, v): 
  18.     used = []  # 已访问栈 
  19.     path = []  # 路径栈 
  20.     for i in xrange(len(graph)): 
  21.         used.append(False)  # 初始化 所有顶点均未被遍历 
  22.         path.append(-1)     # 初始化 未选中起点及到达任何顶点 
  23.     used[v] = True          # 表示从起点开始遍历 
  24.     path[0] = v             # 表示哈密顿通路的***个顶点为起点 
  25.     dfs(graph, path, used, 1) 

完整代码如下:

  1. #! /usr/bin/env python 
  2. # -*- coding: utf-8 -*- 
  3. # Coding with love by Naiquan. 
  4. def dfs(graph, path, used, step): 
  5.     if step == len(graph): 
  6.         print path 
  7.         return True 
  8.     else
  9.         for i in xrange(len(graph)): 
  10.             if not used[i] and graph[path[step-1]][i] == 1: 
  11.                 used[i] = True 
  12.                 path[step] = i 
  13.                 if dfs(graph, path, used, step+1): 
  14.                     return True 
  15.                 else
  16.                     used[i] = False 
  17.                     path[step] = -1 
  18.     return False 
  19. def main(graph, v): 
  20.     used = [] 
  21.     path = [] 
  22.     for i in xrange(len(graph)): 
  23.         used.append(False
  24.         path.append(-1) 
  25.     used[v] = True 
  26.     path[0] = v 
  27.     dfs(graph, path, used, 1) 
  28. def get_index(i, j, G): 
  29.     num = 0 
  30.     for a in xrange(i): 
  31.         num += G[a].count('0'
  32.     for b in xrange(j): 
  33.         if G[i][b] == '0'
  34.             num += 1 
  35.     return i * len(G) + j - num 
  36. def get_graph(G): 
  37.     G = [list(x) for x in G] 
  38.     EG = [] 
  39.     for i in xrange(len(G)): 
  40.         for j in range(len(G[i])): 
  41.             if G[i][j] == '0'
  42.                 continue 
  43.             side_list = [] 
  44.             if j+1 <= len(G[i]) - 1: 
  45.                 if G[i][j+1] == '1'
  46.                     index = get_index(i, j+1, G) 
  47.                     side_list.append(index
  48.             if j-1 >= 0: 
  49.                 if G[i][j-1] == '1'
  50.                     index = get_index(i, j-1, G) 
  51.                     side_list.append(index
  52.             if i+1 <= len(G) - 1: 
  53.                 if G[i+1][j] == '1'
  54.                     index = get_index(i+1, j, G) 
  55.                     side_list.append(index
  56.             if i-1 >= 0: 
  57.                 if G[i-1][j] == '1'
  58.                     index = get_index(i-1, j, G) 
  59.                     side_list.append(index
  60.             EG.append(side_list) 
  61.     return EG 
  62. def get_matrix(graph): 
  63.     result = [[0]*len(graph) for _ in xrange(len(graph))] 
  64.     for i in xrange(len(graph)): 
  65.         for j in graph[i]: 
  66.             result[i][j] = 1 
  67.     return result 
  68. if __name__ == '__main__'
  69.     map_list = [ 
  70.         '111111'
  71.         '101101'
  72.         '111111'
  73.         '101101'
  74.         '111111'
  75.         '000000' 
  76.     ] 
  77.     G = get_graph(map_list) 
  78.     map_matrix = get_matrix(G) 
  79.     # print map_matrix 
  80.     SP = 14 
  81.     main(map_matrix, SP) 

结束

在实现了功能后,我拿着这个程序成功过到了差不多一百关,然后就玩腻了,哈哈哈哈哈哈哈哈哈

本文参考资料:

  • 七桥问题_百度百科

https://baike.baidu.com/item/七桥问题/2580504?fr=aladdin

  • 哈密顿图_百度百科

https://baike.baidu.com/item/哈密顿图/2587317?fr=aladdin

  • 这个数学题我做可以,但世界毁灭了别怪我

https://www.bilibili.com/video/av19009866

  • 基于回溯法寻找哈密顿回路

http://www.cnblogs.com/cielosun/p/5654577.html

 

责任编辑:武晓燕 来源: 唯品会安全应急响应中心
相关推荐

2018-01-03 09:26:56

2018-04-26 14:41:47

机器汉字智能

2012-07-18 14:02:54

锐捷网络

2018-01-15 13:58:46

架构技术栈微信半月刊

2022-03-24 07:57:58

Python水果忍者游戏

2020-08-04 18:23:37

戴尔

2024-01-15 07:47:09

井字棋游戏编程练习Python

2022-09-16 00:32:39

SQL数据库习惯

2020-11-30 06:20:13

javascript

2023-12-07 08:37:49

TCC模式

2012-11-13 10:32:22

2022-04-25 21:50:09

前端JS面试题

2011-05-30 13:27:09

2019-02-21 08:50:09

物联网互联网IOT

2013-08-23 09:37:32

PythonPython游戏Python教程

2015-03-19 10:12:36

下拉刷新开源组件

2021-05-04 16:38:54

Linux数学游戏

2011-01-13 14:29:54

2018-07-03 16:16:24

营销

2020-12-07 16:20:53

Python 开发编程语言
点赞
收藏

51CTO技术栈公众号