博弈论,论脑子的重要性
博弈论——带上脑子,告别赌狗
Part 0.整活
学之前是制杖,学之后发现评价高了。
Part 1.什么是博弈论?
博弈论就是搞几个人来玩一个游戏(或者说是博弈),我们认为这些人非常聪明,有多聪明呢?就聪明到能在当下情况,通过强如超级计算机的脑力做出最最最正确的选择,不存在脑子短路的情况。然后来问我们其中的某人是否有必胜或者必败的策略。
一句非常启发性的话:必胜是对于当前这个状态是必胜的,与是谁无关,赢的人只是处于一个胜的状态而已。
也就是说每一个必胜的状态都是有对手上一个必败的状态推理出来的,必败的状态也是同理。这样,我们可以说,这场博弈的胜负从开始时便注定了结局。
Part 2. 基础的博弈——Bash Game
内容:有 $ n $ 个物品,两个玩家可以从这堆物品中轮流取物,每次最多取 $ m $ 个,不能不取,取到最后一个的人获得游戏的胜利。
考虑从简单到复杂:选择从 $ m=2 $ 开始推算规律。显然,你要获胜必然取完后剩下 $ 0 $ 个,那在你取之前,物品还剩 $ 1 $ 个或 $ 2 $ 个;由于对手也是非常聪明,所以他取之前一定是处于必输的局面,也就是剩下 $ 3 $ 个;要剩下 $ 3 $ 个使得对手处于必败局面,那你就是通过取 $ 1 $ 个或 $ 2 $ 个使得最后剩下 $ 3 $ 个,这样在你取之前还剩 $ 4 $ 个或 $ 5 $ 个。通过一通分析可以发现推理出现了循环,前面的选取只不过是在重复这个过程。
同时我们得到了一个结论:当 $ m=2 $ 时,物品总数 $ n=3 \times k,k \in Z $ 时先手必败,其余先手必胜。
再来分析一下 $ m=3 $ 时:
- 你取完后还剩 $ 0 $ 个是必胜的,在你取之前还剩 $ 1/2/3 $ 个;("/"表示或者)
- 由于对手是没法扭转必败的局面,所以在他取之前只能剩下 $ 4 $ 个;
- 你需要取 $ 1/2/3 $ 个使得最后剩下 $ 4 $ 个,在你取之前还剩 $ 5/6/7 $ 个。
发现此时推理又出现的循环,得到结论:当 $ m=3 $ 时,物品总数 $ n=4 \times k,k \in Z $ 时先手必败,其余先手必胜。
根据归纳法(不会证明就是归纳法)可以得到规律:当物品总数 $ n=(m+1) \times k,k \in Z $ 时先手必败,其余先手必胜。
Part 3. Nim游戏
非常经典的博弈论。
规则:有 $ n $ 堆数,每堆有 $ s_{i} $ 个,每次可以且仅可以取一堆中的若干个数。
还是从小的数据开始推断。
- 当 $ n=1 $ 时,显然先手必胜,可以理解为 $ s_{1} \bigoplus 0=s $ ;
- 当 $ n=2 $ 时,分为两种:
- 如果两堆的数量相同,那么后手只要在与先手不同的堆中取相同的数量则必赢,可以视为 $ s_{1} \bigoplus s_{2}=0 $ 。
- 如果两堆的数量不相同,那么先手只要把两堆的插值取走则可以使情况1中的先后手调换,先手必赢,可以视为 $ s_{1} \bigoplus s_{2}=s,s!=0 $ 。
- 拓展到 $ n $ ,最后状态显然所有堆都是 $ 0 $ ,异或和为 $ 0 $ ,进行最后一次操作的人(视为己方)必赢,那么在最后一次操作之前,异或和肯定不是 $ 0 $ ,因为 $ 0 $ 异或上一个正整数不可能为 $ 0 $ 。此时对手处于必输的局面,但对手本身是不会做出错误选择的,也就是说对手没得选只能这样做,所以在对手取之前,异或和为 $ 0 $ ,这样他无论如何都只能打破异或和为 $ 0 $ 的局面,从而必输。
这样就可以得到一个结论:对于 $ n $ 堆数量,第 $ i $ 堆数量记为 $ s_{i} $ ,如果 $ s_{1} \bigoplus s_{2} \bigoplus s_{3} \cdots \bigoplus s_{n}=0 $ ,则后手必赢,反之则先手必赢。
Part 4. 一些题
1. 洛谷 P2197 【模板】Nim 游戏
板子题,按照上面所述结论写即可。
code
#include <bits/stdc++.h>
#define i8 __int128
#define int long long
#define fuck inline
#define lb long double
using namespace std;
// typedef long long ll;
const int N=2e7+23,M=5e5+520,mod=1114514;
const int inf=INT_MAX,INF=1e9+7;
// const int mod1=469762049,mod2=998244353,mod3=1004535809;
// const int G=3,Gi=332748118;
// const int M=mod1*mod2;
fuck int read()
{
int x=0,f=1;
char c=getchar();
while(c<'0'||c>'9'){if(c=='-'){f=-1;}c=getchar();}
while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c-'0');c=getchar();}
return x*f;
}
fuck void write(int x)
{
if(x<0){putchar('-');x=-x;}
if(x>9) write(x/10);
putchar(x%10+'0');
}
int n;
fuck void solve()
{
cin>>n;
int ans=0;
for(int i=1,x;i<=n;i++)cin>>x,ans^=x;
if(ans==0)cout<<"No"<<"\n";
else cout<<"Yes"<<"\n";
}
signed main()
{
// ios::sync_with_stdio(false);
// cin.tie(0); cout.tie(0);
// int fuckccf=read();
int QwQ=read();
while(QwQ--)solve();
// solve();
return 0;
}
// 6666 66666 666666
// 6 6 6 6 6
// 6 6 6666 6
// 6 6 6 6 6
// 6666 6 6 6666666
2. 洛谷 P1247 取火柴游戏
比较板子。很明显,判断先手是否必败就是板子。考虑如何取先手必赢,显然需要将某堆取到一部分并保证该堆数量大于等于 $ 0 $ 的情况下,使得取后所有的数量值异或和为 $ 0 $ 。捎带一点容斥思想,已经处理的 $ ans $ 是所有数量的异或和,暴力枚举第 $ i $ 堆,将 $ ans $ 异或上第 $ i $ 堆的数量就是除第 $ i $ 堆外所有堆数量的异或和(相当于你把 $ ans $ 里第 $ i $ 堆原本的贡献异或为了 $ 0 $ ,那么剩下的就是其他数的贡献)。而题目要求的便是将该堆减少至除该堆外其他所有堆的异或和,显然能进行操作前提是减少至的数小于原本该堆的数。由于 $ b $ 最小为更高的要求,所以按顺序遍历即可。
code
#include <bits/stdc++.h>
#define i8 __int128
#define int long long
#define fuck inline
#define lb long double
using namespace std;
// typedef long long ll;
const int N=5e5+5,M=5e5+520,mod=2008;
const int inf=INT_MAX,INF=1e9+7;
// const int mod1=469762049,mod2=998244353,mod3=1004535809;
// const int G=3,Gi=332748118;
// const int M=mod1*mod2;
fuck int read()
{
int x=0,f=1;
char c=getchar();
while(c<'0'||c>'9'){if(c=='-'){f=-1;}c=getchar();}
while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c-'0');c=getchar();}
return x*f;
}
fuck void write(int x)
{
if(x<0){putchar('-');x=-x;}
if(x>9) write(x/10);
putchar(x%10+'0');
}
int a[N],n;
fuck void solve()
{
int ans=0,pos=0,sum=0;cin>>n;
for(int i=1;i<=n;i++)cin>>a[i],ans^=a[i];
if(!ans){cout<<"lose"<<"\n";return;}
for(int i=1;i<=n;i++)
{
if((a[i]^ans)<a[i])//满足更新要求
{
pos=i;
sum=a[i]-(a[i]^ans);//减少的值
a[i]=(a[i]^ans);//减少至的值
break;
}
}
cout<<sum<<" "<<pos<<"\n";
for(int i=1;i<=n;i++)cout<<a[i]<<" ";
cout<<"\n";
}
signed main()
{
// ios::sync_with_stdio(false);
// cin.tie(0); cout.tie(0);
// int fuckccf=read();
// int QwQ=read();
// while(QwQ--)solve();
solve();
return 0;
}
// 6666 66666 666666
// 6 6 6 6 6
// 6 6 6666 6
// 6 6 6 6 6
// 6666 6 6 6666666
3. 洛谷 P4301 [CQOI2013] 新Nim游戏
知识点:线性基+贪心+Nim游戏
题目要求我们保证先手必赢,也就是需要我们使得两个人第一回合取完以后所剩的数量异或和不为 $ 0 $ ,且先手所取的数量和最小。看似无法确定取哪些应该取,实际上我们可以惊奇地发现,其要求剩下所有数异或和不为 $ 0 $ ,正好符合线性基的性质(线性基内任意几个不相同的数异或和不为 $ 0 $ ),这样我们可以用线性基去处理答案。如果当前数无法被插入,说明当前数与线性基内某些数异或和为 $ 0 $ (称这些数为有关数),如果不把其取走,那么后手将除有关数外的所有数取走,则会使剩下的数异或和为 $ 0 $ ,使得先手必败,所以对于这样的一个当前数,必须取走。由于题目要求最小值,所以从大到小地插入,尽量不动大数。
code
#include <bits/stdc++.h>
#define i8 __int128
#define int long long
#define fuck inline
#define lb long double
using namespace std;
// typedef long long ll;
const int N=2e7+23,M=5e5+520,mod=1114514;
const int inf=INT_MAX,INF=1e9+7;
// const int mod1=469762049,mod2=998244353,mod3=1004535809;
// const int G=3,Gi=332748118;
// const int M=mod1*mod2;
fuck int read()
{
int x=0,f=1;
char c=getchar();
while(c<'0'||c>'9'){if(c=='-'){f=-1;}c=getchar();}
while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c-'0');c=getchar();}
return x*f;
}
fuck void write(int x)
{
if(x<0){putchar('-');x=-x;}
if(x>9) write(x/10);
putchar(x%10+'0');
}
int p[N];
fuck bool ins(int x)
{
for(int i=63;i>=0;i--)
{
if(!(x&(1ll<<i)))continue;
if(p[i])x^=p[i];
else{p[i]=x;return 1;}
}
return 0;
}
int n,a[N];
fuck bool cmp(int x,int y){return x>y;}
fuck void solve()
{
cin>>n;
int ans=0;
for(int i=1;i<=n;i++)cin>>a[i];
sort(a+1,a+n+1,cmp);
for(int i=1;i<=n;i++)
if(!ins(a[i]))ans+=a[i];
cout<<ans<<"\n";
}
signed main()
{
// ios::sync_with_stdio(false);
// cin.tie(0); cout.tie(0);
// int fuckccf=read();
// int QwQ=read();
// while(QwQ--)solve();
solve();
return 0;
}
// 6666 66666 666666
// 6 6 6 6 6
// 6 6 6666 6
// 6 6 6 6 6
// 6666 6 6 6666666
4. P4279 [SHOI2008] 小约翰的游戏
$ Anti-Nim $ 问题,区别与普通 $ Nim $ 问题只在于取走最后一个石子的人是输的。
- 考虑全是 $ 1 $ 的特殊情况,显然得,偶数个先手必胜,奇数个先手必输。(情况一)
- 考虑一堆为多个(个数大于 $ 1 $ 个),其他堆为 $ 1 $ 个的情况。如果当前总堆数是奇数个,那只要先手把多个的那个堆取到剩 $ 1 $ 个,留给对面奇数个 $ 1 $ 的局面,则先手必赢;如果当前总堆数是偶数个,那先手只要把多个的那一堆全部取掉,那么依然是留给对面奇数个 $ 1 $ 的局面,先手必赢。综上所述,当一堆为多个(个数大于 $ 1 $ 个),其他堆为 $ 1 $ 个时,先手必赢。(情况二)
- 考虑有若干堆多个的情况,可以尝试从情况二进行拓展,仿照普通的 $ Nim $ 游戏采用异或的方法。很显然,对情况二每堆数量进行异或最终得到的结果一定是非 $ 0 $ 的,也就是说谁能在操作前拿到异或和非 $ 0 $ 的状态是必胜,因为此时这个人只需要取完后使得异或和等于 $ 0 $ ,那么对手只能打破异或和等于 $ 0 $ 的情况,从而陷入必败的局面。这样我们就得到了结论:当有若干堆多个时,如果所有堆异或和为 $ 0 $ ,则先手必胜;反之先手必败。
code
#include <bits/stdc++.h>
#define i8 __int128
#define int long long
#define fuck inline
#define lb long double
using namespace std;
// typedef long long ll;
const int N=5e5+23,M=5e3+23,mod=536870912;
const int inf=INT_MAX,INF=1e9+7;
// const int mod1=469762049,mod2=998244353,mod3=1004535809;
// const int G=3,Gi=332748118;
// const int M=mod1*mod2;
fuck int read()
{
int x=0,f=1;
char c=getchar();
while(c<'0'||c>'9'){if(c=='-'){f=-1;}c=getchar();}
while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c-'0');c=getchar();}
return x*f;
}
fuck void write(int x)
{
if(x<0){putchar('-');x=-x;}
if(x>9) write(x/10);
putchar(x%10+'0');
}
int n;
fuck void solve()
{
cin>>n;int ans=0,f=1;
for(int i=1;i<=n;i++)
{
int x;cin>>x;
f=(f&(x==1));
ans^=x;
}
if(f)cout<<(ans?"Brother":"John")<<endl;
else cout<<(ans?"John":"Brother")<<endl;
}
signed main()
{
// ios::sync_with_stdio(false);
// cin.tie(0); cout.tie(0);
// int fuckccf=read();
int QwQ=read();
while(QwQ--)solve();
// solve();
return 0;
}
// 6666 66666 666666
// 6 6 6 6 6
// 6 6 6666 6
// 6 6 6 6 6
// 6666 6 6 6666666
5. P14059 【MX-X21-T4】[IAMOI R5] 使一颗心免于哀伤
抓住博弈的平衡状态: $ 010101……01 $ 。
首先进行特判:
- 全为 $ 1 $ ,星期日赢;
- 全为 $ 0 $ ,知更鸟赢;
- 只有两段,知更鸟赢;
对于一般的情况,我们可以发现最终都会转化为博弈的平衡状态,而且在平衡状态下先手必败。对于需要删除的数字更多的人越能让对手处于平衡状态下的先手,于是我们得到结论:
- 当 $ 1 $ 的数量大于 $ 0 $ 的数量时,知更鸟必胜;
- 当 $ 1 $ 的数量小于 $ 0 $ 的数量时,星期日必胜;
- 当 $ 1 $ 的数量等于 $ 0 $ 的数量时,星期日必胜;
code
#include <bits/stdc++.h>
#define i8 __int128
#define int long long
#define fuck inline
#define lb long double
using namespace std;
// typedef long long ll;
const int N=1e5+5,mod=998244353,S=70,M=2e6+5;
const int INF=1e9+7;
const int inf=LONG_LONG_MAX/2;
// const int mod1=469762049,mod2=998244353,mod3=1004535809;
// const int G=3,Gi=332748118;
// const int M=mod1*mod2;
fuck int read()
{
int x=0,f=1;
char c=getchar();
while(c<'0'||c>'9'){if(c=='-'){f=-1;}c=getchar();}
while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c-'0');c=getchar();}
return x*f;
}
fuck void write(int x)
{
if(x<0){putchar('-');x=-x;}
if(x>9) write(x/10);
putchar(x%10+'0');
}
int n;
string s;
int cnt[5],f[5][N];
fuck void clr()
{
memset(cnt,0,sizeof(cnt));
memset(f,0,sizeof(f));
}
fuck void solve()
{
clr();
cin>>n>>s;
f[s[0]-'0'][++cnt[s[0]-'0']]++;
for(int i=1;i<s.size();i++)
{
if(s[i]==s[i-1])f[s[i]-'0'][cnt[s[i]-'0']]++;
else f[s[i]-'0'][++cnt[s[i]-'0']]++;
}
if(s[0]==s[s.size()-1])f[s[0]-'0'][1]+=f[s[0]-'0'][cnt[s[0]-'0']],cnt[s[0]-'0']--;
if(cnt[0]==0&&cnt[1]==0)
{
if(s[0]=='0'){cout<<"Robin"<<endl;return;}
cout<<"Sunday"<<endl;return;
}
if(cnt[1]==1&&cnt[0]==1){cout<<"Robin"<<endl;return;}
int cnts=0,cntr=0;
for(int i=1;i<=cnt[0];i++)cnts+=f[0][i];
for(int i=1;i<=cnt[1];i++)cntr+=f[1][i];
if(cntr==cnts){cout<<"Sunday"<<endl;return;}
if(cntr>cnts){cout<<"Robin"<<endl;return;}
if(cntr<cnts){cout<<"Sunday"<<endl;return;}
}
signed main()
{
// ios::sync_with_stdio(false);
// cin.tie(0); cout.tie(0);
int QwQ=read();
// int fuckccf=read();
while(QwQ--)solve();
// solve();
return 0;
}
// 6666 66666 666666
// 6 6 6 6 6
// 6 6 6666 6
// 6 6 6 6 6
// 6666 6 6 6666666
完结收工!!!!!

看完点赞,养成习惯
\(\Downarrow\Downarrow\Downarrow\Downarrow\Downarrow\Downarrow\Downarrow\Downarrow\Downarrow\Downarrow\Downarrow\Downarrow\Downarrow\Downarrow\Downarrow\Downarrow\Downarrow\Downarrow\Downarrow\Downarrow\Downarrow\Downarrow\Downarrow\Downarrow\Downarrow\)

浙公网安备 33010602011771号