您的位置:首页 > 其它

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

2013-04-23 23:17 375 查看


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元素所在的集合,回溯时压缩路径*/

int Find_Set(int x)
{

if (x != father[x])

{

father[x] = Find_Set(father[x]); //这个回溯时的压缩路径是精华
}
return father[x];
}

//将秩小的合并到秩大的集合中
void Union(int x, int y)
{
x = Find_Set(x);
y = Find_Set(y);
if (x == y) return;
if (rank[x] > rank[y])
{
father[y] = x;
}
else
{
if (rank[x] == rank[y])
{
rank[y]++;
}
father[x] = y;
}
}


判断关卡是否合格的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];
int size[1001];

int a,b,n,m;

//找到那个结点的祖先(查找函数)
int find(int x)
{
//表示已经找到祖先结点了
if(node[x]==x) return x;
//每次递归,直到找到祖先结点
else return node[x]=find(node[x]);
}

//合并函数
void combine(int a,int b)
{
int ka=find(a);
int kb=find(b);
//如果不是一个祖先的话,将轶小的合并到轶大的集合中
if(ka!=kb)
{
if(size[ka]>size[kb])
{
node[kb]=ka;
size[ka]+=size[kb];
}
else
{
node[ka]=kb;
size[kb]+=size[ka];
}
}
}

int main()
{
//遇到0就退出
while(cin>>n&&n)
{
cin>>m;
//将点和地图清零
memset(degree,0,sizeof(degree));
memset(map,0,sizeof(map));
//建立每个点
for(int i=1;i<=n;i++)
{
//最开始的祖先结点是自己
node[i]=i;
size[i]=1;
}
while(m--)
{
cin>>a>>b;
//每次对两点加入边
if(map[a]==0)
{
degree[a]++;
degree[b]++;
//标记一条边
map[a][b]=map[b][a]=1;
combine(a,b);
}
}
//找到祖先结点(任意一个点的)
int k=find(a);
bool flag=true;
for(int i=1;i<=n;i++)
{
//判定欧拉回路可以实现的充要条件为:(1)连通(2)每个结点度都是偶数(3)每个结点都有共同的祖先结点
if(degree[i]&1||degree[i]==0||find(i)!=k)
{
flag=false;
break;
}
}
if(flag) cout<<"1"<<endl;
else cout<<"0"<<endl;
}
return 0;
}



那么,有了这个AI,我们就可以基本地判定出这个游戏是否是关卡合格的了。注意到这个if条件句,过于严格: if(degree[i]&1||degree[i]==0||find(i)!=k), 这里,可以将if条件句中的第一个条件弱化成“允许两个点的度为奇数”,这样的话,仍然是可以满足关卡的要求的,只是如果这样的话,就不能让起始点和终结 点都是相同的点了,这两个点必须不同。那么,我们可以先用上面一个AI判定出一些“具有AWESOME答案”的关卡,然后,再用改进的AI例程给出一些 “最终只能到达GOOD WORK”这个级别的答案。 我们如何判定一个关卡的难易程度呢?一般情况下,当然,边数和点数越多,关卡应该更难一些,但是,这也不一定。我觉得,参数应该更多一些,比如,挂卡输入到AI中的运行时间,这就是一个不错的判定,毕竟,计算机只是比人要做的更快一些罢了。

[b] 进一步地


我们对关卡的设计给出更高的要求,比如,我们希望加大难度,但是,我们以一种“另外的方式”进行难度的加大,我们不再苛求要一笔解决问题,我们用两笔,或 者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;

int father[maxn],m,n,used[maxn],odd[maxn],deg[maxn];

//初始化函数
void init()
{
int i;
//表明
memset(used, 0, sizeof(used));
//顶点度数的统计
memset(deg, 0, sizeof(deg));
//统计各个欧拉回路的祖先结点为奇数点的情况
memset(odd, 0, sizeof(odd));
for (i = 1; i <= n; i++)
father[i] = i;
}

//查找函数
int find (int x)
{
if(father[x] != x)
father[x] = find(father[x]);
return father[x];
}

//建立并查集,归并祖先结点
void make (int a,int b)
{
int x = find(a);
int y = find(b);
if (x != y) father[y] = x;
}

int main()
{
int a,b,k;
//利用向量容器装填不同的欧拉回路
vector <int> t;
//每次读入一个样例
while (scanf("%d%d",&n,&m)!=EOF)
{
init();
//清空向量容器
t.clear();
int count = 0;
//读入所有的边
while (m--)
{
scanf("%d%d",&a,&b);
//两个点的度数自增
deg[a]++;
deg[b]++;
//归并这两个点
make (a,b);
}
for (int i=1; i<=n; i++)
{
//找到一个祖先结点
k = find(i);
//如果那个祖先结点还没有使用过
if (!used[k])
{
//进入容器
t.push_back(k);
//标记为已经使用了
used[k] = 1;
}
//如果存在结点是奇点,则odd[k]++
if (deg[i] & 1)
odd[k]++;
}
int sum = 0;
//对每个欧拉回路进行遍历
for (int i=0; i<t.size(); i++)
{
k = t[i];
//如果该集合在孤立点的话,则继续,因为,只需要遍历所有的边就可以了
if(deg[k] == 0) continue;
//如果该集合是欧拉回路,则有一条路
else if(odd[k] == 0) sum++;
//否则,加上odd[k] /2
else sum += odd[k]/2;
}
printf("%d\n",sum);

}
return 0;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