题解:P17212 [ICPC 2017 Nanning R] Attacker-Defender Game

· · 题解

题意简述

进攻方与防守方每回合同时选择要展示的能量。较大的一方消耗所展示的能量,并让防守方生命相应减一或加一;相等时双方同时消耗能量。求双方采用最优混合策略时,进攻方的获胜概率。

解题思路

设 f(h,d,a) 表示防守方有 h 点生命和 d 点能量、进攻方有 a 点能量时,进攻方的最优获胜概率。终止状态为:

f(h,d,a)= \begin{cases} 1 & h=0 \\ 0 & a<h \\ 1 & d=0\land a\ge h \end{cases}

第二条成立,是因为进攻方每造成一次伤害至少消耗一点能量。第三条中,防守方只能展示 0,进攻方连续展示 1 即可获胜。

只考虑 h,d\ge1 且 a\ge h 的状态。进攻方选择 x\in[1,a],防守方选择 y\in[1,d]。固定本回合的选择后,进攻方的后续收益为:

G_{x,y}= \begin{cases} f(h-1,d,a-x) & x>y \\ f(h+1,d-y,a) & x<y \\ f(h,d-y,a-x) & x=y \end{cases}

无论属于哪种情况,后继状态的 a+d 都严格减小。因此,可以按照总能量 a+d 递增预处理,构造 G 时需要的状态都已经求出。

双方同时行动,不能把这一回合写成普通的先取最大再取最小。G 是一个有限零和矩阵博弈,双方都可能需要混合策略。直接对所有状态运行单纯形法仍然偏慢,所以先完整刻画答案等于 0 或 1 的状态。

令 r(h,d) 表示让 f(h,d,a)>0 所需的最小 a。边界为:

\begin{aligned} r(0,d) & =0 \\ r(h,0) & =h \end{aligned}

固定防守方的选择 y,只需判断收益矩阵第 y 列是否存在正元素。进攻方有三类有意义的回应:

三类回应要求的最少初始能量分别为:

\begin{aligned} y+1+r(h-1,d) \\ y+r(h,d-y) \\ r(h+1,d-y) \end{aligned}

若某一列全部为零,防守方固定选择该列即可让游戏值为零。若每一列都有正元素,进攻方给所有行分配正概率后,每一列的期望收益都严格为正,游戏值也严格为正。因此:

r(h,d)=\max_{1\le y\le d} \begin{cases} \min\bigl(y+1+r(h-1,d),y+r(h,d-y)\bigr) & y=1 \\ \min\bigl(y+1+r(h-1,d),y+r(h,d-y),r(h+1,d-y)\bigr) & y\ge2 \end{cases}

右侧只依赖更小的 d,或同一 d 下更小的 h,所以按 d、h 递增即可预处理。若 a<r(h,d),直接返回 0。

答案等于 1 的条件更简单:

a\ge h+d

先证充分性。进攻方始终选择 x=1。若 y=1,双方能量各减一,新状态仍满足同样的不等式;若 y>1,防守方生命加一、能量减少 y,新状态仍满足该不等式。对 d 归纳即可保证获胜。

再证必要性。设 a<h+d,进攻方选择任意 x。若 x\le d,防守方取 y=x,后继仍不满足必胜不等式。若 x>d,防守方取 y=d;此时 h\ge2,攻击后也有 a-x<(h-1)+d。按 a+d 归纳,这个回应的后继不是必胜态。因此收益矩阵不存在全为 1 的行,进攻方不能保证概率为 1。

剩下的状态满足:

r(h,d)\le a<h+d

对这些状态求矩阵博弈值。由于 G_{x,y}\in[0,1],令 B=G+1,使所有元素严格为正。考虑防守方对应的线性规划:

\begin{aligned} \max & \sum_{y=1}^{d}z_y \\ \text{s.t.} & \sum_{y=1}^{d}B_{x,y}z_y\le1 & 1\le x\le a \\ & z_y\ge0 & 1\le y\le d \end{aligned}

设最优目标值为 Z。把最优解除以 Z,便得到防守方的概率分布。每一行在矩阵 B 中的期望收益都不超过 1/Z。由线性规划强对偶,进攻方也能保证至少 1/Z,所以 B 的游戏值为 1/Z,原矩阵的游戏值为:

f(h,d,a)=\frac{1}{Z}-1

所有约束右端都是 1,原点与松弛变量直接构成初始可行基。代码建立包含原变量和全部松弛变量的完整单纯形表,用 Bland 规则选择入基变量,并在最小比值相等时按基变量编号破除平局,避免退化循环。

