一、问题概况
本文所讨论的问题:通过编写一个程序,求出大小不超过N*M的沙盘,消除步数不超过steps步,补充随机(视为等概率随机)的情况下,怪物总战斗力尽可能高的一个解。
数据范围限制:
魔王,
,
,沙盘中无石块,怪物有升级概念
战棋,
沙盘中无石块,补充分数可能有上限
矿战:
, 沙盘中无石块
秘境:
, 怪物有升级概念,需要考虑子状态的优劣
竞技场:
,有石块,需要考虑子状态的优劣。
正常通关: , 有石块,怪物有升级概念
篇幅及精力所限,本文只讨论与魔王有关的部分。 其实其他部分也可以以此类推,石块主要影响的是沙盘掉落对算法影响不大。
二、问题分析
- 由于补充是随机,直接模拟全过程完全不可行。目前我能想到的方法都离不开下面这个思路:
1)读入状态后枚举所有操作。 总共的可能性为
种。沙盘中产生绿色怪的规则我们已知了,不了解的可以去翻林木甜的帖子。
2)通过构造一个函数来评判它,选出最优的
3)输出这部操作,让玩家执行,并继续等待下一步操作。
4)玩家把执行了这部操作后的沙盘情况再输入,重复循环。
因此,最核心的一点便是,如何“构造一个函数”。
-
虽然这里不是做题,但是玩个游戏谁也不想那么累。在 的情况下, 如果一步操作需要跑10秒, 那么执行完整个消除就要25分钟,这样的程序没人会去用。 因此我觉得单步时间需要压在3秒以内。
-
由于补充是随机,所以能够求出来的最优的解,只能做到期望下收益最大或者在最多的情况下收益最大。这就是问题的有趣之处,一方面很难很难设计出最好的方法,另一方面如果设计的方法不好,也可能运气原因收到好的结果。而如果是要求在最多的情况下收益最大的话,势必要靠纯粹的枚举消除,这样的复杂度会接近 (v为一次操作内补充怪物数量) 显然不可行。因此我暂时是往期望下收益最大的方向考虑的,这样才可能通过数学知识加速。
-
也是一个偏向实用的考虑: 需要手动输入沙盘是很头疼的事。如果输入两个位置要1秒,一个魔王沙盘就是12.5秒,那么消除一次光是耗在输入上的时间就是20分钟,还很容易出错。因此识图等辅助必不可少(但我并不擅长QAQ)。不过程序可以把操作后一次掉落消除的沙盘输出,这样如果没有大连消(堆出紫橙以后多数是这样)的话可以复制过来然后改一些数字。
三、算法设计部分
由上,我现在就来讲讲除了纯粹的枚举消除以外我曾经想到过的几种算法。
算法甲:10个空位全掉一种怪全消除的情况你们见过吗? 我虽然玩召合游戏时长还没过千小时,但也是断断续续玩了一年多,我表示光是一步操作5个空位掉一种怪的情况我都没见过。这便启示我们可以在随机生成掉落怪物的时候做的“平均”一些。
比如,像消除王一样12怪循环,然后只保留每种怪物数量在3-6的结果。(也就是说,只考虑三种怪的比例为336,345,444的情况)
这样,情况数就从
降至
种。
但这本质上相对暴力做法还只是一个常数优化。即使只考虑444,情况数也是
级别的。因此这种想法我一开始写了,后来则当做了一个辅助处理空格较少时的情况。但事实上可能还是不行,因为第一步的少数补充怪有可能引起大消除需要更多的补充怪…
算法乙: 沙盘消除最活跃的元素就是由掉落怪参与的消除。因此考虑把沙盘适当简化成下面这样的状态,只有空位、三种白怪和暂时不会消除的其他怪共5种构成,在此之前先特殊处理一下高级消除使得沙盘内部没有消除块需要依靠补充怪即可。这样状态数就一下下降了。更何况全部 种中还有很多性质,空位肯定在上,还不可以自身产生消除,无数种状态又是不可能出现的。因此考虑预处理一下每种状态,分别求出出现时得最优方案,然后运行时等待一会便可以 完成一次处理。(非常有前途)
可是我还是低估了这样的状态数目。笔者曾经尝试构造出全部这样的状态,为了保证它们都是可以通过交换生成的,先构造所有全白怪的状态再消除。但是光是 沙盘的全白怪(不可消除)状态就超过 种,因此依然不可行。因此我最后没有写下去。不过这种方法还是有潜力的,毕竟全白怪的情况也只有第一步有,前三步之后场面上的白怪数量就会少很多,那时的状态数有变得可以预处理得可能。有待进一步研究
算法丙:这就是我现在写出的等效方法。就是想办法构造一个函数的做法。
1.先定义以下常量:
: 三种家族的权值。因为他们有强有弱,所以不妨先给一个权值。其意义自不必言。不过对于大多数玩家,一般都按照1:1:1来考虑也可。
: 一个战场上的怪与一个沙盘内的怪的价值比。我目前采用的是一个统一值
,但最近的测试让我发现也许有必要根据5种家族形态设置不同的值。其意义在于能够在合适的时候拖出怪,避免无脑拖蓝紫以及憋到没地方了还不拖。
步数和怪物是问题当中分离的“两种资源”(姑且这么叫吧),步数很难直接转化成怪物战力,但可以根据情况进行等价。这种关系有点像三国杀里血和手牌的关系,直接等价很不合理,但是许多时候都是1血=2牌计收益
: 相差一形态等级的两种怪的战力比。意义同样自不必言。同上,我目前采用的是同一个值
,但也许很有必要分开
这样,家族1的白怪价值便是
,以后亦然。
:用于结算“沙盘活力值”,意义下面讲。
这些常量都需要在不断的实验中进行修改从而趋向合理,目前都是我瞎凑的…
2. 我们按照如下方式计算权值,其暂时由三部分组成:
:直接掉入沙盘的收益(包括直接收益和期望直接收益)
:存留在沙盘中的收益
:沙盘健康度
最终沙盘价值:
1)进行操作后,先处理掉落,已经消除掉的怪物计入
部分 保留沙盘当中的空格。
2)计算出,之后的一次掉落当中,获得的直接收益与沙盘的期望收益(主要贡献为绿怪)。直接收益部分计入
。
3)继续消除,处理可能存在的非白怪消除,同样将收益计入
.
4)将沙盘中的怪放入一个数列,在其中再补入若干个绿怪,数量为2)步中计算出的期望绿怪数向下取整。取沙盘当中权值前
大的怪,将得分计入
.
5)对当前沙盘处理一番,为了考虑到沙盘的健康度,计入ptt。
6)得出最后的价值。
下面我解释一下2)和5)我的做法。
2):这是优化复杂度的关键。读懂需要一定的计算机科学知识
根据期望的线性性,我们可以单独找到并剥离出沙盘当中每一条可能存在的,仅由空格和3种白怪之一构成的消除链(行、列、主副对角线)。单独通过递推计算出他们收益期望。
定义: E(s) (s为一个消除链,例如X000X)为“某条链达成包括第一个块在内的消除”的消除数量期望。暂时,我们令一行3个的消除贡献为3.
P(s) 为“某条链达成包括第一个块在内的消除,即产生绿怪”的概率。
显然,P(s)为(
(q1为这一行前3个格子中空格的数目)。然后减去上一格格子产生包括这里的消除链的消除链即可
E可以这么计算:
1)先令E = 3,辅助变量p0.
2)第4格以后每个位置贡献为(
(q2为这一行从第4格到这个位置的所有位置中空格的数目),求和后加入E
3)将E乘P(s)即为答案。
现在假设我们发现了X000X这条链,其中X为怪物,0为空格。
则P(X000X)=
因为前三个当中有两个空格。
然后前三格的E = 3,第四格、第五格各贡献
,和为
, 乘P后得到
,则E(X000X)即为
. 可以自己枚举一下,看是否正确。(为什么?自己想吧)
程序中,采用递推实现。
其他情况同理。之所以如此定义E(s)是为了避免重复。
在计算完所有格子的贡献以后:
某一个格子的四个方向的链,可以通过容斥原理得到这一格产生消除块的贡献。
假设某一个格子的四个方向得到的E’与P‘分别为E1到E4,P’为P1到P4.
则:
总消除数量
为
但又由于我们熟知的补充规则,如果四个方向都构成消除(非常非常罕见),必定会补充2个绿。因此要多减一个
.
总产生绿怪
为
同理,需要多加一个
总生成怪物期望数量为
.
空格数量
为
所有格相加即可得到答案。
(严格来说,其实容斥原理时的格子的P应该是"生成包括它的消除链的期望”而并不一定由它为第一格。由此可以得到类似的递推式。不过我发现问题以后,懒了 没来得及修改…发完贴就改,还是明显会影响答案)
5)沙盘活力指数
这是一个我认为有必要考虑进去但完全没头绪如何合理地考虑地变量。目前它的贡献是这么计算的:
设沙盘内现在期望空格数为
, 共有
个格子通过同色格子与空格相连。则沙盘活力为
这么定义完全是自己空想出来的,只是觉得活力指数和“可能继续消除的块”的平方数正相关。
是多少我也没想法,目前取了一个很小的
,得留到以后的实验了。(也许一样可以算所有情况搞个平均?)
3.计算完权值以后,找到权值最大的沙盘输出即可。
1)程序依然在调试与测试… 欢迎有意向者前来陪同测试,提出新的想法,修复一些bug,确定常数值,寻找优化用户体验的方法。
2)会想到写这样一个程序,也是纯粹觉得这比简单的扫荡刷副本养成更有意义而已,也算是对自己的挑战了。虽然平时上学很忙,但我会尽量抽时间,如果能够坚持到最后的应用那当然是再好不过了。
附上半成品代码:
#include <bits/stdc++.h>
#define FI first
#define SE second
#define LL long long
#define PB push_back
#define MP make_pair
using namespace std;
const int MAXN = 7;
const int dx[8] = {1,-1,1,-1,0,0,1,-1};
const int dy[8] = {1,-1,0,0,1,-1,-1,1};
const int DX[4] = {-1,-1,-1,0};
const int DY[4] = {0,-1,1,-1};
const double EVOLVE = 3.25;//低阶怪与高阶怪的战力比
const double IN_OUT = 0.982;//沙盘外怪和沙盘内怪的战力比
const double K = 0.0004;
const double eps = 1e-8;
const int BRUTE = 1;//空格数≤BRUTE时我们暴力,大于时使用估值法
int W[4];
double SCORE[37];
int remstep,n,m,X1,Y1,X2,Y2;
bool cs;
int Num[510000];
LL mi3[27];
bool avl(int p,int q){return (p>=0)&&(q>=0)&&(p<n)&&(q<m);}
inline short C(int p,int q)
{
return p * n + q;
}
struct field
{
short a[MAXN][MAXN],space,xx1,yy1,xx2,yy2;
double monster,sandbox,ptt;
LL Hash;
inline void clear()
{
memset(a,0,sizeof(a)); monster = sandbox = 0.0;
}
inline void cspace()
{
int cnt = 0;
for (int j = 0; j < m; ++j)
for (int i = 0; i < n; ++i,cnt++) if (a[i][j]) break;
space = cnt;
}
inline double cal()
{
}
bool good(int i,int j){return a[i][j]>0; }
bool operator < (const field &b) const
{
return monster + sandbox * IN_OUT + ptt < b.monster + b.sandbox*IN_OUT + b.ptt;
}
bool operator == (const field &b) const
{
bool pd = 1;
for (int i = 0; i < n; ++i)
for (int j = 0; j < m; ++j)
if (a[i][j] != b.a[i][j]) pd = 0;
return pd;
}
inline void print()
{
printf("%.5f %.5f %.5f\n",monster,sandbox,ptt);
printf("(%d,%d),(%d,%d)\n",xx1,yy1,xx2,yy2);
for (int i = 0; i < n; ++i,putchar(10))
for (int j = 0; j < m; ++j)
printf("%5d ",a[i][j]);putchar(10);
}
}ori,Ansnow;
vector<field> data,gen;
vector<double>Exp[8],Pos[8];
priority_queue<field> q;
set<LL>gens;
LL Mi3;
inline void init()//预处理E和P
{
Mi3 = 1LL;
for (int i = 1; i <= 12; ++i) Mi3 *= 3LL;
for (int h = 3; h <= 7; ++h)
{
Exp[h].resize((1<<h)+5); Pos[h].resize((1<<h)+5);
//Ec = 这一块贡献的总长度期望, Pc为构成子消除块的概率
for (int i = 0; i < (1<<h); ++i)
{
double Pc = 1.0,Ec = 3.0,pc = 1.0;
for (int j = 0; j < 3; ++j) Pc /= ((1<<j)&i)?1:3;
for (int j = 3; j < h; ++j)
{
pc /= ((1<<j)&i)?1:3;Ec += pc;
}
Ec *= Pc;
Exp[h][i] = Ec, Pos[h][i] = Pc;
// if (h <= 5) printf("%d %d %.5f %.5f\n",h,i,Ec,Pc);
}
}
}
inline void falldown(field &o)//处理沙盘掉落的子程序,顺带处理了以前的石块
{
int b[MAXN],cnt = 0;
for (int j = 0; j < m; ++j)
{
cnt = 0;
for (int i = n - 1; i >= 0; --i) if (o.a[i][j] > 0) b[++cnt] = o.a[i][j];
int tmp = n - 1;
for (int i = 1; i <= cnt; ++i)
{
while (o.a[tmp][j] < 0&& tmp >= 0) tmp--;
o.a[tmp][j] = b[i]; tmp--;
}
for (int i = tmp;i >= 0; --i) o.a[i][j] = min(o.a[i][j],(short)0);
}
}
inline bool clear(field &u)//刚拿到手的沙盘只进行一次消除,下面可能有高级消除之类的
{
//field v = u;
int match[MAXN][MAXN] = {0};
int keep[MAXN][MAXN] = {0};
int num[30] = {0};
int cnt = 0;
for (int i = 0; i < n; ++i)
for (int j = 0; j < m; ++j)
{
if (u.a[i][j]==0) continue;
for (int k = 0; k < 4; ++k)
if (avl(i,j)&&avl(i+DX[k],j+DY[k]))
{
if (u.a[i][j] == u.a[i+DX[k]][j+DY[k]])
{
int k1 = 0;
while (avl(i+DX[k]*k1,j+DY[k]*k1))
{if (u.a[i][j] == u.a[i+DX[k]*k1][j+DY[k]*k1])k1++; else break; }
if (k1 > 2)
{
int mt = match[i][j];
for (int k2 = k1-1; k2 > 0; --k2)
{
if (match[i+DX[k]*k2][j+DY[k]*k2]) mt = match[i+DX[k]*k2][j+DY[k]*k2];
}
if (!mt)
{
mt = ++cnt; keep[i+DX[k]*(k1-2)][j+DY[k]*(k1-2)] = cnt;
}
for (int k2= 0; k2 < k1; ++k2) match[i+DX[k]*k2][j+DY[k]*k2] = mt;
}
}
}
}
for (int i = 0; i < n; ++i)
for (int j = 0; j < m; ++j) num[match[i][j]]++;
bool pd = 0;
if (cs)
{
for (int i = 0; i < n; ++i)
for (int j = 0; j < m; ++j)
{
if (match[i][j])
{
pd = 1;
if ((i==X1&&j==Y1)||(i==X2&&j==Y2)) u.monster -= SCORE[u.a[i][j]]*1.0,u.a[i][j]++; else u.monster+=SCORE[u.a[i][j]],u.a[i][j] = 0;
}
}
cs = 0;
}
else
{
for (int i = 0; i < n; ++i)
for (int j = 0; j < m; ++j)
if (match[i][j])
{
pd = 1;
if (keep[i][j]) u.monster -= SCORE[u.a[i][j]]*1.0,u.a[i][j]++; else u.monster+=SCORE[u.a[i][j]],u.a[i][j] = 0;
}
}
falldown(u);u.cspace(); //if (X1 ==0&&Y1==2&&X2==2&&Y2==1)u.print();
return pd;
// if (pd) clear(u);
}
inline void brute_force(field &u)
{
int wz[14],js[4]={0};
double expect = 0.0;int all = 0;
for (int i = 0; i < 6561*3; ++i)
{
field u1 = u;
int c = i; bool pd = 1;
for (int j = 0; j < 9; ++j)wz[j] = c%3, c/=3,js[wz[j]]++;
for (int j = 1; j <= 3; ++j) if (wz[j] < 1||wz[j] > 5) pd = 0;
int cnt = 0;
if (pd)
{
all++;
while (1)
{
for (int i1 = 0; i1 < n; ++i1)
for (int j1 = 0; j1 < m; ++j1)
{
if (!u1.a[i1][j1])
{
u1.a[i1][j1] = (wz[cnt]+1)*10+1;
cnt++;if (cnt==9) cnt= 0;
}
}
if (!clear(u1)) break;
}
vector<int> v;
for (int i1 = 0; i1 < n; ++i1)
for (int j1 = 0; j1 < m; ++j1)
{
v.PB(-SCORE[u.a[i1][j1]]);
}
double tot = 0.0;
for (int i1 = 0; i1 < min(n*m,remstep); ++i1) tot -= v[i1];
expect += u1.monster + tot * IN_OUT;
}
}
u.monster += expect / (all);
if (Ansnow < u) Ansnow = u;
}
inline void calc(field &u)
{
//为了迎合之前做消除块的设置我们决定从下往上跑
double recE[MAXN][MAXN][4][4],recP[MAXN][MAXN][4][4];
double moExpect = 0.0,spExpect = 0.0,lvExpect[4];
memset(lvExpect,0,sizeof(lvExpect)); memset(recE,0,sizeof(recE));memset(recP,0,sizeof(recP));
for (int i = n - 1; i >= 0; --i)
for (int j = m - 1; j >= 0; --j)
{
if (u.a[i][j]%10>1) continue;
for (int fea = 1; fea <= 3; ++fea)
{
double p[4],e[4],E0 = 0, P0 = 0;
//E0 = 消除数量期望(单行计3, 一个L计5)
//P0 = 产生绿怪数量期望(还在沙盘内部)
//p = 产生消除块数量期望
for (int dir = 0; dir < 4; ++dir)
{
int signal = 0,cnt = 0;
while (avl(i+cnt*DX[dir],j+cnt*DY[dir]))
{
if (u.a[i+cnt*DX[dir]][j+cnt*DY[dir]]==0) cnt++;
else if (u.a[i+cnt*DX[dir]][j+cnt*DY[dir]] == fea*10+1) signal += (1<<cnt),cnt++;
else break;
}//cerr<<'!'<<endl;
if (cnt <=3) break;
recP[i][j][fea][dir] = p[dir] = Pos[cnt][signal]; recE[i][j][fea][dir] = e[dir] = Exp[cnt][signal];
cnt = 1;
for (;avl(i-cnt*DX[dir],j-cnt*DY[dir]);cnt++)
recP[i][j][fea][dir] *= (1-recP[i-cnt*DX[dir]][j-cnt*DY[dir]][fea][dir]),
recE[i][j][fea][dir] *= (1-recP[i-cnt*DX[dir]][j-cnt*DY[dir]][fea][dir]);
p[dir] = recP[i][j][fea][dir];
e[dir] = recE[i][j][fea][dir];
// cerr<<cnt<<signal<<endl;
}
E0 = (e[0]+e[1]+e[2]+e[3]) - (p[0]*p[1]+p[0]*p[2]+p[0]*p[3]+p[1]*p[2]+p[2]*p[3]+p[1]*p[3]) + (p[0]*p[1]*p[2]+p[0]*p[1]*p[3]+p[0]*p[2]*p[3]+p[1]*p[2]*p[3]);
- 2*(p[0]*p[1]*p[2]*p[3]);//如果四个方向真的都能消除,那么一定是会出现双绿的
P0 = (p[0]+p[1]+p[2]+p[3]) - (p[0]*p[1]+p[0]*p[2]+p[0]*p[3]+p[1]*p[2]+p[2]*p[3]+p[1]*p[3]) + (p[0]*p[1]*p[2]+p[0]*p[1]*p[3]+p[0]*p[2]*p[3]+p[1]*p[2]*p[3]);
;
moExpect += (E0-2*P0) * W[fea];//期望消除量
spExpect += E0 - P0;//期望消除量-期望绿怪量
lvExpect[fea] += (P0);
if (e[0]+e[1]+e[2]+e[3]>0.000001)
{
// cerr<<i<<' '<<j<<endl;
// for (int i11 = 0; i11 < 4; ++i11) printf("%.5f %.5f\n",e[i11],p[i11]);
// printf("%.5f %.5f %.5f\n",E0,P0,moExpect);//system("pause");
}
}
}
int ct = 0;
while(clear(u));//先结算完,再把可能有的绿蓝消除给处理了。
//
// if (X1==2&&Y1==2&&X2==2&&Y2==4) u.print();
// if (X1==1&&Y1==2&&X2==2&&Y2==2) u.print();
vector<double> v;
bool Vi[MAXN][MAXN] = {0};
for (int i = 0; i < n; ++i)
for (int j = 0; j < m; ++j)
{
v.PB(min(-SCORE[u.a[i][j]],-SCORE[21]));
if (u.a[i][j]%10==1)
for (int k = 0; k < 8; ++k)
{
if (!u.a[i+dx[k]][j+dy[k]] || (Vi[i+dx[k]][j+dy[k]] && u.a[i][j] == u.a[i+dx[k]][j+dy[k]]))ct++,Vi[i][j] = 1;
}
}
for (int h = 1; h <= 3; ++h)
for (int i = 1; i*1.0 <= lvExpect[h]; i++) v.PB(-SCORE[h*10+2]);
sort(v.begin(),v.end());
u.monster += moExpect;
for (int i = 0; i < min(n*m,remstep); ++i) u.sandbox += -v[i];
u.ptt += 1.0 * ct * spExpect * K;
// if (X1==2&&Y1==2&&X2==2&&Y2==4) u.print();
}
inline void debug()
{
clear(ori);
ori.print();
}
int main()
{
init();
scanf("%d%d%d",&remstep, &n, &m);
scanf("%d%d%d",&W[1],&W[2],&W[3]);
SCORE[11] = W[1]*1.0; SCORE[21] = W[2]*1.0; SCORE[31] = W[3]*1.0;
for (int i = 2; i <= 36; ++i)
if (i%5!=1) SCORE[i] = SCORE[i-1]*EVOLVE;
while(remstep--)
{
for (int i = 0; i < n; ++i)
for (int j = 0; j < m; ++j) scanf("%d",&ori.a[i][j]);
for (int i1 = 0; i1 < n; ++i1)
for (int j1 = 0; j1 < m; ++j1)
for (int i2 = i1; i2 < n; ++i2)
for (int j2 = 0; j2 < m; ++j2)
{
if (C(i1,j1) >= C(i2,j2)) continue;
field now = ori;
if (now.a[i1][j1]==now.a[i2][j2]) continue;
now.xx1 = i1, now.yy1 = j1, now.xx2 = i2, now.yy2 = j2;
swap(now.a[i1][j1],now.a[i2][j2]);cs = 1;X1 = i1,Y1 = j1, X2 = i2, Y2 = j2;
if (!clear(now)) continue;
if (now.space < BRUTE) brute_force(now); else
calc(now);
if (Ansnow < now) Ansnow = now;
// system("pause");
// now.print();
}
/* for (int i1 = 0; i1 < n; ++i1)
for (int j1 = 0; j1 < m; ++j1)
{
field now = ori;int fail = 3;
now.sandbox += SCORE[now.a[i1][j1]]; now.a[i1][j1] = 0;
now.xx1 = now.xx2 = i1, now.yy1 = now.yy2 = j1;
falldown(now);
double score = 0.0;
for (int k = 1; k <= 3; ++k)
{
field Now = now;
Now.a[0][j1] = k*10+1;if (!clear(Now)) {score += SCORE[Now.a[i1][j1]]; continue;}
else if (Now.space < BRUTE) brute_force(Now); else calc(Now);
score += Now.monster + Now.ptt + Now.sandbox * IN_OUT;
}
if (Ansnow.monster + Ansnow.ptt + Ansnow.sandbox*IN_OUT < score / 3.0) Ansnow = now;
}
*/
Ansnow.print();
Ansnow.clear();
}
样例输入:
1 5 5
1 1 1
11 11 21 11 11
11 11 21 11 11
21 21 31 31 21
11 11 21 11 11
11 11 21 11 11
输出:
(1,2) (2,2)(从0开始标号,这代表交换第二行第三列与第三行第三列)
后面是一个进行过单次消除的沙盘
}
还有很多可能的更改…