吴昊品游戏核心算法 Round 18 —— 吴昊教你玩Glow Puzzle(前篇)

  

  

  Glow Puzzle,又名辉光难题,欢迎来到这个非常吸引人的连接点益智游戏。是一个极其上瘾的游戏!我们的目标是连接中的所有难题点采用连续路径,但你不能重 复使用任何已完成的路径。有多达659大脑可让你在这场比赛中挑战戏弄的水平。3游戏模式可供选择:(1)经典模式,659级!(2) 挑战模式 (3)记忆模式。

   

  

如图所示,这是Glow Puzzle中的两个比较基本的问题,也就是Puzzle #1和Puzzle #3,在关卡的设计中,我们不能单单地根据顶点和边数的多少来判定这个游戏的复杂程度,有很多其余的参数需要考虑(这里我会在本期Round的之后部分说 明的)。游戏的规则类似于“七座桥问题”,也就是说,你采用一种策略,通过一笔将这幅图连续地勾勒出来,我们的要求是无重复,也就是,每条路径都恰好经过 一次。这款游戏除了画面比较绚丽以外,左右下角的两个辅助工具——左边那个当然是退回到起始的状态,而右边那个则是一盏灯,同理地,它可以提示你之后的一 些路径信息(有且仅能用一次,如果你一开始就是错的,它也会适时地指出来)

  关卡设计者的奥义

(欧拉回路的详细内容在后篇介绍)

  关于这款游戏,我们可以设计两个AI,一个为游戏的关卡设计者准备,一个为游戏的玩家准备。对于关卡设计者来说,为了让游戏变得更丰富,更有趣味性,当然 是不能过于唯一地用“胜与负”来衡量一个游戏,这样会很无聊的,我们可以增加一些小物件,使得游戏更可爱一些,如图所示:

     

  

  Glow Puzzle中可以返回三种结果,一种是GOOD WORK,代表你合格地完成了任务,FAILED代笔你没有合格地完成任务,AWESOME则代表你很“恐怖”地完成了任务。其中有两个判定点,一个是你 可以从起始点回到起始点,而不是除了起始点以外的另外一个点,这当然是其一,其二,代表你的速度足够地快,按照相关的算法可以计算出一个你所得到的返回分 数。
  对于关卡设计者来说,我们判断一个关卡是否是合理的,则要看它是否可以让玩家完成任务(同时还要一定程度地考虑一个关卡的难易程度)

  首先,我们还是来解决一个最基本的问题,就是判定,需要用到的数据结构有:并查集+欧拉回路。

  并查集奥义

  对不相交集合进行俩种操作:

  1.  检索某元素属于哪个集合

  2.  合并两个集合

  我们最常用的数据结构是并查集的森林实现,也就是说,在森林中,每棵树代表一个集合,用树根来标识一个集合,树的形态不重要,重要的是每棵树有哪些元素。

  查找操作

  查找一个元素v很简单,只需要顺着叶子到根节点的路径找到u所在的根节点,然后把v到u的路径上面的结点的父节点都设置为根节点,这样减少了查找的次数(路径压缩)。

  合并操作

  为了把两个集合s1和s2并起来,只需要把s1的根的父亲设置为s2的根节点,我们可以做一个优化,将深度小的合并成为深度大的子树,这样子查找的次数少些。

 


 

  
并查集的实现

 


 1  /* 初始化集合*/
 2  void Make_Set(int x)
 3   {
 4       father[x] = x; //根据实际情况指定的父节点可变化
 5       rank[x] = 0;   //根据实际情况初始化秩也有所变化
 6  } 
 7 
 8 
 9 /* 查找x元素所在的集合,回溯时压缩路径*/
