吴昊品游戏核心算法 Round 18 —— 吴昊教你玩Glow Puzzle(前篇)
关卡设计者的奥义
(欧拉回路的详细内容在后篇介绍)
关于这款游戏,我们可以设计两个AI,一个为游戏的关卡设计者准备,一个为游戏的玩家准备。对于关卡设计者来说,为了让游戏变得更丰富,更有趣味性,当然 是不能过于唯一地用“胜与负”来衡量一个游戏,这样会很无聊的,我们可以增加一些小物件,使得游戏更可爱一些,如图所示:
Glow Puzzle中可以返回三种结果,一种是GOOD WORK,代表你合格地完成了任务,FAILED代笔你没有合格地完成任务,AWESOME则代表你很“恐怖”地完成了任务。其中有两个判定点,一个是你 可以从起始点回到起始点,而不是除了起始点以外的另外一个点,这当然是其一,其二,代表你的速度足够地快,按照相关的算法可以计算出一个你所得到的返回分 数。
对于关卡设计者来说,我们判断一个关卡是否是合理的,则要看它是否可以让玩家完成任务(同时还要一定程度地考虑一个关卡的难易程度)
首先,我们还是来解决一个最基本的问题,就是判定,需要用到的数据结构有:并查集+欧拉回路。
并查集奥义
对不相交集合进行俩种操作:
1. 检索某元素属于哪个集合
2. 合并两个集合
我们最常用的数据结构是并查集的森林实现,也就是说,在森林中,每棵树代表一个集合,用树根来标识一个集合,树的形态不重要,重要的是每棵树有哪些元素。
查找操作
查找一个元素v很简单,只需要顺着叶子到根节点的路径找到u所在的根节点,然后把v到u的路径上面的结点的父节点都设置为根节点,这样减少了查找的次数(路径压缩)。
合并操作
为了把两个集合s1和s2并起来,只需要把s1的根的父亲设置为s2的根节点,我们可以做一个优化,将深度小的合并成为深度大的子树,这样子查找的次数少些。
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过于严格,更好的可以在之后进行改进。
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中的运行时间,这就是一个不错的判定,毕竟,计算机只是比人要做的更快一些罢了。
进一步地
我们对关卡的设计给出更高的要求,比如,我们希望加大难度,但是,我们以一种“另外的方式”进行难度的加大,我们不再苛求要一笔解决问题,我们用两笔,或 者N笔解决问题。那么,游戏的难度确实加大了,加大的同时,我们甚至不认为它加大了,因为,我们有几次的重复,这样,使得游戏也变得更有意思了。
这个AI,我们仍然按照原来的方式输入一个地图,只是返回的值不再是0和1,我们返回一个最小的笔画。
公式:
(a)如果是个欧拉回路一笔就可以完成。
(b)笔划数=奇度数/2。
(c)总之笔划数 = 奇度数%2 + 欧拉回路数。
这里,对于每个连通集,用STL中的向量容器来装填。
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, 0, sizeof(used));
19 //顶点度数的统计
20 memset(deg, 0, sizeof(deg));
21 //统计各个欧拉回路的祖先结点为奇数点的情况
22 memset(odd, 0, sizeof(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] == 0) continue;
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 }