博弈论
accoders 做题记录
【典例】Game
两个人玩一个游戏。有一个 \(1\times n\) 的棋盘,有一些格子已经染了色。两个人轮流操作,每次一个人可以选择一个没有染色的格子,把它染成白色或黑色,要求相邻的格子不能染成相同的颜色。最后不能操作的那个人为输。\(n\le 100000\)
Step 1 发现子问题:实质是若干个极长连续0区间的游戏。
Step 2 发现游戏性质:极长连续0区间 [l,r]显然可以归纳为 4 种(称l-1,r+1为两端):①两端同色②两端异色③一端自由一端有色④两端自由
Step 3 SG 递推:过于显然,略
Step 4 找规律:从 SG 表中寻找规律,得:①0②1③区间长④区间长%2
Step 5 回到原问题:每一段 SG 值异或一下就可知道答案
【针对练习】[HNOI2014]江南乐(评:很直接的暴力 SG 递推,找规律后用整除分块优化也显然)
【例1 - 阶梯博弈】Georgia and Bob (评:告诉你什么是阶梯博弈)
【针对练习】[POI2004]Gra (评:难度飙升)
想到阶梯博弈是不难的,但是要把思路打开——怎样套到阶梯博弈的模型里。模拟一下操作应该可以发现方法是,将每个极长黑色连通段看成随后第一个白色上的棋子;可以用的白格子是位置 \(\le m-2\) 的,必败态是所有棋子从 m-2 往左接连排开(这样你不管操作那一步都会到占据 m-1 的必胜态)
有一些要注意的点:
- 如果 m-1 被占据则应该直接输出答案:最后一段极长黑色连通段的长度;
- 如果 m-2 被占据则忽略占据 m-2 的这一极长黑色连通段;
于是我们按照刚才的划分方式摆出这个阶梯。由于 \(m\le 10^9\),因此可以忽略的白格子是得忽略的:
- 一段长度为偶数的白格子保留 1 个空阶梯
- 一段长为奇数且长度 ≥ 3 的白格子保留 2 个空阶梯,长度 = 1 则不加空阶梯
特别注意第 2 条的前者,不能保留 0 个,这 2 个看似可以忽略,实则可以作为奇数台阶上的棋子向偶数台阶转运的场所。
#include <bits/stdc++.h>
#define pii pair<int,int>
#define fi first
#define se second
using namespace std;
inline int read(){
register char ch=getchar();register int x=0;
while(ch<'0'||ch>'9')ch=getchar();
while(ch>='0'&&ch<='9')x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
return x;
}
const int N=1e6+5;
int m,n,tot;
pii a[N];
vector<int>st;
int main(){
m=read(),n=read();
for(int i=1,p;i<=n;i++){
p=read();
if(!tot||a[tot].se+1<p)a[++tot]=make_pair(p,p);
else a[tot].se++;
}
if(a[tot].se==m-1){
cout<<a[tot].se-a[tot].fi+1;
return 0;
}
st.push_back(2333);
a[tot+1].fi=m-1;
if(a[tot].se==m-2)tot--;
for(int i=tot;i;i--){
if((a[i+1].fi-1-a[i].se)%2==0)st.push_back(0);
else if((a[i+1].fi-1-a[i].se)!=1)st.push_back(0),st.push_back(0);
st.push_back(a[i].se-a[i].fi+1);
}
n=st.size()-1;
int sg=0;
for(int i=1;i<=n;i++){
if(i&1)sg^=st[i];
}
if(!sg){puts("0");return 0;}
int ans=0;
for(int i=1;i<=n;i++){
if((i&1)&&(sg^st[i])<st[i])ans++;
if(!(i&1)&&(sg^st[i-1])>st[i-1]&&(sg^st[i-1])<=st[i-1]+st[i])ans++;
}
cout<<ans;
}
基本策略
如果有必胜策略,采用必胜策略;
先保证自己不输,再想害别人。
ICG
一、概念
- NIM 博弈:\(n\) 堆石子,\(a_1,a_2,\cdots,a_n\),每人可任选一堆取任意个但不能不取,拿走最后一颗石子的人获胜,问先手是否必胜。
- 局面:游戏期间某一时刻面临的状态;先手、后手:游戏开始最先操作操作的人称先手。
- 先手必胜:两人均采取最优策略,先手必胜,后手必败。
- 最优策略:如果可以操作使得对方必败则优先采取这样的操作。
- 公平组合游戏(ICG):两名玩家交替行动;任意时刻,每人可以执行的操作和轮到谁无关;不能执行合法操作的玩家失败。
- 有向图游戏:将公平组合游戏中的局面看成节点,每个节点有后继节点(可以转移到的局面),节点与后继节点间连有有向边,因此 ICG 可看作有向图上的游戏。
- 有向图游戏的和:如果一个有向图 \(G\) 游戏是这样玩的:从子有向图游戏 \(G_1,G_2,\cdots,G_m\) 中选一个 \(G_i\) 走一步,那么 \(G\) 称为 \(G_1,G_2,\cdots,G_m\) 的和。
二、SG 函数
- mex 运算:非负整数集合 S,mex(S) 表示不属于集合 S 的最小非负整数。
- SG 函数:一个有向图游戏中的节点(局面) \(x\) 的后继节点(后继局面)\(y_1,y_2,\cdots,y_k\),\(x\) 的 SG 函数值 \(SG(x)=mex({SG(y_1),SG(y_2),\cdots,SG(y_k)}\)。
- 有向图游戏的和的 SG 值等于它的所有子有向图游戏的 SG 值的异或和。做题时应该先考虑子游戏的 SG 值(用 mex 表示的那个),再用 xor 合并之。
- SG(G)=SG(初始局面)。
- 定理:SG(x) 则局面 \(x\) (先手)必胜,!SG(x) 则局面 \(x\) 先手必败。
习题
【例】剪纸游戏ACwing219
ACcode:(题解:设置2*3,3*3,3*2,2*2
四个状态为终止条件,这时一定必败了。附注:调了几个小时,原来问题出在之前没有把unordered_map放在SG()中声明;每执行一次SG()应该拥有自己的unordered_map,放在全局定义反而会乱七八糟。另外,博弈的题搜索注意剪枝,这题的剪枝就有两个:1记忆化,2大于n/2,m/2的i就不跑了,会重;还需注意做一次就把sg[n][m]跟sg[m][n] 一起填了。这题如果用vector求mex会比桶标记慢不少,这种问题就不知道怎么避免了,大概只能碰运气。。)
#include <bits/stdc++.h>
using namespace std;
int vis[205][205],sg[205][205];
int SG(int n,int m){
if(vis[n][m]) return sg[n][m];
vis[n][m]=vis[m][n]=1;
if(n<=3 && m<=3)
return 0;
unordered_map<int,int> book; book.clear();
for(int i=2;i+i<=n;i++) book[SG(i,m)^SG(n-i,m)]=1;
for(int i=2;i+i<=m;i++) book[SG(n,i)^SG(n,m-i)]=1;
int it=0; while(book[it]) it++;
return sg[n][m]=sg[m][n]=it;
}
signed main()
{
int n,m;
while(cin>>n>>m){
memset(sg,0,sizeof(sg)); memset(vis,0,sizeof(vis));
if(SG(n,m)) puts("WIN"); else puts("LOSE");
}
return 0;
}
CF919F A game with Numbers
基本递推,注意不是DAG所以倒推。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int tot,box[8],card[500][8],mp[625*625+5],in[500][500],f[500][500],a[8],b[8];
vector<pair<int,int> >G[500][500];
inline int calc(int a[8]){
int s=0;
sort(a,a+8);for(int i=0;i<8;i++)s=s*5+a[i];
return s;
}
void pfs(int x){
if(x==8){
for(int i=0;i<7;i++)if(box[i]>box[i+1])return;
memcpy(card[++tot],box,sizeof(box));mp[calc(box)]=tot;;
return;
}
for(int i=0;i<5;i++){box[x]=i;pfs(x+1);}
}
int main(){
pfs(0);
memset(f,-1,sizeof(f));
queue<pair<int,int> >Q;
for(int i=1;i<=tot;i++)for(int ii=1;ii<=tot;ii++){
for(int j=0;j<8;j++)
for(int jj=0;jj<8;jj++){
if(card[i][j]*card[ii][jj]==0)continue;
for(int uu=0;uu<8;uu++)card[0][uu]=card[i][uu];
card[0][j]=(card[0][j]+card[ii][jj])%5;
int u=mp[calc(card[0])];
G[ii][u].push_back(make_pair(i,ii));in[i][ii]++;
}
bool o=0;
for(int jj=0;jj<8;jj++)if(card[ii][jj]){o=1;break;}
if(!o)f[i][ii]=0,Q.push(make_pair(i,ii));
}
while(!Q.empty()){
int x=Q.front().first,y=Q.front().second;Q.pop();
for(int i=0;i<G[x][y].size();i++){
int xx=G[x][y][i].first,yy=G[x][y][i].second;
if(f[xx][yy]!=-1)continue;//beware of pushing in the same node multiple times
in[xx][yy]--;
if(!f[x][y]){f[xx][yy]=1;Q.push(make_pair(xx,yy));}
else if(!in[xx][yy]){f[xx][yy]=0;Q.push(make_pair(xx,yy));}
}
}
int T;cin>>T;
while(T--){
int xs;cin>>xs;
for(int i=0;i<8;i++)cin>>a[i];
for(int i=0;i<8;i++)cin>>b[i];
if(xs)for(int i=0;i<8;i++)swap(a[i],b[i]);
int tmp=f[mp[calc(a)]][mp[calc(b)]];;
if(tmp>0)puts(xs?"Bob":"Alice");
else if(!tmp)puts(xs?"Alice":"Bob");
else puts("Deal");
}
}
CF850C Arpa and a game with Mojtaba
分解质因数后,一个质因数的游戏的 SG 值显然需要暴力算,但是可以优化这个暴力,因为 \(p^k\),k相同的堆肯定同生息共命运,所以只需要状压 k 就可以。可以 \(\log_2 a\) 地暴力算。
upd:这东西好像是O(玄学)的……
人人尽说江南好
https://www.cnblogs.com/impyl/p/15361019.html
CF1704F Colouring Game
https://www.cnblogs.com/impyl/p/cf.html#cf1704f-colouring-game
AGC010D Decrementing
有一个数列 \((a_1,a_2,...,a_n)\),玩家每次依次执行下列操作:
- 选一个 \(>1\) 的数 \(-1\);
- 将所有数除以其 gcd;
问先手有无必胜策略?
首先考虑,当数列中存在 \(1\) 时,有奇数个偶数先手必胜,有偶数个奇数后手必胜。——所有玩家都只能做 \(-1\) 操作。
其次考虑,当数列的 gcd 为奇数时,所有数除以一个奇数奇偶性均不变。
同时考虑,当数列中偶数的个数为奇数时,先手 \(-1\),一定能让偶数个数偶数并存在一个奇数,那么 gcd 就是奇数,于是把一个偶数个数为偶数的数列给了对方,对方 \(-1\),偶数个数又成奇数,从而先手可以保持偶数个数始终为奇数,一直到出现一个 \(1\),他就必胜了。
再次考虑,当数列中偶数个数为偶数时,先手已经较为被动,希望扭转局面,扭转局面的意思是使偶数个数成为奇数,唯一的方法就是除以一个偶的 gcd,他可以这样做当且仅当给他的数列里只有一个奇数,便将它 \(-1\),再除以偶的 gcd,他就有可能赢。注意是“有可能”,因为除以偶的 gcd 并不能保证让偶数个数奇偶性反转。反之,如果奇数个数 \(>1\),他就必败了,因为无法扭转局面。
将上文粗体部分合并起来,就是这个题的实现方法。
由于只有最后一种分情况需要接下去考虑,而此时一定已经除以了一个 \(\ge 2\) 的 gcd,所以迭代次数应不超过 \(\log_210^9\)时间复杂度 \(O(n\log 10^9)\)。
#include <bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,a[N];
int gcd(int a,int b){return b?gcd(b,a%b):a;}
bool dfs(){
bool flag=0;
for(int i=1;i<=n;i++)if(a[i]==1){flag=1;break;}
int cnt=0;
for(int i=1;i<=n;i++)cnt+=a[i]&1^1;
if(flag)return (cnt&1);
if(cnt&1)return 1;
if(n-cnt>1)return 0;
int g=0;
for(int i=1;i<=n;i++){if(a[i]&1)a[i]--;g=gcd(g,a[i]);}
for(int i=1;i<=n;i++)a[i]/=g;
return (dfs()^1);
}
int main(){
cin>>n;for(int i=1;i<=n;i++)cin>>a[i];
puts(dfs()?"First":"Second");
}
经典博弈问题
1.Nim Game
最为经典
n堆石子,每次可以从一堆里面取任意个石子
对于一堆石子,SG函数就是石子数
整个游戏的SG函数是每一堆石子的SG函数的异或和
必胜:SG不为0
必败:SG为0
2.Bash Game
每次最多取m个石子,其他同Nim
一堆石子的SG函数为石子数mod(m+1)
整个游戏的SG函数是每一堆石子的SG函数的异或和
必胜:SG不为0
必败:SG为0
3.Nimk Game
每次最多可以同时从k堆石子进行操作,这k堆可以取不同数量的石子
一堆石子的SG函数为石子数
对每一个二进制位单独算,求SG函数每一个二进制位1的个数mod(k+1),如果都为0,则必败,否则必胜
证明:
对于必败态不管怎么走都只能走到必胜态
对于变化的SG的最高位,你至少变化为1,最多变化为k,所以这一位1的个数不可能mod(k+1)还是为0
对于必胜态我们肯定可以找到一种方法走到必败态
我们从高位往低位做,记s为这一位可以随意填值的数字个数(如果把某一位从1变成0,那么更低位就能随便取值了)
假设我们现在做到第k位,记n为除了能随便取值的s位以外这一位1的个数mod(k+1)
如果n+s<=k,那么很简单,我们取出n个第k位为1的让这些数字的第k位变成0,那s个数字这一位也变成0,然后s+=n
如果n+s>k,即n+s>=k+1,那么s>=k+1-n,我们在s中间取k+1-n个变为1,其他变为0就可以满足条件了
4.Anti-SG
5.Staircase Nim
阶梯博弈
每次可以从一个阶梯上拿掉任意数量石子放到下一层阶梯,不能操作的为输
SG函数为奇数阶梯上的石子的异或和
如果移动偶数层的石子到奇数层,对手一定可以继续移动这些石子到偶数层,使得其SG不变
6.Wythoff Game
有两堆石子,每次可以从一堆或者两堆里拿走一样数目的石子,不能取的为输
必败态为(1,2)(3,5)(4,7)(6,10)...
差为1,2,3,4.....每一对数的第一个数为前面没出现的最小的正整数
7.Take & Break
每次可以把一堆石子分成两堆甚至多堆不为0的石子,不能操作的为输
暴力计算SG
8.树上删边游戏
给定根节点,每次可以删掉一条边,不与根节点相连的部分删除
叶子节点SG为0,其他节点\(SG(u)=\bigoplus_{v\in son(u)}(SG(v)+1)\)
证明:
另外,"SG(长为\(i\)的链)=i"的结论应熟知。
9.无向图删边
一颗有根树上还挂着一些环,环两两无公共边,且原树和环有唯一的公共点;每次可以删掉一条边并丢弃不和根相连的部分,先手必胜?
结论:SG(奇环)=1,缩成一条新边,SG(偶环)=0,缩成一个点,然后套用树上删边游戏
证明:奇环-删掉的1条边=长度偶数,成为的两条“链”奇偶性相同,xor起来是偶数,SG是不同方案取mex,那就是mex{0,2,...}=1;偶环同理。
10.翻硬币游戏
n枚硬币排成一排,有的正面朝上,有的反面朝上。
游戏者根据某些约束翻硬币(如:每次只能翻一或两枚,或者每次只能翻连续的几枚),但他所翻动的硬币中,最右边的必须是从正面翻到反面。
谁不能翻谁输。
需要先开动脑筋把游戏转化为其他的取石子游戏之类的,然后用如下定理解决:
局面的 SG 值等于局面中每个正面朝上的棋子单一存在时的 SG 值的异或和。
11.Every-SG
n个游戏排成一排,每个游戏是两堆石子,不妨记为(a,b)(a<=b),一个人操作一个游戏是将b-ka(\(k∈\N_+,b-ka\ge 0\))。两个人每次把所有还能操作的游戏操作一次,不能执行者失败(n,a,b<=1000)
解法:
在一般SG的基础上,若SG(S)>0,先手想拖延时间,step[S]=max{step[T]}+1;若SG(S)=0,先手想尽快结束,step[S]=min{step[T]}+1.可以对所有a,b<=1000把SG(a,b)和step[(a,b)]求了,DAG的点数是max(a*b)=1e6的,暴力dp。一个局面必胜当且仅当每一堆的step取max是奇数。
p1.“日”字形
可以选择不构成环的边集任意取(不能不取)的nim.
解法:
先手必败当且仅当v1=v2=v3,v5=v6=v7,v1 xor v4 xor v7=0
p2.含参k[i]
每次可以取一堆的\([1,\lfloor a_i/k_i\rfloor]\)个的nim.\(n\le 200,a_i,k_i\le 10^9\)
解法:打表找规律:当A%k==0时SG(A)=A/k,否则SG(A)=SG(A-A/k)(/表示整除)。就可以根号分治。\(k>\sqrt n\),暴力减,直到——;\(k\le \sqrt n\),根据整除分块的思想,可以O(1)跳完一个块,而块数\(\le\sqrt n\)。
p3.动态减法问题
一堆\(n\)个石子,每次取不超过上次选手取的\(k\)倍的石子数,第一次不能取完,先手必胜?\(n\le 10^8,k\le 10^5\)
解法:先从\(k=1,2\)入手。\(k=1\)时,每次取\(lowbit(n)\),对手就只能取等于或低于这一位的,而不能取到高于这一位的,这种格局下去最后一个1一定为我所取;\(k=2\)时,考虑\(n\)的斐波那契分解(显然是唯一的),斐波那契分解的相邻两项之间一定至少隔着一个单位,否则(如果挨着)就能合并,又由于\(2fib_{i-1}\ge fib_i>2fib_{i-2}\),因此取完\(fib_{i-2}\)最多只能取到\(fib_{i-1}\),不会惊动\(\ge fib_i\),所以同\(k=1\)格局不会被打破。
那么我们可以观察一下这样子的构造方式本质是什么?本质是我们想要构造一个\(\forall i,ka_i<a_{i+k}\)的数列,它能表示\([1,n]\)。可以记\(b_i\)为\(a_1\sim a_i\)最多表示到几,那么我们有
a[i]=b[i-1]+1;
if(upper_bound(a,a+i,(a[i]-1)/k)-a>0)b[i]=a[i]+b[(upper_bound(a,a+i,(a[i]-1)/k)-a-1)];
else b[i]=a[i];