P3185 [HNOI2007] 分裂游戏
题目描述
聪聪和睿睿最近迷上了一款叫做分裂的游戏。
该游戏的规则是: 共有 \(n\) 个瓶子, 标号为 \(0, 1, \ldots, n-1\),第 \(i\) 个瓶子中装有 \(p_i\) 颗巧克力豆,两个人轮流取豆子,每一轮每人选择 \(3\) 个瓶子,标号为 \(i,j,k\), 并要保证 \(i \lt j, j \leq k\),且第 \(i\) 个瓶子中至少要有 \(1\) 颗巧克力豆,随后这个人从第 \(i\) 个瓶子中拿走一颗豆子并在 \(j,k\) 中各放入一粒豆子(\(j\) 可能等于 \(k\)) 。如果轮到某人而他无法按规则取豆子,那么他将输掉比赛。胜利者可以拿走所有的巧克力豆!
两人最后决定由聪聪先取豆子,为了能够得到最终的巧克力豆,聪聪自然希望赢得比赛。他思考了一下,发现在有的情况下,先拿的人一定有办法取胜,但是他不知道对于其他情况是否有必胜策略,更不知道第一步该如何取。他决定偷偷请教聪明的你,希望你能告诉他,在给定每个瓶子中的最初豆子数后是否能让自己得到所有巧克力豆,他还希望你告诉他第一步该如何取,并且为了必胜,第一步有多少种取法?
输入格式
输入文件第一行是一个整数 \(t\),表示测试数据的组数。
每组测试数据的第一行是瓶子的个数 \(n\),接下来的一行有 \(n\) 个由空格隔开的非负整数,表示每个瓶子中的豆子数。
输出格式
对于每组测试数据,输出包括两行,第一行为用一个空格两两隔开的三个整数,表示要想赢得游戏,第一步应该选取的 \(3\) 个瓶子的编号 \(i,j,k\),如果有多组符合要求的解,那么输出字典序最小的一组。如果无论如何都无法赢得游戏,那么输出用一个空格两两隔开的三个 \(-1\)。
第二行表示要想确保赢得比赛,第一步有多少种不同的取法。
提示
\(1 \leq t \leq 10\),\(2 \leq n \leq 21\),\(0 \leq p_i \leq 10^4\),
Solution:
博弈论部分:
P:
MnZn 刚学博弈论 1ms ......
首先我们先来介绍一下博弈论这一坨东西:定义博弈状态:
基本知识:
定理 1:没有后继状态的状态是必败状态。
定理 2:一个状态是必胜状态当且仅当存在至少一个必败状态为它的后继状态。
定理 3:一个状态是必败状态当且仅当它的所有后继状态均为必胜状态。
—— oiwiki
然后我们以 Nim 游戏 为例来说一下 sg 函数:
首先我们要进行一个转化:我们假设两个互不相交状态A,B,只要满足:
- 1:没有后继状态的状态是\(B\)。
- 2:一个状态是 \(A\) 当且仅当存在至少一个 \(B\) 为它的后继状态。
- 3:一个状态是 \(B\) 当且仅当它的所有后继状态均为 \(A\) 。
那么就可以说 \(A\) 是该游戏的必胜态,\(B\) 是该游戏的必败态
然后我们定义:
\(sg(x)=mex\) { \(sg(y) \ | \ y \in son(x)\) }
定义一个状态 \(S\) 的 sg 函数为该状态下所有点的 sg 函数的异或和。
然后我们将 $A=[sg(S) \ne 0] \ B= [sg(S) = 0] $ 带入 :
在 Nim 游戏中,我们将石子个数定为 \(sg\) 函数的值。
显然满足 :
- 1:没有后继状态的状态是\(sg(S)=0\)。
然后我们只需证明:
- 2 :一个状态是 \(sg(S) \ne 0\) 当且仅当存在至少一个 \(sg(S) = 0\) 为它的后继状态。
- 3:一个状态是 \(sg(S) = 0\) 当且仅当它的所有后继状态均为 \(sg(S) \ne 0\) 。
对于2,我们设 \(k=sg(S)\),假设他的二进制最高位为 \(d\) ,说明在这个状态下有奇数个数的二进制最高位为 \(d\),那么我们从中选一个数,将他的 \(d\) 位改成 0,然后将其他位改成去掉这个数之后剩下数的异或和。
对于 3:很显然随便改一个数字就变成0了。
本题思路:
我们首先我们发现先手的操作是可以被后手仿照的,所以我们只关心豆子的奇偶性,然后我们\(O(n^3)\) 预处理构造出 sg 函数,对于每个询问构造初始状态然后再 \(O(n^3)\) 计数就好了。
Code:
#include<bits/stdc++.h>
const int N=21;
using namespace std;
int sg[N+5],vis[N<<2],a[N+5];
struct ANS{int i,j,k,cnt;
}ans;
int n;
void get_sg()
{sg[1]=0;for(int i=2;i<=N;i++){for(int j=1;j<=i;j++){for(int k=j;k<i;k++){vis[sg[j]^sg[k]]=i;for(sg[i]=0;vis[sg[i]]==i;sg[i]++);}}}
}
void solve(int x)
{ans={-1,-1,-1,0};for(int i=n;i;i--)for(int j=i-1;j;j--)for(int k=j;k;k--){if(!(x^sg[i]^sg[j]^sg[k])){if(!ans.cnt)ans=(ANS){n-i,n-j,n-k,1};反转else ans.cnt++;}}printf("%d %d %d\n%d\n",ans.i,ans.j,ans.k,ans.cnt);
}
void work()
{cin>>n;int x=0;for(int i=n;i;i--)cin>>a[i];//注意,这里将数组反转了。for(int i=1;i<=n;i++)if(a[i]&1)x^=sg[i];solve(x);
}
int main()
{//freopen("P3185.in","r",stdin);freopen("P3185.out","w",stdout);ios_base::sync_with_stdio(0);cin.tie(0),cout.tie(0);get_sg();int T;cin>>T;while(T--)work();return 0;
}