[CQOI2013] 新Nim游戏 题解
Statement
[P4301 CQOI2013] 新Nim游戏 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
Solution
Nim 博弈+线性基+贪心
由于蒟蒻比较菜,才入门 Nim 博弈和线性基,所以会讲一讲二者的基础内容
Nim 博弈
又叫做尼玛博弈
简单一提,比较系统的可以看 这里
Nim 博弈可以这样定义:
- 假设现在有 \(n\) 堆石子,每堆分别有 \(x_n\) 个
- 两个玩家轮流取石子,每次必须从其中的一堆取至少一个石子
- 最后一个取石头的玩家获胜
定义一个多元组 \((x_1,x_2,\dots,x_n)\) 表示现在每堆的石头数量,叫做状态
定义 \(P-position\) 为后手必胜的状态,\(N-position\) 为先手必胜的状态
(\(P\to prev\quad N\to next\) ,也就是说 \(prev/next\) 做完了操作之后形成的局面)
比如 \((0,0,\dots,0)\) 就是一个 \(P\) (简称)
有一个经典结论
Bouton’s Theorem: 对于一个有 \(n\)个堆的 Nim 游戏,假设场上石头数为 \(x_1,x_2,\dots,x_n\) ,那么状态 \((x_1,x_2,\dots,x_n)\) 是一个 P-position 当且仅当 \(\oplus_{i=1}^n x_i==0\) ,这里 \(\oplus\) 指异或,异或和在这里被称为是 Nim-Sum。
我们可以口胡一手证明:
单纯从 \(N/P\) 的定义出发,可以发现,
-
如果一个状态是 \(P\) ,那么进行任意一个操作,新状态会是状态 \(N\)
这样理解:当前该先手走,后手胜,先手走了,这个时候,先后手的对象交换了
-
如果一个状态是 \(N\) ,那么存在一个操作进行,使得新状态为 \(P\)
这样理解:注意这里的操作不一定是最优操作,所以允许存在 sb 行为
-
结束点为 \(P\)
可以说明这个三个条件和 \(N/P\) 的定义是充要的
说明:我们这样构造一种必胜方案:假设 \(n=2\) ,局面 \((x,y),x< y\)
这个时候,显然先手必胜,先手只需要让状态变成 \((x,x)\) 即可。
而反手只能让 x,y 不同,所以最后一步 \((0,x)\to(0,0)\) 一定是先手做的
所以,\((x,y)\) 是一个 \(N\) 状态,\((x,x)\) 是一个 \(P\) 状态。
明确这个结论后,容易知道上述定义的转化是正确的
所以,为了证明上面那个定理,我们需要证明的是:
- 如果 \(\oplus_{i=1}^n x_i==0\) ,那么随便改一个 \(x_i\) 会使得 \(\oplus_{i=1}^n x_i\neq0\)
- 如果 \(\oplus_{i=1}^n x_i\neq 0\) ,那么可以改成 \(\oplus_{i=1}^n x_i^{\prime}==0\) ,这里要求只能改一个数且只能改小
- \((0,0,\dots,0)\) 为 \(P\)
显然,关键在于第二点的证明
我们假设最后 \(Nim-sum\) 长成这个样子: \(000...101011...\)
那么,我们只需要找到一个 \(x\) ,要求它二进制 1 的最高位是 \(Nim-sum\) 二进制 1 最高位
可以把 \(x^{\prime}\) 构造成缺失 \(x\) 最高位的 1 之后,后面的位置按照 \(Nim-sum\) 的需求来搞。那么一定存在一种构造使得用 \(x^{\prime}\) 替换 \(x\) 后, \(Nim-sum^{\prime}\) 为 \(0\)
算是证毕。
所以回到这道题,我们的目标瞬间明确了起来:干掉权值尽量小的堆们,使得剩下的堆的任何一个子集的 \(Nim-sum\) 都不为 \(0\)
怎么做到呢?我们需要 线性基
线性基
一个序列 \(a\) 的线性基 (Linear basis) 是一个数集,满足原序列 \(a\) 的任何一个数都可以通过它的子集的异或和凑出
其作用在于缩小集合大小,干掉了一些冗余信息
为了尽可能多的干掉冗余,最后形成的线性基应该是极小的
比较系统的可以看这里 dalao博客
它有这样几个性质:
- 原数列里的任何一个数都可以通过线性基里的数异或表示出来
- 线性基里任意一个子集的异或和都不为 0
- 一个数列可能有多个线性基,但是线性基里数的数量一定唯一,而且是满足性质一的基础上最少的
我们可以这样构造出一个线性基:(其中 \(x=a_1...a_n\))
void insert(int x){
for(int i=62;~i;--i)
if((1LL<<i)&x){
if(p[i])x^=p[i];
else{p[i]=x,cnt++;break;}
}
}
可以把这个过程理解成一个高斯消元的过程
我们把每一个 \(a_i\) 的二进制摆出来,举例子:
那么最后的 \(p\):
线性基可以做的是:
- 查询一个元素是否可以被异或出来
- 查询异或最值
- 查询异或 \(kth\)
对于这道题,我们显然只需要让剩下的堆中无法查找出异或为 0 的情况即可
怎么查询一个元素是否可以被异或出来:
int find(int x) {
for(int i=62;~i;--i)
if(x&(1LL<<i)) x^=p[i];
return x==0;
}
所以,考虑在插入 \(x\) 的时候,如果找到了,这时就要把 \(x\) 取走
为了取出的权值尽量小,我们考虑先把权值大的插入线性基
这个贪心可以这样理解:后插入线性基被找到的概率更高
把 \(a\) 从大到小排序即可
Code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 105;
int read(){
int s=0,w=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
while(ch>='0'&&ch<='9')s=s*10+ch-'0',ch=getchar();
return s*w;
}
int a[N],p[35];
int n,ans;
bool find(int x){
for(int i=32;~i;--i)
if((1LL<<i)&x){
if(p[i])x^=p[i];
else break;
}
return x>0;
}
void insert(int x){
for(int i=32;~i;--i)
if((1LL<<i)&x){
if(p[i])x^=p[i];
else{p[i]=x;break;}
}
}
signed main(){
n=read();
for(int i=1;i<=n;++i)a[i]=read();
sort(a+1,a+1+n,greater<int>());
for(int i=1;i<=n;++i)
if(!find(a[i]))ans+=a[i];
else insert(a[i]);
printf("%lld\n",ans);
return 0;
}

浙公网安备 33010602011771号