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

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

  

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

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

  

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

  关卡设计者的奥义

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

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

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

  

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

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

  并查集奥义

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

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

  2.  合并两个集合

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

  查找操作

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

  合并操作

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

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


 

  
并查集的实现

 


 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  }
  吴昊品游戏核心算法 Round 18 —— 吴昊教你玩Glow Puzzle(前篇)

 

  

 判断关卡是否合格的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  }

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

 

  

那么,有了这个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中的向量容器来装填。

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

 

  

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

 

 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 }

 


 

你可能感兴趣的:(round)