[CQOI2013] 新Nim游戏 题解

Statement

[P4301 CQOI2013] 新Nim游戏 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

Solution

Nim 博弈+线性基+贪心

由于蒟蒻比较菜,才入门 Nim 博弈和线性基,所以会讲一讲二者的基础内容

Nim 博弈

又叫做尼玛博弈

简单一提,比较系统的可以看 这里

Nim 博弈可以这样定义:

  1. 假设现在有 \(n\) 堆石子,每堆分别有 \(x_n\)
  2. 两个玩家轮流取石子,每次必须从其中的一堆取至少一个石子
  3. 最后一个取石头的玩家获胜

定义一个多元组 \((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\) 的二进制摆出来,举例子:

\[13=1\ 1\ 0\ 1\\ \ \ 9=1\ 0\ 0\ 1\\ \ \ 6=0\ 1\ 1\ 0\\ \]

那么最后的 \(p\)

\[p[3]=13=1\ 1\ 0\ 1\\ p[2]=4=0\ 1\ 0\ 0\\ p[1]=2=0\ 0\ 1\ 0\\ p[0]=0=0\ 0\ 0\ 0\\ \]

线性基可以做的是:

  • 查询一个元素是否可以被异或出来
  • 查询异或最值
  • 查询异或 \(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;
}
posted @ 2021-11-05 21:49  _Famiglistimo  阅读(74)  评论(0)    收藏  举报