阈值预处理需要 O(AD^2)。其余状态的矩阵至多为 A\times D;单纯形法理论最坏为指数级,但本题 A,D<50,且两个精确阈值已经跳过绝大多数状态。DP 空间复杂度为 O(A^2D),单纯形表为 O((A+D)^2)。

正确性证明

状态边界与收益矩阵准确覆盖题目规定的三种比较结果。每个后继的总能量严格减小,所以递增预处理顺序合法。

当 $a\ge h+d$ 时,固定选择 $x=1$ 的策略在防守方所有回应后都保持归纳条件,故进攻方必胜。当 $a<h+d$ 时,防守方针对任意一行都能选出一个不属于必胜态的后继,所以矩阵博弈值不可能为一。两个阈值判断均无遗漏。 中间状态的第一回合正是矩阵 $G$ 的零和博弈。给所有收益加一只会让游戏值增加一。线性规划的可行解给出防守方的上界策略,强对偶给出相同的进攻方下界,因此 `simplex` 返回的 $1/Z-1$ 正是当前状态值。 程序对三个互斥且完备的区间分别返回 $0$、矩阵博弈值和 $1$,所以所有询问的答案正确。 ## 参考代码 ```cpp #include <bits/stdc++.h> using namespace std; using ld=long double; const int N=55; const int Q=1205; const int L=110; const ld eps=1e-15L; ld f[N][N][N],g[N][N],p[L][L]; int need[N][N],q[Q][3],bs[L]; void pivot(int r,int s,int m,int n) { ld inv=1/p[r][s]; for(int i=0;i<=n;i++)p[r][i]*=inv; for(int i=0;i<=m;i++) { if(i==r)continue; ld k=p[i][s]; if(fabsl(k)<eps) { p[i][s]=0; continue; } for(int j=0;j<=n;j++) { if(j!=s)p[i][j]-=k*p[r][j]; } p[i][s]=0; } bs[r]=s; } ld simplex(int m,int n) { int tot=m+n; for(int i=0;i<=m;i++) { for(int j=0;j<=tot;j++)p[i][j]=0; } for(int i=0;i<m;i++) { for(int j=0;j<n;j++)p[i][j]=g[i][j]+1; p[i][n+i]=1; p[i][tot]=1; bs[i]=n+i; } for(int i=0;i<n;i++)p[m][i]=-1; while(1) { int s=-1; for(int i=0;i<tot;i++) { if(p[m][i]<-eps) { s=i; break; } } if(s==-1)return p[m][tot]; int r=-1; for(int i=0;i<m;i++) { if(p[i][s]<=eps)continue; if(r==-1) { r=i; continue; } ld x=p[i][tot]/p[i][s],y=p[r][tot]/p[r][s]; if(x<y-eps||(fabsl(x-y)<=eps&&bs[i]<bs[r]))r=i; } pivot(r,s,m,tot); } } ld val(int h,int d,int a) { if(!h)return 1; if(a<h)return 0; if(!d)return 1; return f[h][d][a]; } ld solve(int h,int d,int a) { if(a<need[h][d])return 0; if(a>=h+d)return 1; for(int i=1;i<=a;i++) { for(int j=1;j<=d;j++) { if(i>j)g[i-1][j-1]=val(h-1,d,a-i); else if(i<j)g[i-1][j-1]=val(h+1,d-j,a); else g[i-1][j-1]=val(h,d-j,a-i); } } ld z=simplex(a,d); return max(0.0L,min(1/z-1,1.0L)); } void init_need(int md,int ma) { int inf=ma+1; for(int i=0;i<=ma+1;i++) { for(int j=0;j<=md;j++)need[i][j]=inf; } for(int i=0;i<=md;i++)need[0][i]=0; for(int i=1;i<=ma+1;i++)need[i][0]=min(i,inf); for(int i=1;i<=md;i++) { for(int j=1;j<=ma;j++) { int mx=0; for(int k=1;k<=i;k++) { int mn=min(k+1+need[j-1][i],k+need[j][i-k]); if(k>=2)mn=min(mn,need[j+1][i-k]); mx=max(mx,min(mn,inf)); } need[j][i]=mx; } } } void init(int md,int ma) { init_need(md,ma); for(int i=2;i<=ma+md;i++) { for(int j=1;j<=ma;j++) { int d=i-j; if(d<1||d>md)continue; for(int k=1;k<=j;k++)f[k][d][j]=solve(k,d,j); } } } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int T; cin>>T; int md=0,ma=0; for(int i=0;i<T;i++) { cin>>q[i][0]>>q[i][1]>>q[i][2]; md=max(md,q[i][1]); ma=max(ma,q[i][2]); } init(md,ma); cout<<fixed<<setprecision(6); for(int i=0;i<T;i++)cout<<val(q[i][0],q[i][1],q[i][2])<<'\n'; return 0; } ```