10 
11  int Find_Set(int x)
12 {
13 
14      if (x != father[x])
15 
16      {
17 
18        father[x] = Find_Set(father[x]); //这个回溯时的压缩路径是精华
19      }
20      return father[x];
21  }
22 
23 
24 //将秩小的合并到秩大的集合中
25  void Union(int x, int y)
26  {
27      x = Find_Set(x);
28      y = Find_Set(y);
29      if (x == y) return;
30      if (rank[x] > rank[y]) 
31      {
32          father[y] = x;
33      }
34      else
35      {
36          if (rank[x] == rank[y])
37          {
38              rank[y]++;
39          }
40          father[x] = y;
41      }
42  }
 

 

  

 判断关卡是否合格的AI实现

   这个AI很简单,输入先是一张无向图的点数和边数(N,M),后面则是M条对应的边,每条边用两个对应的端点表示。在输出中,我们用1表示关卡合格,用0 表示关卡是不合格的(在AI中充分利用了并查集的上述两个函数,也就是查找函数和合并函数,建立欧拉回路的过程是在主函数给出的)。实际上,在一笔画判定 的函数中,该AI过于严格,更好的可以在之后进行改进。

 

 1  #include<iostream>
 2  using namespace std;
 3  
 4  //读入每个结点的度,由于是无向图,不计入度和出度
 5  int degree[1001];
 6  //制造一个游戏界面
 7  bool map[1001][1001];
 8  
 9  int node[1001];
10  int size[1001];
11  
12  int a,b,n,m;
13  
14  //找到那个结点的祖先(查找函数)
15  int find(int x)
16  {
17    //表示已经找到祖先结点了
18    if(node[x]==x) return x;
19    //每次递归,直到找到祖先结点
20    else return node[x]=find(node[x]);    
21  }
22  
23  //合并函数
24  void combine(int a,int b)
25  {
26    int ka=find(a);
27    int kb=find(b);
28    //如果不是一个祖先的话,将轶小的合并到轶大的集合中
29    if(ka!=kb)     
30    {
31      if(size[ka]>size[kb])
32      {
33        node[kb]=ka;
34        size[ka]+=size[kb];                     
35      }               
36      else
37      {
38        node[ka]=kb;
39        size[kb]+=size[ka];    
40      }
41    }
42  }
43  
44  int main()
45  {
46    //遇到0就退出
47    while(cin>>n&&n)
48    {
49      cin>>m;
50      //将点和地图清零
51      memset(degree,0,sizeof(degree));
52      memset(map,0,sizeof(map));
53      //建立每个点
54      for(int i=1;i<=n;i++)
55      {
56        //最开始的祖先结点是自己
57        node[i]=i;
58        size[i]=1;        
59      }           
60      while(m--)
61      {
62        cin>>a>>b;
63        //每次对两点加入边
64        if(map[a][b]==0)  
65        {
66          degree[a]++;
67          degree[b]++;
68          //标记一条边
69          map[a][b]=map[b][a]=1;
70          combine(a,b);                  
71        }        
72      }     
73      //找到祖先结点(任意一个点的)
74      int k=find(a);
75      bool flag=true;
76      for(int i=1;i<=n;i++)
77      {
78       //判定欧拉回路可以实现的充要条件为:(1)连通(2)每个结点度都是偶数(3)每个结点都有共同的祖先结点
79        if(degree[i]&1||degree[i]==0||find(i)!=k)
80        {
81          flag=false;
82          break;                                         
83        }        
84      }
85      if(flag) cout<<"1"<<endl;
86      else cout<<"0"<<endl;
87    }
88    return 0;    
89  }

 

 

  

