巴巴博弈——博弈论模块浅谈
可能更好的阅读体验
巴巴博弈
本文将竭尽所能带您一步步了解博弈论,如有错误,请各位大手子在评论区慷慨指出,谢谢!
正文
博弈论是个本质困难非常有趣的学科。
无特殊说明,本文均假设博弈者聪明绝顶。
1. 博弈基础
在竞赛中,我们一般面对的都是组合博弈:
- 两名玩家,轮流操作
- 信息对双方公开,无隐藏信息(如军棋,扑克就不是)
- 无随机性
- 会在有限步数内终止
- 无平局
博弈分类
- 公平组合游戏:当前操作只与状态有关,与玩家无关的组合博弈(如象棋就不是)。
- 零和博弈:玩家收益之和恒不变。
反之则为非公平组合游戏、非零和博弈。
本文无特殊说明,都是公平组合游戏,且都至少取走
N/P 态
必胜态
(N) :即对于当前操作的人来说,有可以必胜的决策。必败态
(P) :即对于当前操作的人来说,没有决策可以让他避免失败。
根据定义,我们可以推出以下结论:
- 当一个状态为必胜态
(N) ,他的后继状态(即他操作后的状态)中有一个必败态(P) 。 - 当一个状态为必败态
(P) ,他的所有后继状态均是必胜态(N) 。
证明很容易,因为假设博弈者们是聪明绝顶,自行手推一下就懂了。
注:建议把
2. 常见公平组合游戏模型
接下来,我将讲解经典博弈模型,难度主观从易到难。
巴什博弈 (bash game)
例题 - HDU4764 vj
有两个人,他们面前有一堆石子,每次他们可以取
[1,k] 个石子,无法操作的人输
::::info[结论]
当石子数为
::::success[证明1]
显然,当此时的石子为
由此,总数为
我们来几张图加深理解:
可以看到这里有一共
则,先手先拿
若后手拿
以此类推,直至还剩
| 此时到后手,而他至多拿 |
|---|
::::success[证明2]
我们尝试从
可以得知,
而如此,
自然而然地,根据定义,
| 因此, |
|---|
对于例题,我们可以将其换个角度,即将
::::info[代码]
记得
while(cin>>n>>k&&n&&k){
n--;
if(n%(k+1))cout<<"Tang\n";
else cout<<"Jiang\n";
}
::::
Nim 游戏
例题-P2197
还是两个人,但这次他们面前有 k 堆石子,每次他们可以从一堆中取出若干个(至少为
1 ),无法操作的人输。
::::info[结论]
设第 k 堆石子中有
| 若 |
|---|
::::success[证明]
依旧从
当无石子时,一定是
从而得知,只有一堆石子时,一定是
有奇数堆数量为
而偶数堆数量为
由此可依次推出
经过一系列推导,我们可以发现,当
为什么呢?
其实,对于任意异或和不为
显然,一定会存在一个数,将其与异或和异或后,其会小于本身,此时异或和也变为
这样,到了最后是必胜的,让石子数异或和为
有一种证法是说对于异或和为
| 发现与 |
|---|
例题就是模板。
::::info[代码]
while(t--){
cin>>n;int sum=0;
for(int i=1,x;i<=n;i++)cin>>x,sum^=x;
cout<<(sum?"Yes\n":"No\n");
}
::::
阶梯 Nim 游戏 (Moore`s Nim) — Nim 变式
例题 - P3480
在一个有
n 个阶梯的楼梯上,依次放着任意数量石子,每次可选一个阶梯上的若干个石子,使他们下到下一个阶梯,下到地面(0 阶梯)的石子不能移动。
::::info[结论]
对于所有的奇数阶梯,对其石子数做异或和,异或和为
::::success[证明]
我们考虑,阶梯上没有石子时,是
而我们套用 Nim 游戏的第二套证法,则对于奇数异或和为
为什么不用考虑偶数?
| 因为若对方移动石子至偶数阶梯或从偶数阶梯移至奇数,我们都可以移动相同数量的石子来维护异或和。并且 |
|---|
例题大意:
有
n 堆数量非严格升序的石子,每次可选一堆取石子,但要满足数量依旧非严格升序,无法操作的人输。
对于例题,我们发现,一个堆可以移动的数量只与前一个数有关,若前一个堆移走一定数目,则后一个堆可移动的会增加相同数目。诶,这不就是相当于将前一堆的移至下一堆吗!不就是阶梯 Nim 吗!石子数量是当前堆数量减上一堆。记得是从
::::info[代码]
while(t--){
cin>>n;int ans=0;
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1;i<=n;i++)
if((i%2)^(n%2==0))ans^=a[i]-a[i-1];
if(ans==0)cout<<"NIE\n";
else cout<<"TAK\n";
}
::::
Nim-k 游戏 — Nim 扩展
例题 - P2490
游戏规则在 Nim 基础上变成变成了可任选不超过
k 堆石子,在这几堆石子中取走任意个。
::::info[结论]
将石子数转为二进制,若存在某个二进制位上的
::::success[证明] 考虑从 Nim 游戏中扩展。
当石子数为
我们每次可以选
我们思考如何一步使取模为
我们从高位开始,则对于已经选的堆,当前位可
假设已经选
-
当
m\le k-s 时,直接全选。 -
当
m>k-s 时,我们让那m 堆中有k-m+1 个1 就行。因为我们从高位开始,且k-m+1<k-(k-s)+1=s+1 ,即k-m+1\le s ,所以一定可以做到。
举个例子,假设
当
| 当 |
|---|
例题大意:
在一个长度为
n 的棋盘上,有k 个棋子,有黑白两种颜色,每种颜色数量相同,白棋子只能往右,黑棋子只能往左,最左边的一定是白棋子,最右边的一定是黑棋子。两人分别控制黑白棋子,每次可移动1-d 个棋子任意步,但不能越过任何棋子。无法移动者输。
对于例题,显然不是模板而且似乎还是非公平组合游戏,但我们其实可以转为公平组合游戏,我们可以将一白一黑棋子之间的格子数看作石子数,这样他们的操作就类似移走这一堆中的石子,题目就变成
转化做完,开始想怎么来统计个数。
正着统计先手必胜不好做,那就统计先手必败吧!
考虑
显然需要一点组合数学细胞… ::::info[代码]
void init(){
c[0][0]=1;
for(int i=1;i<=10000;i++){
c[i][0]=1;
for(int j=1;j<=min(1ll*i,k);j++)c[i][j]=(c[i-1][j-1]+c[i-1][j])%mod;
}
}
int main(){
cin>>n>>k>>d;dp[0][0]=1;
init();
for(int i=0;i<13;i++){
for(int j=0;j<=n-k;j++){
for(int x=0;x*(d+1)*(1<<i)+j<=n-k&&x*(d+1)*2ll<=k;x++){
(dp[i+1][j+(1<<i)*x*(d+1)]+=dp[i][j]*c[k/2][x*(d+1)]%mod)%=mod;
}
}
}
ll ans=0;
for(int i=0;i<=n-k;i++)(ans+=dp[13][i]*c[n-i-k/2][k/2]%mod)%=mod;
cout<<(c[n][k]-ans+mod)%mod;
return 0;
}
::::
Anti-Nim (反尼姆游戏,简单提提)
例题 - P4279
与 Nim 游戏相差不大,但是是取走最后的人输。
::::info[证明与结论]
我们先考虑全为
再考虑不全为
- 只有一堆
>1 :显然先手可根据n 的奇偶来决定取空还是留一个,先手必胜。 - 两堆及以上:当异或和为
0 ,可以变为上面情况或还是有两堆以上,但异或和不为0 ;当异或和不为0 ,那一定有办法使其异或和为0 ,且还是有两堆及以上。第一种能变为第二种或必胜态,而第二种能变为第一种,所以第二种为必胜态,第一种为必败态。
所以,综合起来,我们发现:
全为一时异或和为 0 先手必胜,存在一堆不为1 时,异或和不为0 先手必胜。
例题即模板。
::::info[代码]
cin>>n;bool f=0;sum=0;
for(int i=1,x;i<=n;i++)cin>>x,sum^=x,f|=(x>1);
if((sum>0)^f)cout<<"Brother\n";
else cout<<"John\n";
::::
威佐夫博弈 (Wythoff`s game)
例题 - P2252
两堆石子,每次可以选一堆取走若干个,或从两堆中取走相同数目的石子,无法操作的人败。
::::info[结论] 当两堆石子的差乘黄金分割比等于较小堆石子时,先手必败,反之,后手必败。 ::::
::::success[证明] 我们考虑必败态是什么。
我们设第
为什么可以这样假设?
因为对于
那么,我们可以发现一个性质:若
然后,根据这个,还有一个性质,就是每个数只能出现一次,我们接下来来证明每个数都存在。
假设存在最小缺失数
必胜态的操作
-
(k-j,k+i) -
(k-j,k+i-j)
但
每个
从中取三个不同的
- 若为
(k-m,k+i) 型,则(k-m,k+i_1) 与(k-m,k+i_2) 均为必败态,较小数同为k-m ,与只出现一次矛盾。 - 若为
(k-m,k+i-m) 型,则(k-m,k+i_1-m) 与(k-m,k+i_2-m) 均为必败态,同样矛盾。
故假设不成立,不存在缺失的数
所以每个数都会且仅会出现一次。
我们若将所有的必败态按
0 0
1 2
3 5
4 7
6 10
…
我们可以证明,
代到
解出
因为其为正有理数,故
我们便可得到
得证
对于例题,是模板,直接用结论就行。
::::info[代码]
cin>>x>>y;
long double s=(sqrtl(5)+1.0)/2.0;
if(floor(abs(x-y)*s)==min(x,y))cout<<0;
else cout<<1;
::::
斐波那契博弈 (Fibonacci Nim)
例题 - P6487
一堆石子,每次至多取上一个人取的数目的两倍,先手第一次可以取任意个,但不能一次取完,无法操作的人败。
::::info[结论] 当石子数为斐波那契数时,先手必败,反之,后手必败。 ::::
::::success[证明] 尝试用归纳法(归纳法真是太好用了)
设
对于
假设
对于
由假设,对于
若先手取
故对于
那如果不是斐波那契数呢?
有一个定理,名为 齐肯多夫定理
任何正整数可以表示为若干个不连续的斐波那契数之和。
如分解
| 这样,先手就可以从分解后的最小堆入手,取完,因为不连续,所以倒数第二堆一定大于这一堆的两倍,后手不能一次取完。如此,后手对于每一堆都是必败,先手每次都能取走最后一颗。 |
|---|
例题就是典型模板。
::::info[代码]
ll n,k;cin>>n;
f[1]=f[2]=1;
for(int i=3;i<=85;i++){
f[i]=f[i-1]+f[i-2];
if(n>=f[i])k=i;
}
if(n==f[k])cout<<n;
else{
ll tn=n,ans=1e18;
for(int i=85;i>0;i--)if(tn>=f[i]&&!v[i+1])tn-=f[i],v[i]=1,ans=min(ans,f[i]);
cout<<ans;
}
::::
3. SG 函数
SG 函数可以说是解决博弈论的一大利器,其巧妙的思想基本可以覆盖所有公平组合博弈问题(某些非公平组合博弈也可以,看具体情况)。
一些定义
后继状态:当前状态进行一步后的可达状态。
SG 值:当前状态的一个值,其为后继状态的
mex 。终止状态:无后继状态,SG值为
0 。
注:
博弈与 DAG
我们可以发现,公平组合博弈一般都能化成一个 DAG,因为每个状态都有确定的后继,且为了有限步数内终止,一定无环。所以我们求 SG 便可用拓扑了。
SG 运作
首先,终止状态的 SG 值为
- 终止状态:SG 为
0 ,为P 态。 - 归纳假设:对于深度小于
d 的状态节点,SG 为0 对应P 态,SG 不为0 对应N 态。 - 对于深度为
d 的点:- 若其 SG 为
0 ,则其后继均为N 态,则当前为P 态。 - 若其 SG 不为
0 ,则其后继中有P 态,则当前为N 态。
- 若其 SG 为
这就是
但这只适用于单个游戏,对应 bash
那对于 Nim 这种多堆的相当于多个游戏揉和的怎么办?
其实,SG 还有另一个定理,即 Sprague–Grundy 定理。
总游戏的 SG 为各个独立游戏的 SG值的异或和。
对于 Nim,我们可以将每一堆视为一个独立游戏,因为堆与堆之间是互不干扰的,而异或和就与 Nim 那里一样的意思。
对于其他博弈,我们也可以看作是在做 Nim,每一个独立游戏就相当于 Nim 中的一个堆。
或者我们也可以这样理解,Nim 中石子数量决定了它后缀状态的数量,SG 中的 SG 值也决定了它后缀状态的种类,这也是为什么 Nim 可以与 SG 结合。
这便是 Nim 与 SG 的关联。
SG 值在一些题目中通常带有规律,周期。所以在一些规模较大的题我们也可以用这种规律来求 SG 值。
SG 例题
例题 abc278_g Generalized Subtraction Game
::::info[例题讲解]
对于例题,考虑
对于
对于
cin>>n>>l>>r;
if(l<r||((l^n)&1)==0){//将其分成两部分,与其做镜像
cout<<"First"<<endl;
for(int i=l;i<=r;i++){
if(((i^n)&1)==0){
cout<<(n-i)/2+1<<" "<<i<<endl;
break;
}
}
while(1){
int x,y;
cin>>x>>y;
if(x==0&&y==0)return 0;
if(x==-1&&y==-1)return 0;
cout<<n-x-y+2<<" "<<y<<endl;
}
}
for(int i=1;i<=n;i++){//计算长度为i的sg
for(int j=1,k=l;k<=i;k++,j++)vs[sg[j-1]^sg[i-k]]=1;//将区间分为三份:1-j-1,j-1-k(取走),k+1-i
for(int j=0;;j++){
if(!vs[j]){
sg[i]=j;
break;
}
}
for(int j=1,k=l;k<=i;k++,j++)vs[sg[j-1]^sg[i-k]]=0;
}
if(sg[n]==0){
cout<<"Second"<<endl;
int x,y;
cin>>x>>y;
if(x==0&&y==0)return 0;
if(x==-1&&y==-1)return 0;
for(int i=x;i<=x+y-1;i++)vs[i]=1;
}else cout<<"First"<<endl;
while(1){
cnt=0;
for(int i=1;i<=n;i++){
if(!vs[i]){
if(i==1)qj[++cnt][0]=1,qj[cnt][1]=1;
else{
if(vs[i-1])qj[++cnt][0]=1,qj[cnt][1]=i;
else qj[cnt][0]++;
}
}
}
int k1=0,f=0;
for(int i=1;i<=cnt;i++)k1^=sg[qj[i][0]];
for(int i=1;i<=cnt;i++){
int k2=k1^sg[qj[i][0]];
for(int j=1,k=l;k<=qj[i][0];j++,k++){
if((sg[j-1]^sg[qj[i][0]-k]^k2)==0){//若处理这个能使达P态
cout<<qj[i][1]+j-1<<" "<<l<<endl;
for(int p=qj[i][1]+j-1;p<=qj[i][1]+j-1+l-1;p++)vs[p]=1;
f=1;
break;
}
}
if(f)break;
}
int x,y;
cin>>x>>y;
if(x==0&&y==0)return 0;
if(x==-1&&y==-1)return 0;
for(int i=x;i<=x+y-1;i++)vs[i]=1;
}
::::
4. SG 常见场景
浅提树删边
题目大概为给一颗有根树,每次可删一条边,然后这条边所连接的子树消失,无法操作的人输。
结论为:
证明用归纳法:
-
-
- 若删了这个儿子,SG 为 $0$。 - 删除这个儿子的子树,那根据归纳法,$\{0,1,…,SG_x-1\}$ 是新树的后继 SG,那 $x$ 的 SG 后继即为 $\{1,2,…,SG_y\} 则
x 的 SG 值即为总集合的mex ,即SG_y+1 。 -
至于例题,可自行去找找,我这可以推荐一道 agc017d Game on Tree
浅提反 SG
反 SG 并不是很常见,本人也没怎么做过此类题目,所以简单了解一下就行。
其实跟 Anti-Nim 差不多,因为 Nim 与 SG 的关系就摆在这,所以 Anti-SG 与 Anti-Nim 的结论类似:
- SG 值异或和为
0 ,且所有独立游戏的 SG 值都不大于1 。- SG 值异或和不为
0 ,且存在独立游戏的 SG 值大于1 。
5. 非公平组合博弈
本版块主要通过下面练习题来引导。
对于非公平组合博弈,我们没有上面那些套路,只能靠思维碰撞。不过好像有诸如超现实数之类的方法,但过于困难,本人也不太懂,有兴趣可以去看看 2021 年集训队论文《浅谈超现实数与不平等博弈》 中的第 134 页。
我们对于此类题目,只能思考能不能将其保持平衡(即一个人取,另一个人可以通过一些操作将其状态与之前相差不多),或有没有什么特殊性质,以此来想出解法。
也有一些题,是有贪心性质的,这与其平衡性有关,如下面的 CF388C Fox and Card Game,这个题目就很好地体现了平衡。
但是,也不妨有些题,是可以用 SG 的,如下面的练习题 CF1704F Colouring Game。
当我们把题意化解后,题目的限制变成与公平组合游戏大差不差时,便可以用 SG。
6. DP + 博弈论
主要讲一下对于 DP 与博弈论结合的题目的一些思路。
DP 与博弈论结合,DP 的状态设计一般是游戏状态。
然后,根据题目限制,转移状态,考虑有哪些可以转到这个状态,这个状态又能转到哪个。有时还能用 SG 来简化状态设计,转移。
零和博弈
遇到零和博弈,一般都会用到 DP。
CF859C Pie Rules
::::info[讲解] 我们尝试以谁是先手来 DP。
那么,我们倒序 DP 是可以保证准确性,因为我们无法知道之后的先手是谁,但知道第一个先手一定是 Bob。
考虑 DP 转移,如果把当前给对方,那先手及得分不变,状态直接从
得到总转移式
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=n;i>=1;i--)sum+=a[i],dp[i]=max(dp[i+1],sum-dp[i+1]);
cout<<sum-dp[1]<<" "<<dp[1];
::::
有些时候我们会遇到求极小化极大,即最大差值。这类题的 DP 定义与区间 DP 差不多,改改转移式就行。
7. 题目汇总
样题
HDU4764
P2197
P3480
P2490
P2252
P6487
abc278_g
CF859C
练习题(难度不递增,按类型排)
P17405(神)
P2575(SG)
P6791(DP)
P2734(零)
P17128(非)
CF388C(非)
CF1704F(非)
P4576(非)
自主练习(难度很大)
P9170 [省选联考 2023] 填数游戏(非)
P3210 [HNOI2010] 取石头游戏(零)
P3179 [HAOI2015] 数组游戏(DP)
P4225 [清华集训 2017] 福若格斯(非)
P14017 [ICPC 2024 Nanjing R] 棋字井(非)
练习题讲解
P17405 【MX-X31-T1】「FAOI-R14」四元博弈
很神的一道题,思维难度不低。
::::info[讲解] 我们可以发现,每次操作都会操作到根,所以状态就与根有关。
当根的值不为
因此,根为
代码如下:
#include<bits/stdc++.h>
using namespace std;
const int N=22;
int n,a[N];
int main(){
int t;cin>>t;
while(t--){
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1,u,v;i<n;i++)cin>>u>>v;
if(a[1]==0)cout<<"Yuan\n";
else cout<<"Si\n";
}
return 0;
}
::::
P2575 高手过招
挺好的一道博弈题。
::::info[讲解] 实质上是 SG+阶梯 Nim
为什么是阶梯 Nim 呢?
因为我们能发现对于每个连续块,我们可以移动不同棋子,使其分成两个块,而我们可以看作是将这几个石子往下移。
诶,这不就是阶梯 Nim 吗!
行与行之间不干扰,可以直接异或。
#include<bits/stdc++.h>
using namespace std;
int main(){
ios::sync_with_stdio(false),cin.tie(0);
int t,n,m,v[22],sum;
cin>>t;
while(t--){
cin>>n;sum=0;
for(int i=1;i<=n;i++){
cin>>m;
memset(v,0,sizeof(v));int c=20-m+1,a=0,t=0;
//为什么加1?因为我们统计的是一个空格后的连续棋子,在左边加一个空格能避免最左边的棋子没有被统计
for(int j=1,x;j<=m;j++)cin>>x,v[x]=1;
for(int j=1;j<=20;j++){
if(!v[j]){
if((--c)&1)a^=t;
t=0;
}else t++;
}
sum^=a;
}
cout<<(sum>0?"YES":"NO")<<"\n";
}
return 0;
}
::::
P6791 [SNOI2020] 取石子
与经典模型很好的结合。
::::info[讲解]
可以发现如果不考虑
那么,我们考虑如果加了
#include<bits/stdc++.h>
using namespace std;
using ll=long long;
using ld=long double;
using i128=__int128;
using PI=pair<int,int>;
ll f[100],v[100],k,dp[100];
ll dfs(int p,bool vs,bool ls){
if(p<k)return 1;
if(!vs&&!ls&&dp[p]!=-1)return dp[p];
ll r=dfs(p-1,vs&(v[p]==0),0);
if(!ls&&(!vs||v[p]==1))r+=dfs(p-1,vs,1);
if(!ls&&!vs)dp[p]=r;
return r;
}
int main(){
ios::sync_with_stdio(0);cin.tie(0);
int t;cin>>t;
f[1]=1,f[2]=1;
for(int i=3;i<=90;i++)f[i]=f[i-1]+f[i-2];
while(t--){
ll n;cin>>k>>n;n--;//转anti
memset(v,0,sizeof(v));
for(int i=1;i<=90;i++)if(k<f[i]){k=i;break;}
ll tn=n;for(int i=90;i>0;i--)if(tn>=f[i])tn-=f[i],v[i]=1;
memset(dp,-1,sizeof(dp));
cout<<(n-dfs(90,1,0)+1)<<"\n";
}
return 0;
}
::::
P2734 [IOI 1996 / USACO3.3] 游戏 A Game
来道比较经典的零和博弈。
::::info[讲解] 题目比较简单,就是设计一个区间 DP,表示先手对于这个区间的最大收益。其实与上面的例题大差不差。
转移挺好想的,
#include<bits/stdc++.h>
using namespace std;
int n,a[102],dp[102][102],s[102];
int main(){
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i],s[i]=s[i-1]+a[i];
for(int l=1;l<=n;l++)
for(int i=1;i+l-1<=n;i++){
int j=i+l-1,sum=s[j]-s[i-1];
dp[i][j]=sum-min(dp[i+1][j],dp[i][j-1]);
}
cout<<dp[1][n]<<" "<<s[n]-dp[1][n];
return 0;
}
::::
P17128 [ICPC 2025 Shanghai R] AGI
开始进入非公平组合游戏。🙃
::::info[讲解] 说实话这题是有点难度的。
我们考虑每个数字出现次数的影响。
- 若某个数出现了偶数次,显然两者立场的不同会使其均分(这就是平衡),那影响就是确定的了。
- 若某个数出现了奇数次,就会有一给落单。落单的数的个数一定为偶数。
接下来我们考虑落单的数的影响。
- 当有两个落单的数时,若其中有先手需要的,即其与上面已计算的贡献相同,那先手是必赢的,反之,必输。
- 当大于两个时,后手完全可以取走先手需要的,所以先手必输。
#include<bits/stdc++.h>
using namespace std;
using ll=long long;
using ld=long double;
using i128=__int128;
using PI=pair<int,int>;
const int N=2e5+3;
int n,a[N<<1];
int main(){
// ios::sync_with_stdio(0);cin.tie(0);
int t;cin>>t;
while(t--){
cin>>n;
int ans=0,cnt=0;
for(int i=1;i<=2*n;i++)cin>>a[i];
sort(a+1,a+1+n+n);
vector<int>v;
for(int i=1,j=0;i<=2*n;i++){
if(a[i]!=a[i+1]||i==2*n){
int k=(i-j)/2;
if(k%2)ans^=a[i];
if((i-j)%2)cnt++,v.push_back(a[i]);
j=i;
}
}
if(cnt>=4)cout<<"Bot\n";
else if(cnt==2){
if(ans==v[0]||ans==v[1])cout<<"Menji\n";
else cout<<"Bot\n";
}else{
if(ans)cout<<"Bot\n";
else cout<<"Menji\n";
}
}
return 0;
}
::::
CF388C Fox and Card Game
这道题挺简单,也很好体现了平衡。
::::info[讲解] 首先,我们可以发现每一排数一定会均分(也就是平衡),因为两人的立场不同。
而至于多出来的数,两人可以轮流取最大。
#include<bits/stdc++.h>
using namespace std;
using ll=long long;
using ld=long double;
using i128=__int128;
using PI=pair<int,int>;
int n,a[103][103],res,s,ans;
int main(){
ios::sync_with_stdio(0);cin.tie(0);
cin>>n;
priority_queue<int>q;
for(int i=1,x;i<=n;i++){
cin>>x;
int sum=0;
for(int j=1;j<=x;j++)cin>>a[i][j],sum+=a[i][j]*(j<=x/2),s+=a[i][j];
if(x%2)q.push(a[i][(x+1)/2]);
res+=sum;
}
int f=1;
while(!q.empty())ans+=q.top()*f,f^=1,q.pop();
cout<<res+ans<<" "<<s-res-ans;
return 0;
}
::::
CF1704F Colouring Game
这道题可谓是无间道啊!
::::info[讲解] 我们可以先将字符串拆成连续相同子串,那么,每个人的最优策略就是选交界,尽可能减少对手的操作次数,这其实也是在引导我们去找红蓝交叉段。
这样,我们就只剩怎么处理红蓝交叉段了。
可以发现,现在两人就相当与在选相邻的两个格子将其涂白,两人的操作集一样了!游戏变成了公平组合!于是我们便可考虑用 SG。
SG 的使用就十分粗暴,但这里用到了其循环节,打表可以发现其拥有长度为
#include<bits/stdc++.h>
using namespace std;
using ll=long long;
using ld=long double;
using i128=__int128;
using PI=pair<int,int>;
const int N=5e5+3;
int sg[N];
bool f[103];
void init(){
sg[2]=1;
for(int i=3;i<1000;i++){
for(int j=1;j<i;j++)f[sg[j-1]^sg[i-j-1]]=1;
for(int j=0;j<100;j++)if(!f[j]){sg[i]=j;break;}
for(int j=0;j<100;j++)f[j]=0;
}
for(int i=1000;i<N;i++)sg[i]=sg[i-68];
}
int main(){
init();
ios::sync_with_stdio(0);cin.tie(0);
int t;cin>>t;
while(t--){
int n;
cin>>n;
string s;
cin>>s;
int a=count(s.begin(),s.end(),'R'),b=count(s.begin(),s.end(),'B');
if(a>b){
cout<<"Alice\n";
continue;
}
if(b>a){
cout<<"Bob\n";
continue;
}
int S=0,len=0;
for(int i=0;i<n;i++){
if(i&&s[i]==s[i-1]){
S^=sg[len];len=0;
}
len++;
}
S^=sg[len];
if(S)cout<<"Alice\n";
else cout<<"Bob\n";
}
return 0;
}
::::
P4576 [CQOI2013] 棋盘游戏
一道与搜索结合的题,难度较为简单了。
::::info[讲解]
显然,这游戏十分不公平,不是,非公平组合也没叫你不公平到这种地步啊。
我们可以发现,除非先手能直接吃,否则他必输,因为对方机动性更强。
那么,先手就只能尽量拖,后手就尽量追,于是我们可以记搜来找出答案。
#include<bits/stdc++.h>
using namespace std;
const int inf=1e9+7,M=25;
int f[2][60][M][M][M][M],n,ans;
int dfs(int x,int y,int r1,int c1,int r2,int c2){
int ans;
if(y>3*n)return inf;//游戏显然不会进行到这个步数
if(f[x][y][r1][c1][r2][c2])return f[x][y][r1][c1][r2][c2];
if(r1==r2&&c1==c2)return x?inf:0;
if(!x){
ans=0;
if(r1<n)ans=max(ans,dfs(1,y+1,r1+1,c1,r2,c2));
if(r1>1)ans=max(ans,dfs(1,y+1,r1-1,c1,r2,c2));
if(c1<n)ans=max(ans,dfs(1,y+1,r1,c1+1,r2,c2));
if(c1>1)ans=max(ans,dfs(1,y+1,r1,c1-1,r2,c2));
}else{
ans=inf;
if(r2<n)ans=min(ans,dfs(0,y+1,r1,c1,r2+1,c2));
if(r2>1)ans=min(ans,dfs(0,y+1,r1,c1,r2-1,c2));
if(c2<n)ans=min(ans,dfs(0,y+1,r1,c1,r2,c2+1));
if(c2>1)ans=min(ans,dfs(0,y+1,r1,c1,r2,c2-1));
if(r2<n-1)ans=min(ans,dfs(0,y+1,r1,c1,r2+2,c2));
if(r2>2)ans=min(ans,dfs(0,y+1,r1,c1,r2-2,c2));
if(c2<n-1)ans=min(ans,dfs(0,y+1,r1,c1,r2,c2+2));
if(c2>2)ans=min(ans,dfs(0,y+1,r1,c1,r2,c2-2));
}
return f[x][y][r1][c1][r2][c2]=++ans;
}
int main(){
int r1,c1,r2,c2;
cin>>n>>r1>>c1>>r2>>c2;
if(abs(r1-r2)+abs(c1-c2)<=1)puts("WHITE 1");
else printf("BLACK %d",dfs(0,0,r1,c1,r2,c2));
return 0;
}
::::
8. 参考
鸣谢下面几篇讲解深刻的博客。
博弈论的算法总结
自为风月马前卒的博弈论合集
Wolfycz 的浅谈算法——博弈论(从零开始的博弈论)
wjyppm1403 的博弈论半家桶-从入门到门入从
Moya_Rao 的浅谈博弈 DP
😀