[博弈dp] UVA1500 Alice and Bob
posted on 2024-04-29 06:18:45 | under | source
更新了下题解。
dp 做法
首先,令 \(S=sum+n-1\)。这是最大操作数。
然后有个 simple 的想法:每次操作会改变 \(S\) 奇偶,如果 \(S\) 是奇数,先手必胜;反之后手必胜。
但是很容易被 hack,原因在于没有考虑 \(a_i=1\) 的情况。在此情况下可以移走 \(a_i=1\) 的堆将其清零,然后 \(S\) 奇偶不变。
不过,对于 \(\forall a_i>1\) 的情况,结论依旧成立。胜者只要每次将场上 \(a=1\) 的堆合并,那么败者怎么操作都无法将某一堆删成 \(0\)。即最优策略下每次操作都会改变奇偶。
这启示我们按照将所有石子分为 \(a_i=1\) 和 \(a_i>1\) 的两部分。定义 \(f_{cnt,S}\) 表示 \(cnt\) 个 \(a_i=1\),\(a_i>1\) 部分的 \(sum+n-1\)(即最大操作数)为 \(S\) 时,先手是否必胜。
然后考虑 \(f_{cnt,S}\) 转移到哪些局面:
-
只对 \(a_i>1\) 的进行操作,删除或合并都一样:\(f_{cnt,S-1}\)。这里无需考虑 \(2\) 堆挪去一个石子变成 \(1\) 堆,因为根据先前讨论赢家可以合并从而让这个 \(1\) 堆无法被清空。
-
删除 \(a_i=1\) 的:\(f_{cnt-1,S}\)。
-
合并两个 \(a_i=1\) 的,注意特判 \(S=0\):\(f_{cnt-2,S+2+[S>0]}\)。
-
合并 \(a_i=1\) 和 \(a_i>1\):\(f_{cnt-1,S+1}\)。
然后注意下边界:
- \(cnt=0\):必胜条件为 \(S\) 是奇数。
- \(cnt>0\) 且 \(S=1\),此时把 \(S\) 中这个 \(1\) 堆挪去会导致两次变化,所以需要返回 \(f_{cnt+1,0}\)。
将上述过程用记忆化搜索实现即可。复杂度 \(O(n\sum a)\)。
代码
#include<bits/stdc++.h>
using namespace std;
#define MIN(a, b) a = min(a, b)
const int N = 55, M = 50005;
int T, n, a[N], f[N][M];
inline int dfs(int cnt, int S){
if(f[cnt][S] != -1) return f[cnt][S];
if(!cnt) return f[cnt][S] = (S & 1);
if(S == 1) return f[cnt][S] = dfs(cnt + 1, 0);
int res = 2;
if(cnt) MIN(res, dfs(cnt - 1, S));
if(cnt >= 2) MIN(res, dfs(cnt - 2, S + 2 + (S ? 1 : 0)));
if(S) MIN(res, dfs(cnt, S - 1));
if(cnt && S) MIN(res, dfs(cnt - 1, S + 1));
return f[cnt][S] = (res == 0 ? 1 : 0);
}
signed main(){
memset(f, -1, sizeof f);
cin >> T;
for(int t = 1; t <= T; ++t){
scanf("%d", &n);
int _cnt = 0, _S = 0;
for(int i = 1; i <= n; ++i){
scanf("%d", &a[i]);
if(a[i] == 1) ++_cnt;
else _S += a[i] + 1;
}
_S -= (_S > 0);
printf("Case #%d: ", t);
if(dfs(_cnt, _S)) puts("Alice");
else puts("Bob");
}
return 0;
}
大力推理做法
我们记 \(sum\) 表示只对 \(\ge 2\) 的石子堆操作的最大操作数,\(cnt\) 表示 \(=1\) 的石子堆数量。
- 结论 \(1\):若 \(sum>2,2\mid sum,2\mid cnt\),则后手必胜。
证明:考虑归纳,首先 \(cnt=0\) 时结论成立。其它情况下先手无论如何操作,后手都能将其归约到上述条件。就算先手把一个 \(2\) 堆移成 \(1\) 堆,由于 \(sum>2\) 所以必然存在另一个 \(\ge 2\) 的堆,合并过去即可,游戏局数有限所以后手必胜。
- 结论 \(2\):若 \(sum>2\),后手必胜当且仅当 \(2\mid sum,2\mid cnt\)。
证明:
-
\(2\nmid sum,2\mid cnt\):若 \(cnt=0\) 显然,否则先手将两个 \(1\) 堆合并,由结论 \(1\) 可知先手必胜。
-
\(2\nmid sum,2\nmid cnt\):只需将一个 \(1\) 堆合并到 \(\ge 2\) 堆上即可。
-
\(2\mid sum,2\nmid cnt\):将一个 \(1\) 删掉即可。
- 结论 \(3\):若 \(sum=0,2\),后手必胜当且仅当 \(3\mid cnt\)。
证明:归纳,记 \(cnt=3k+p\)。
- \(k=0\): 手玩下即可发现结论成立。
- \(k>1\):当 \(p=0\) 时枚举所有情况可知后手必胜;当 \(p=1\) 时先手将 \(1\) 堆删去即可;\(p=2\) 时先手合并 \(1\) 堆即可。
复杂度 \(O(n)\)。
代码
#include<bits/stdc++.h>
using namespace std;
int T, n, a, sum, c1, c2;
signed main(){
cin >> T;
while(T--){
scanf("%d", &n), sum = c1 = c2 = 0;
for(int i = 1; i <= n; ++i){
scanf("%d", &a);
if(a > 1) sum += a, ++c2;
else ++c1;
}
sum += max(0, c2 - 1);
if(sum > 2)
if(sum % 2 == 0 && c1 % 2 == 0) puts("NO");
else puts("YES");
else
if(c1 % 3) puts("YES");
else puts("NO");
}
return 0;
}

浙公网安备 33010602011771号