那么,有了这个AI,我们就可以基本地判定出这个游戏是否是关卡合格的了。注意到这个if条件句,过于严格:  if(degree[i]&1||degree[i]==0||find(i)!=k), 这里,可以将if条件句中的第一个条件弱化成“允许两个点的度为奇数”,这样的话,仍然是可以满足关卡的要求的,只是如果这样的话,就不能让起始点和终结 点都是相同的点了,这两个点必须不同。那么,我们可以先用上面一个AI判定出一些“具有AWESOME答案”的关卡,然后,再用改进的AI例程给出一些 “最终只能到达GOOD WORK”这个级别的答案。

  我们如何判定一个关卡的难易程度呢?一般情况下,当然,边数和点数越多,关卡应该更难一些,但是,这也不一定。我觉得,参数应该更多一些,比如,挂卡输入到AI中的运行时间,这就是一个不错的判定,毕竟,计算机只是比人要做的更快一些罢了。

  进一步地

  我们对关卡的设计给出更高的要求,比如,我们希望加大难度,但是,我们以一种“另外的方式”进行难度的加大,我们不再苛求要一笔解决问题,我们用两笔,或 者N笔解决问题。那么,游戏的难度确实加大了,加大的同时,我们甚至不认为它加大了,因为,我们有几次的重复,这样,使得游戏也变得更有意思了。

  这个AI,我们仍然按照原来的方式输入一个地图,只是返回的值不再是0和1,我们返回一个最小的笔画。

  公式:

  (a)如果是个欧拉回路一笔就可以完成。

  (b)笔划数=奇度数/2。

  (c)总之笔划数 = 奇度数%2 + 欧拉回路数。

  这里,对于每个连通集,用STL中的向量容器来装填。

 

 

  

这里的统计也很严格,所谓的欧拉回路,也是当起始点和终止点为同一个点的时候才算的。另外,由于关卡只要求将边完全地遍历,而没有必要考虑点,所以,孤立点可以不考虑,就让它孤立吧!

 

 1    #include <iostream>
 2  #include <stdio.h>
 3  #include <memory.h>
 4  #include <vector>
 5  
 6  #define maxn 100005
 7  #define maxm 200005
 8  
 9  using namespace std;
10 
11  int father[maxn],m,n,used[maxn],odd[maxn],deg[maxn];
12 
13  //初始化函数
14  void init()
15  {
16    int i;
17    //表明
18    memset(used, 0sizeof(used));
19    //顶点度数的统计
20    memset(deg, 0sizeof(deg));
21    //统计各个欧拉回路的祖先结点为奇数点的情况
22    memset(odd, 0sizeof(odd));
23    for (i = 1; i <= n; i++)
24      father[i] = i;
25  }
26 
27  //查找函数
28  int find (int x)
29  {
30    if(father[x] != x)
31      father[x] = find(father[x]);
32    return father[x];
33  }
34 
35  //建立并查集,归并祖先结点
36  void make (int a,int b)
37  {
38    int x = find(a);
39    int y = find(b);
40    if (x != y) father[y] = x;
41  }
42  
43  int main()
44  {
45    int a,b,k;
46    //利用向量容器装填不同的欧拉回路
47    vector <int> t;
48    //每次读入一个样例
49    while (scanf("%d%d",&n,&m)!=EOF)
50    {
51      init();
52      //清空向量容器
53      t.clear();
54      int count = 0;
55      //读入所有的边
56      while (m--)
57      {
58        scanf("%d%d",&a,&b);
59        //两个点的度数自增
60        deg[a]++;
61        deg[b]++;
62        //归并这两个点
63        make (a,b);
64      }
65      for (int i=1; i<=n; i++)
66      {
67        //找到一个祖先结点
68        k = find(i);
69        //如果那个祖先结点还没有使用过
70        if (!used[k])
71        {
72          //进入容器
73          t.push_back(k);
74          //标记为已经使用了
75          used[k] = 1;
76        }
77        //如果存在结点是奇点,则odd[k]++
78        if (deg[i] & 1)
79          odd[k]++;
80      }
81      int sum = 0;
82      //对每个欧拉回路进行遍历
83      for (int i=0; i<t.size(); i++)
84      {
85        k = t[i];
86        //如果该集合在孤立点的话,则继续,因为,只需要遍历所有的边就可以了
87        if(deg[k] == 0continue;  
88        //如果该集合是欧拉回路,则有一条路
89        else if(odd[k] == 0) sum++;
90        //否则,加上odd[k] /2     
91        else sum += odd[k]/2;
92           }
93           printf("%d\n",sum);
94              
95     }
96     return 0;
97 }

 


 

posted on 2013-04-23 23:17  吴昊系列  阅读(1097)  评论(5编辑  收藏  举报

导航