题解:P17092 [ICPC 2017 Qingdao R] Battle in Two Pairs of Heroes
lailai0916 · · 题解
题意简述
Alice 与 Bob 各有两名英雄。每回合,行动方的每名存活英雄都选择一名存活敌人攻击。双方均采用最优策略。分别判断 Alice 先手和 Bob 先手时的胜者,再按题意输出三种结论。
解题思路
用五元组
定义
每次攻击至少让一名存活英雄的生命值严格下降。因此,四个生命值之和沿转移严格递减,搜索图没有环,可以直接记忆化。
生成行动时,先收集双方仍存活的英雄。行动方至多有两人,目标也至多有两人,所以攻击分配最多只有
若两种行动得到的敌方生命值分别为
则第一种行动不会更差。它让两名敌人的生命值都不更多,之后可以沿用第二种行动对应的策略。因此,只保留按坐标偏序极小的后继。
单纯记忆化仍可能接近四维生命值空间的边界。还需要利用胜负的单调性。把原生命值改写为对 Alice 越大越有利的坐标:
增加任意一个
Alice 行动时,较有利局面至少保留原局面中全部存活的 Alice 英雄,并且 Bob 的生命值不更高。Alice 可以选择与原策略相同的攻击目标,得到一个仍不弱于原后继的局面。
Bob 行动时,从较有利局面出发,Bob 的每种攻击都能在原局面中找到相同的攻击分配。原局面的 Alice 生命值不更高,Bob 生命值不更低,所以原后继不会比新后继更有利于 Alice。由归纳可得胜负关于每个
固定行动方和另外三个坐标,只观察
win:已经完整求出的必胜状态中,最小的z_i ;lose:已经完整求出的必败状态中,最大的z_i+1 。
查询新状态时,只要某个方向满足
通过边界推导出的状态不反过来更新边界。只有真正完成极小极大搜索的状态才会写入边界和精确记忆化表,这样每条边界都始终有一个已求值状态作为依据。
四个生命值都小于
最后分别求初始状态在
设真正展开的状态数为
正确性证明
每次转移都会严格降低生命值总和,所以递归一定到达某方全灭的终止状态。Alice 回合对后继取或,准确表达她可以主动选择一种获胜行动;Bob 回合对后继取与,准确表达 Alice 必须抵抗 Bob 的所有选择。因此,不考虑剪枝时,记忆化搜索正是有限完全信息游戏的极小极大值。
若一个后继的两名敌方英雄生命值均不高于另一个后继,当前行动方可以把前者视作后者的强化结果。更早死亡的敌方英雄只会失去攻击能力,不会获得新行动。因此删除被支配后继不会改变当前状态的胜负。
变换后的四个坐标都与 Alice 的优势同向。增加 Alice 生命值不会减少她的可用行动,减少 Bob 生命值不会增强 Bob 的行动。对剩余回合数归纳,若状态
win 只由已求出的必胜状态更新,lose 只由已求出的必败状态更新。单调性保证边界外侧的推断全部正确,而边界之间仍交给完整搜索。因此,剪枝不会产生错误判定。
程序分别精确得到两种先手情况下 Alice 是否必胜,并按这两个布尔值分类输出,所以最终结论正确。
参考代码
#include <bits/stdc++.h>
using namespace std;
using uc=unsigned char;
using ui=unsigned int;
using hp=array<int,4>;
const int M=1000005;
int d[2][2][2];
uc win[4][2][M],lose[4][2][M];
unordered_map<ui,uc> mem;
int line(const hp &z,int p)
{
int res=0;
for(int i=0;i<4;i++)if(i!=p)res=res*100+z[i];
return res;
}
hp change(const hp &h)
{
return {h[0],h[1],99-h[2],99-h[3]};
}
int ask(const hp &h,int t)
{
hp z=change(h);
for(int i=0;i<4;i++)
{
int s=line(z,i);
if(z[i]>=win[i][t][s])return 1;
if(z[i]<lose[i][t][s])return 0;
}
return -1;
}
void upd(const hp &h,int t,bool w)
{
hp z=change(h);
for(int i=0;i<4;i++)
{
int s=line(z,i);
if(w)win[i][t][s]=min(win[i][t][s],uc(z[i]));
else lose[i][t][s]=max(lose[i][t][s],uc(z[i]+1));
}
}
vector<hp> next(const hp &h,int t)
{
int from=t?2:0,to=t?0:2;
int a[2],b[2],ac=0,bc=0;
for(int i=0;i<2;i++)
{
if(h[from+i])a[ac++]=i;
if(h[to+i])b[bc++]=i;
}
int ways=1;
for(int i=0;i<ac;i++)ways*=bc;
vector<hp> q;
for(int i=0;i<ways;i++)
{
hp z=h;
int x=i;
for(int j=0;j<ac;j++)
{
int k=b[x%bc];
x/=bc;
z[to+k]=max(0,z[to+k]-d[t][a[j]][k]);
}
q.push_back(z);
}
sort(q.begin(),q.end());
q.erase(unique(q.begin(),q.end()),q.end());
vector<hp> res;
for(int i=0;i<q.size();i++)
{
bool bad=0;
for(int j=0;j<q.size();j++)
{
if(i==j)continue;
if(q[j][to]<=q[i][to]&&q[j][to+1]<=q[i][to+1])bad=1;
}
if(!bad)res.push_back(q[i]);
}
sort(res.begin(),res.end(),[to](const hp &x,const hp &y)
{
return x[to]+x[to+1]<y[to]+y[to+1];
});
return res;
}
ui encode(const hp &h,int t)
{
ui res=0;
for(int i=0;i<4;i++)res=res*100+h[i];
return res*2+t;
}
bool dfs(const hp &h,int t)
{
if(!h[2]&&!h[3])return 1;
if(!h[0]&&!h[1])return 0;
ui s=encode(h,t);
auto it=mem.find(s);
if(it!=mem.end())return it->second-1;
int k=ask(h,t);
if(k!=-1)return k;
bool ans=t;
for(auto z:next(h,t))
{
bool w=dfs(z,t^1);
if(w!=t)
{
ans=w;
break;
}
}
mem[s]=ans+1;
upd(h,t,ans);
return ans;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin>>T;
while(T--)
{
hp h;
for(int i=0;i<4;i++)cin>>h[i];
for(int i=0;i<2;i++)
{
for(int j=0;j<2;j++)cin>>d[0][i][j];
}
for(int i=0;i<2;i++)
{
for(int j=0;j<2;j++)cin>>d[1][i][j];
}
mem.clear();
mem.reserve(1<<18);
mem.max_load_factor(0.7);
memset(win,100,sizeof win);
memset(lose,0,sizeof lose);
bool x=dfs(h,0),y=dfs(h,1);
if(x&&y)cout<<"Alice wins"<<'\n';
else if(!x&&!y)cout<<"Bob wins"<<'\n';
else cout<<"It depends"<<'\n';
}
return 0;
}