「算法笔记」博弈论入门

一、公平组合游戏 ICG

1. 公平组合游戏的定义

若一个游戏满足:

  1. 游戏有两个人参与,二者轮流做出决策。
  2. 在游戏进程的任意时刻,可以执行的合法行动与轮到哪名玩家无关。
  3. 不能行动的玩家判负。

则称该游戏为一个 公平组合游戏

2. 一些说明

我们把游戏过程中面临的状态称为 局面,整局游戏第一个行动的为 先手,第二个行动的为 后手。我们讨论的博弈问题一般只考虑理想情况,即两人均无失误,都采取 最优策略 行动时游戏的结果。

定义 必胜态 为先手必胜的状态 ,必败态 为先手必败的状态 。注意,在一般确定操作状态的组合游戏中,只会存在这两种状态,如果先手和后手都足够聪明,不会出现介于必胜态和必败态之间的状态。

一个重要的性质:一个状态是必败态当且仅当它的所有后继都是必胜态。一个状态是必胜态当且仅当它至少有一个后继是必败态。特别地,没有后继状态的状态是必败态(因为无法操作则负)。

二、Nim 博弈

\(\text{Nim}\) 游戏是一个公平组合游戏。大概是这样的:

现在有 \(n\) 堆石子,第 \(i\) 堆有 \(a_i\) 个。两人轮流操作,每人每次可以从任选一堆中取走任意多个石子,但是不能不取。取走最后一个石子的人获胜(即无法再取的人就输了)。

结论:\(\text{Nim}\) 博弈先手必胜,当且仅当 \(a_1\oplus a_2\oplus \cdots \oplus a_n\neq 0\)。

证明:为了证明这个结论,我们需要证明:

  • 1. 所有石子都被取走是一个必败局面。

  • 2. 对于任意一个局面,若 \(a_1\oplus a_2\oplus \cdots \oplus a_n\neq 0\),一定 能 得到一个 \(a_1\oplus a_2\oplus \cdots \oplus a_n=0\) 的局面。

  • 3. 对于任意一个局面,若 \(a_1\oplus a_2\oplus \cdots \oplus a_n=0\),一定 不能 得到一个 \(a_1\oplus a_2\oplus \cdots \oplus a_n=0\) 的局面。

首先,所有石子都被取走是一个必败局面(对手取走最后一个石子,已经获得胜利),此时显然有  \(a_1\oplus a_2\oplus \cdots \oplus a_n=0\)。

其次,若 \(a_1\oplus a_2\oplus \cdots \oplus a_n=x\neq 0\),设 \(x\) 的二进制表示下最高位的 \(1\) 在第 \(k\) 位,那么至少存在一堆石子 \(a_i\) 的第 \(k\) 位是 \(1\)。显然 \(a_i\oplus x<a_i\),于是就可以从第 \(i\) 堆取走若干个石子,使得第 \(i\) 堆的石子数量变为 \(a_i\oplus x\),就得到了一个 \(a_1\oplus a_2\oplus \cdots \oplus a_n=0\) 的局面。

若 \(a_1\oplus a_2\oplus \cdots \oplus a_n=0\),假设可以得到一个 \(a_1\oplus a_2\oplus \cdots \oplus a_n=0\) 的局面,其中第 \(i\) 堆的 \(a_i\) 个石子被取成了 \(a_i'\)。由异或的运算可得 \(a_i'=a_i\),与“不能不取石子”矛盾。所以一定不能得到一个 \(a_1\oplus a_2\oplus \cdots \oplus a_n=0\) 的局面。

综上所述,\(a_1\oplus a_2\oplus \cdots \oplus a_n\neq 0\) 是一个必胜局面,反之必败。

//Luogu P2197
#include<bits/stdc++.h>
#define int long long
using namespace std;
int t,n,x,ans;
signed main(){
    scanf("%lld",&t);
    while(t--){
        scanf("%lld",&n),ans=0;
        for(int i=1;i<=n;i++)
            scanf("%lld",&x),ans^=x;
        puts(ans?"Yes":"No");    //各堆石子数异或起来不等于 0 则必胜,否则必败 
    }
    return 0;
} 

三、有向图游戏

给定一个有向无环图,图中有一个唯一的起点,在起点上放有一枚棋子。两人交替地移动棋子(将棋子从一个点沿有向边移动到另一个点,每次移动一步),无法移动者输。

该游戏被称为 有向图游戏

事实上,任何一个公平组合游戏都可以转化为有向图游戏。具体方法是,把局面看成图中一个节点,并且从每个局面向沿着合法行动能够到达的下一个局面连有向边。

四、SG 函数

\(S\) 表示一个非负整数集合。定义 \(\text{mex}(S)\) 为求出不属于集合 \(S\) 的最小非负整数的运算,即:

\(\text{mex}(S)=\min\limits_{x\in \mathbb{N},x\notin S}\{x\}\)

\(\text{SG}\) 函数的定义如下:最终状态(不可操作状态)的 \(\text{SG}\) 函数为 \(0\),其余状态的 \(\text{SG}\) 函数为它的后继状态的 \(\text{SG}\) 函数值构成的集合再执行 \(\text{mex}\) 运算的结果。

换一种说法,在有向图游戏中,对于每个节点 \(x\),设从 \(x\) 出发共有 \(k\) 条有向边,分别到达节点 \(y_1,y_2,\cdots,y_k\),则:

\(\text{SG}(x)=\text{mex}(\{\text{SG}(y_1),\text{SG}(y_2),\cdots,\text{SG}(y_k)\})\)

举两个栗子,一个状态有 \(2\) 个后继状态,它们的 \(\text{SG}\) 函数分别为 \(2\)\(3\),则当前状态的 \(\text{SG}\) 函数为 \(0\)\(2\) 个后继状态的 \(\text{SG}\) 函数分别为 \(0\)\(2\),则当前状态的 \(\text{SG}\) 函数为 \(1\)

\(\text{SG}\) 函数判断状态是否必胜的规则是,如果当前状态的 \(\text{SG}\) 函数为 \(0\),则当前状态必败,否则当前状态必胜。

五、有向图游戏的和

\(m\) 个有向图游戏,分别为 \(G_1,G_2,\cdots,G_m\)。定义有向图游戏 \(G\),它的行动规则是任选某个有向图游戏 \(G_i\),并在 \(G_i\) 上行动一步。\(G\) 被称为有向图游戏 \(G_1,G_2,\cdots ,G_m\) 的和。

有向图游戏的和的 \(\text{SG}\) 函数值等于它包含的各个子游戏 \(\text{SG}\) 函数值的异或和,即:

\(\text{SG}(G)=\text{SG}(G_1)\oplus \text{SG}(G_2)\oplus \cdots \oplus \text{SG}(G_m)\)

其证明方法与 \(\text{Nim}\) 博弈类似。此处略。

定理:有向图游戏的某个局面必胜,当且仅当该局面对应节点的 \(\text{SG}\) 函数值大于 \(0\)。有向图游戏某个局面必败,当且仅当该局面对应节点的 \(\text{SG}\) 函数值等于 \(0\)

可以这样理解:

  • 在一个没有出边的节点上,棋子不能移动,它的 \(\text{SG}\) 值为 \(0\),对应必败局面。

  • 若一个节点的某个后继节点 \(\text{SG}\) 值为 \(0\),在 \(\text{mex}\) 运算后,该节点的 \(\text{SG}\) 值大于 \(0\)。这等价于,若一个局面的后继局面中存在必败局面,则当前局面为必胜局面。

  • 若一个节点的后继节点 \(\text{SG}\) 值均不为 \(0\),在 \(\text{mex}\) 运算后,该节点的 \(\text{SG}\) 值为 \(0\)。这等价于,若一个局面的后继局面全部为必胜局面,则当前局面为必败局面。

六、Nim 博弈的变种

1. 阶梯 Nim

顾名思义,就是在阶梯上进行博弈。每层有若干个石子(地面表示第 \(0\) 层),每次可以从任意层的石子中取若干个移动到该层的下一层。

换一种说法:有 \(n\) 堆石子。两人轮流操作,每人每次可以从第 \(i\) 堆的石子中取若干个石子放到第 \(i-1\) 堆里(\(1<i\leq n\)),或者从第 \(1\) 堆的石子中取若干个,无法操作者负。

阶梯 \(\text{Nim}\) 经过转换可以变为 \(\text{Nim}\)

把石子从奇数堆移动到偶数堆可以理解为拿走石子。那么,如果两人都只移动奇数堆的石子,那么等价于两人在玩 \(\text{Nim}\) 游戏。

考虑有人移动偶数堆的石子到奇数堆怎么处理。先假设 \(\text{Nim}\) 游戏先手必胜,那么先手肯定优先玩 \(\text{Nim}\) 游戏。

若后者试图破坏局面,移动第 \(x\) 堆(\(x\) 为偶数)的若干个石子到 \(x-1\) 堆,那么先手就可以紧接着把他动的那些石子从第 \(x-1\) 堆继续移到第 \(x-2\) 堆上,所以第 \(x-1\) 堆(\(x-1\) 为奇数)的石子数不会有变化,且先后手关系不变,对局面没有影响。

所以阶梯 \(\text{Nim}\) 等价于是奇数堆的 \(\text{Nim}\) 博弈。因此只需考虑奇数堆的石子数异或和是否为 \(0\)(为 \(0\) 则先手必败,否则必胜)。

POJ 1704 Georgia and Bob

题目大意:\(n\) 个格子,在某些格子上有一个石子。每个格子最多只能包含一个石子。两人轮流操作,每人每次可以选择一个石子向左移动若干格,但不能越过其他石子或越过左边边缘。不能操作者负。

Solution:

把相邻两个石子之间的距离看作一堆石子中的石子个数,向左移动石子就等价于把第 \(i\) 堆石子移动到第 \(i+1\) 堆里。反一下,就转化为了阶梯 \(\text{Nim}\)。只需考虑奇数堆的石子数异或和是否为 \(0\)(为 \(0\) 则先手必败,否则必胜)。

#include<cstdio>
#include<algorithm>
#define int long long
using namespace std;
const int N=1e3+5;
int t,n,a[N],b[N],ans;
signed main(){
    scanf("%lld",&t);
    while(t--){
        scanf("%lld",&n),ans=0;
        for(int i=1;i<=n;i++)
            scanf("%lld",&a[i]);
        sort(a+1,a+1+n);
        for(int i=1;i<=n;i++)
            b[i]=a[i]-a[i-1]-1;    //把相邻两个石子之间的距离看作一堆石子中的石子个数 
        reverse(b+1,b+1+n);    //反一下转化为阶梯 Nim 
        for(int i=1;i<=n;i+=2) 
            ans^=b[i];    //计算奇数堆石子数的异或和 
        if(ans) puts("Georgia will win");    //先手必胜 
        else puts("Bob will win");    //先手必败 
    }
    return 0;
}

2. 反 Nim 博弈

同样地,反 \(\text{Nim}\) 博弈也是 \(\text{Nim}\) 博弈的一个变种。\(\text{Nim}\) 博弈是取到最后一个石子者胜,那么反 \(\text{Nim}\) 博弈就是取到最后一个石子负。其余条件不变。

即:现在有 \(n\) 堆石子,第 \(i\) 堆有 \(a_i\) 个。两人轮流操作,每人每次可以从任选一堆中取走任意多个石子,但是不能不取。取走最后一个石子的人输。

可以分为两种情况分别讨论:

  • \(n\) 堆石子的石子数均为 \(1\)

  • 至少有一堆石子数大于 \(1\)

对于第一种情况,显然:当 \(n\) 为偶数时,先手必胜,否则必败

对于第二种情况:

  • \(a_1\oplus a_2\oplus \cdots \oplus a_n\neq 0\) 时:

    • 若至少有两堆的石子数大于 \(1\),此时一定存在一种方式转化为至少有两堆的石子数大于 \(1\) 且各堆石子异或和为 \(0\) 的状态。于是变成了下一种情况(各堆石子异或和为 \(0\) 的情况),相当于是把下一种情况的局面交给后手。此时先手必胜。(见下文)

    • 若只有一堆的石子数大于 \(1\) 时:假设石子数为 \(1\) 的有 \(m\) 堆。若 \(m\) 是奇数,先手就可以将唯一的石子数大于 \(1\) 的那一堆全部取走;反之,就将这堆取到只剩下一个石子。于是就转化为了石子数均为 \(1\) 并且石子堆数为奇数的情况。算上先手的这一次操作,总操作数为偶数。所以先手必胜。

    • 因此,在这种情况下,先手必胜

  • \(a_1\oplus a_2\oplus \cdots \oplus a_n=0\) 时:这样的话至少有两堆的石子数大于 \(1\)。那么先手决策完之后,必定能得到一个 \(a_1\oplus a_2\oplus \cdots \oplus a_n\neq 0\) 的局面,这样便到了先手必胜局(上一个情况)。由于是先手决策完后到了先手必胜局,相当于是把先手必胜局交给了后手。所以当 \(\text{SG}\)\(0\) 时,先手必败

//LightOJ 1253 Misere Nim
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=110;
int t,n,a[N],k,cnt,ans;
signed main(){
    scanf("%lld",&t);
    while(t--){
        scanf("%lld",&n),ans=cnt=0;
        for(int i=1;i<=n;i++){ 
            scanf("%lld",&a[i]),ans^=a[i];
            if(a[i]>1) cnt++;
        } 
        printf("Case %lld: ",++k);
        if(!cnt) puts(n%2==0?"Alice":"Bob");    //n 堆石子的石子数均为 1 
        else puts(ans?"Alice":"Bob");    //至少有一堆石子数大于 1 
    }
    return 0;
} 

3. Nim-K 游戏

有 \(n\) 堆石子,两人轮流操作,每人每次可以从不超过 \(k\) 堆中取走任意多个石子,但是不能不取。无法再取的人败。

结论:\(n\) 堆石子的石子数用二进制表示,统计每一个二进制位上 \(1\) 的个数。若每一位上 \(1\) 的个数对 \(k+1\) 取模全为 \(0\),则先手必败,否则先手必胜。

\(\text{Nim}\) 游戏可以看做是 \(k=1\)\(\text{Nim}-K\) 游戏,因为异或就相当于把每一位 \(1\) 的个数加起来对 \(2\) 取模。

七、其他

1. 斐波那契博弈

有一堆石子(石子数 \(\geq 2\)),两人轮流操作,先手第一次可以取走任意多个石子(不能不取,也不能全部取完);接下来每个人取的石子数都不能超过上个人的两倍,但不能不取。无法操作者输。

结论:先手必败,当且仅当石子数为斐波那契数。

2. 无向图删边游戏

无向图删边游戏

八、SG 函数博弈

1. 小练习

Exercise 1

题目大意:有一堆石子,共有 \(n\) 个。两人轮流操作,每人每次可以从这堆石子里面取 \(l\sim r\) 个石子,不能操作者负。

Solution:打表找规律。打表代码:

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
int n,l,r,sg[N];
bool vis[N];
signed main(){
    scanf("%lld%lld%lld",&n,&l,&r);
    for(int i=1;i<=n;i++){
        memset(vis,0,sizeof(vis));
        for(int j=l;j<=r;j++)    //每次可以取 l~r 个 
            if(i>j) vis[sg[i-j]]=1;    //标记后继状态
        for(int j=0;j<=n;j++)
            if(!vis[j]){sg[i]=j;break;}     //mex 运算 
    }
    for(int i=1;i<=n;i++)
        printf("%lld%c",sg[i],i==n?'\n':' ');
    return 0;
} 
/*
Input: 30 3 7
Output: 0 0 0 1 1 1 2 2 2 3 0 0 0 1 1 1 2 2 2 3 0 0 0 1 1 1 2 2 2 3
*/

容易发现 \(\text{SG}(i)=\lfloor \frac{i\bmod (l+r)}{l}\rfloor\)

Exercise 2

题目大意:现在有 \(n\) 堆石子,第 \(i\) 堆有 \(a_i\) 个,两人轮流操作,每人每次可以从一堆石子中取任意多个,也可以把一堆石子分成两堆。不能操作者负。

Solution:

一堆石子变成两堆,相当于是变成了两个独立的游戏。那么这个游戏的 \(\text{SG}\) 值就是其子游戏的异或值。

所以 \(\text{SG}(i)=\text{mex}(\text{SG}(i-j),\text{SG}(i-j)\oplus \text{SG}(j))\)

\(\text{SG}(i-j)\) 是从石子中选 \(j\) 个,\(\text{SG}(i-j)\oplus \text{SG}(j)\) 是将石子分为两堆,分别有 \(i-j\) 个和 \(j\) 个石子。

然后呢?打表找规律!

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
int n,sg[N];
bool vis[N];
signed main(){
    scanf("%lld",&n);
    for(int i=1;i<=n;i++){
        memset(vis,0,sizeof(vis));
        for(int j=1;j<=i;j++) 
            vis[sg[i-j]]=1;
        for(int j=1;j<i;j++)
            vis[sg[j]^sg[i-j]]=1;    //sg[j] 和 sg[i-j] 肯定已经算出来了 
        for(int j=0;j<=n;j++)
            if(!vis[j]){sg[i]=j;break;} 
    }
    for(int i=1;i<=n;i++)
        printf("%lld%c",sg[i],i==n?'\n':' ');
    return 0;
} 
/*
Input: 30
Output: 1 2 4 3 5 6 8 7 9 10 12 11 13 14 16 15 17 18 20 19 21 22 24 23 25 26 28 27 29 30
*/

容易发现,mod 4 一个周期。

Exercise 3

题目大意:有一堆石子,共有 \(n\) 个。两人轮流操作,每人每次可以从取当前个数的因数个石子(不能是本身),不能操作者(剩下一个)负。

Solution:

\(\text{SG}(i)=\text{mex}(\text{SG}(i-j))\),其中 \(j\mid i\)

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
int n,sg[N];
bool vis[N];
signed main(){
    scanf("%lld",&n);
    for(int i=1;i<=n;i++){
        memset(vis,0,sizeof(vis));
        for(int j=1;j<i;j++) 
            if(i%j==0) vis[sg[i-j]]=1;
        for(int j=0;j<=n;j++)
            if(!vis[j]){sg[i]=j;break;} 
    }
    for(int i=1;i<=n;i++)
        printf("%lld%c",sg[i],i==n?'\n':' ');
    return 0;
} 
/*
Input: 20
Output: 0 1 0 2 0 1 0 3 0 1 0 2 0 1 0 4 0 1 0 2
*/

发现 \(\text{SG}(i)\)\(i\) 在二进制下末尾 \(0\) 的个数。

Exercise 4

题目大意:有一堆石子,共有 \(n\) 个。两人轮流操作,每人每次可以取与当前个数互质的数字个石子,不能操作者负。

Solution:

\(\text{SG}(i)=\text{mex}(\text{SG}(i-j))\),其中 \(\text{gcd}(i,j)=1\)

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
int n,sg[N];
bool vis[N];
int gcd(int x,int y){
    if(!y) return x;
    return gcd(y,x%y);
}
signed main(){
    scanf("%lld",&n);
    for(int i=1;i<=n;i++){
        memset(vis,0,sizeof(vis));
        for(int j=1;j<=i;j++) 
            if(gcd(i,j)==1) vis[sg[i-j]]=1;
        for(int j=0;j<=n;j++)
            if(!vis[j]){sg[i]=j;break;} 
    }
    for(int i=1;i<=n;i++)
        printf("%lld%c",sg[i],i==n?'\n':' ');
    return 0;
} 
/*
Input: 30
Output: 1 0 2 0 3 0 4 0 2 0 5 0 6 0 2 0 7 0 8 0 2 0 9 0 3 0 2 0 10 0
*/

发现偶数的 \(\text{SG}\) 值为 \(0\),奇数的 \(\text{SG}\) 值为它的最小质因子在质数表中的编号。特别地,\(\text{SG}(1)=1\)

Exercise 5

题目大意:有⼀张 \(1\times n\) 的纸条,两人轮流在格子里画 \(\text{X}\) ,画了连续的三个 \(\text{X}\) 者获胜。

Solution:

放了一个后,左右各延伸的两格都不能放,那么左右两边的纸条就是独立的游戏,于是 \(\text{SG}\) 值异或一下就行了(中间的是一个必胜局面,也要作为一个游戏,也就是说一共有 \(3\) 个子游戏)。

2. LOJ 10243 移棋子游戏

题目大意:给定一个 \(n\) 个节点的有向无环图,图中某些节点上有棋子。两人交替地移动棋子(将棋子从一个点沿有向边移动到另一个点,每次移动一步),无法移动者负。问先手是否必胜。

Solution:

\(\text{DFS}\) 出每个节点的 \(\text{SG}\) 值,最后整个游戏的 \(\text{SG}\) 函数值就是每一个棋子的 \(\text{SG}\) 值的异或和。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e3+5,M=6e3+5;
int n,m,k,x,y,cnt,hd[N],to[M],nxt[M],sg[N],ans;
bool v[N];
void add(int x,int y){
    to[++cnt]=y,nxt[cnt]=hd[x],hd[x]=cnt;
}
int solve(int x){
    if(v[x]) return sg[x];
    bool vis[N];
    v[x]=1,memset(vis,0,sizeof(vis));
    for(int i=hd[x];i;i=nxt[i]){
        int y=to[i];
        vis[solve(y)]=1;
    }
    for(int i=0;i<=n;i++)
        if(!vis[i]) return sg[x]=i;    //mex 运算 
}
signed main(){
    scanf("%lld%lld%lld",&n,&m,&k);
    for(int i=1;i<=m;i++)
        scanf("%lld%lld",&x,&y),add(x,y);
    for(int i=1;i<=n;i++) sg[i]=solve(i);    //计算每个节点的 SG 值 
    for(int i=1;i<=k;i++)
        scanf("%lld",&x),ans^=sg[x];    //最后整个游戏的 SG 函数值就是每一个棋子的 SG 值的异或和
    puts(ans?"win":"lose");
    return 0;
}

3. PE306 Paper-strip Game

题目大意:\(n\) 个白色方块,两人轮流操作,每人每次可以选择两个连续的白色方块并将其涂成黑色。不能操作者负。问对于所有的 \(n\) 满足 \(1\leq n\leq 10^6\),有多少个值可以使得先手必胜。

Solution:

选择两个连续的白色方块并将其涂成黑色相当于把 \(i\) 个白色方块分为了两部分(不算中间 \(2\) 个黑色方块的部分),分别有 \(j\) 个和 \(i-j-2\) 个白色方块。相当于是变成了两个独立的游戏。

所以 \(\text{SG}(i)=\text{mex}(\text{SG}(j)\oplus \text{SG}(i-j-2))\)。特别地,\(\text{SG}(1)=0,\text{SG}(2)=1\)

然后就可以打表找规律了。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
int n,sg[N];
bool vis[N];
signed main(){
    scanf("%lld",&n),sg[1]=0,sg[2]=1;
    for(int i=3;i<=n;i++){
        memset(vis,0,sizeof(vis));
        for(int j=1;j<=i-2;j++)
            vis[sg[j]^sg[i-j-2]]=1;
        for(int j=0;j<=n;j++)
            if(!vis[j]){sg[i]=j;break;} 
    }
    for(int i=1;i<=n;i++)
        printf("%lld%c",sg[i],i==n?'\n':' ');
    return 0;
} 
//Input: 1000 

拖动窗口找循环节。前两行会有问题,后面的就是循环节。循环节有 \(34\) 位。放个图(每行有 \(34\) 个数):

前两行共 \(34\times 2=68\) 个数中,\(\text{SG}\) 值大于 \(0\) 的共有 \(55\) 个。后面的每一行,即每 \(34\) 个数中,\(\text{SG}\) 值大于 \(0\) 的共有 \(29\) 个。\((1000000-68)\bmod 34=26\),循环节的前 \(26\) 个数中,\(\text{SG}\) 值大于 \(0\) 的共有 \(22\) 个,所以答案为 \(55+\lfloor\frac{1000000-68}{34}\rfloor \times 29+22=852938\)

4. POJ2311 Cutting Game

题目大意:给定 \(n\times m\) 的矩阵网格纸。两人轮流操作,每人每次可以任选一张矩阵网格纸(游戏开始时,只有一张 \(n\times m\) 的矩阵网格纸,在游戏的过程中,可能会有若干张大小不同的矩形网格纸),沿着某一行或者某一列的格线,把它剪成两部分。首先剪出 \(1\times 1\) 的玩家获胜。问先手是否必胜。

Solution:

在此题中,不能行动的局面,即 \(1\times 1\) 的纸张,是一个必胜局面。而 \(\text{ICG}\) 是以必败局面收尾的。因此,我们需要作出一些转化。

思考哪些局面是必败局面。

首先,对于任何一人,都不会剪出 \(1\times x\)\(x\times 1\) 的纸张,否则必败(因为对手就可以剪出 \(1\times 1\) 从而获胜)。其次,能够剪出 \(1\times 1\) 的方法必定要经过 \(2\times 2\)\(2\times 3\)\(3\times 2\) 三种局面之一。而在这三种局面下,先手无论如何行动,都会剪出 \(1\times x\)\(x\times 1\) 的形状。所以 \(2\times 2\)\(2\times 3\)\(3\times 2\) 是必败局面。那么我们就可以把这三者作为终止局面判负。

把这张纸剪成了两部分,相当于是变成了两个独立的游戏。与之前一样类似计算即可。

\(\text{SG}(n,m)=\text{mex}(\text{SG}(i,m) \oplus \text{SG}(n-i,m),\text{SG}(n,i)\oplus \text{SG}(n,m-i))\)

其中 \(\text{SG}(i,m) \oplus \text{SG}(n-i,m)\) 为沿着第 \(i\) 行下边的格线剪开,\(\text{SG}(n,i)\oplus \text{SG}(n,m-i)\) 为沿着第 \(i\) 列右边的格线剪开。

#include<cstdio>
#include<cstring>
#define int long long
using namespace std;
const int N=210;
int n,m,sg[N][N];
bool v[N][N],vis[N];
int solve(int x,int y){
    if(v[x][y]) return sg[x][y];
    bool vis[N];
    v[x][y]=1,memset(vis,0,sizeof(vis));
    for(int i=2;i<=x-i;i++)
        vis[solve(i,y)^solve(x-i,y)]=1;
    for(int i=2;i<=y-i;i++)
        vis[solve(x,i)^solve(x,y-i)]=1;
    for(int i=0;i<=200;i++)
        if(!vis[i]) return sg[x][y]=i;
}
signed main(){
    sg[2][2]=sg[2][3]=sg[3][2]=0,v[2][2]=v[2][3]=v[3][2]=1;    //2*2,2*3,3*2 的局面已确定为是必败局面 
    while(~scanf("%lld%lld",&n,&m)) puts(solve(n,m)?"WIN":"LOSE");
    return 0;
}

5. Luogu P1290 欧几里德的游戏

题目大意:给定两个正整数 \(m\)\(n\),两人轮流操作,每人每次可以选择其中较大的数,减去较小数的正整数倍,要求得到的数不能小于 \(0\)。得到了 \(0\) 者胜。

Solution:

暴力 \(\text{SG}\)\(\text{SG}(m,n)=\text{mex}(\text{SG}(m,n-m),\text{SG}(m,n-2m),\cdots,\text{SG}(m,n\bmod m))\)。特别地,\(\text{SG}(m,0)=0\)

想办法简化计算过程。我们发现:

  • \(\text{SG}(m,n)=\text{mex}(\text{SG}(m,n-m),\text{SG}(m,n-2m),\cdots,\text{SG}(m,n\bmod m))\)

  • \(\text{SG}(m,n-m)=\text{mex}(\text{SG}(m,n-2m),\cdots,\text{SG}(m,n\bmod m))\)

因此,\(\text{SG}(m,n)\) 可以由 \(\text{SG}(m,n-m)\) 推导。容易得出,\(\text{SG}(m,n\bmod m+m)=\text{mex}(\text{SG}(m,n\bmod m))\)

  • \(\text{SG}(m,n\bmod m)=0\),则 \(\text{SG}(m,n\bmod m+m)=1,\text{SG}(m,n\bmod m+2m)=2,\text{SG}(m,n\bmod m+3m)=3\cdots\) 依次类推,直到 \(\text{SG}(m,n)\)。此时为必胜局。

  • \(\text{SG}(m,n\bmod m)\neq 0\),则 \(\text{SG}(m,n\bmod m+m)=0,\text{SG}(m,n\bmod m+2m)=1,\text{SG}(m,n\bmod m+3m)=2\cdots\) 依次类推,直到 \(\text{SG}(m,n)\)。此时视 \(n-m=n\bmod m\) 的情况而定。

因此,只需计算 \(\text{SG}(m,n\bmod m)\) 再加以讨论即可。

#include<bits/stdc++.h>
#define int long long
using namespace std;
int t,n,m;
bool solve(int m,int n){    //m<n
    if(!m) return 0;
    if(solve(n%m,m)==0) return 1;    //当 SG(m,n%m)=0 时,为必胜局面。由于 n%m<m,所以这里写成 SG(n%m,m)。 
    return n-m==n%m?0:1;    //当 SG(m,n%m)!=0 时,若 n=n%m+m,也就是 n-m=n%m 时,SG 值为 0;否则大于 0。 
}
signed main(){
    scanf("%lld",&t);
    while(t--){
        scanf("%lld%lld",&m,&n);
        if(m>n) swap(n,m);
        puts(solve(m,n)?"Stan wins":"Ollie wins");
    }
    return 0;
}

6. Luogu P2148「SDOI 2009」E&D

题目大意:\(2n\) 堆石子,编号为 \(1\sim 2n\)。将第 \(2k-1\) 堆与第 \(2k\) 堆 (\(1\leq k\leq n\))视为同一组。 一次分割操作指的是,任取一堆石子,将其移走,然后分割它同一组的另一堆石子,从中取出若干个石子放在被移走的位置,组成新的一堆。操作完成后,所有堆的石子数必须保证大于 \(0\)。两人轮流进行分割操作,无法操作者负。

Solution:

一组数看作一个 \(\text{ICG}\)。对于单个游戏 \((a,b)\),可以转移到 \((c,d)\),满足 \(c+d=a\)\(c+d=b\)

我们只关心每一个 \(\text{ICG}\) 中的 \(\text{mex}(\{\text{SG}(c,d)\mid c+d=a\},\{\text{SG}(c,d)\mid c+d=b\})\)。因此对于每一个 \(a\),考虑所有 \(c+d=a\)\((c,d)\)\(\text{SG}\) 值的取值集合。

打个表:(用二进制表示。可以用 \(\text{bitset}\) 存)

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=10,M=15;
int n;
bitset<N>s[M];
int mex(bitset<N>b){    //mex 操作 
    int cnt=0;
    while(b[cnt]) cnt++;
    return cnt;
} 
signed main(){
    scanf("%lld",&n);
    for(int i=2;i<=n;i++)    //a
        for(int j=1;j<=n&&i-j>=1;j++)    //c=i,d=i-j 
            s[i].set(mex(s[j]|s[i-j])); 
    for(int i=1;i<=n;i++)
        printf("%lld: ",i),cout<<s[i],printf("%c",i%5?' ':'\n');
    return 0; 
}
/*
Input: 10
Output:
1: 0000000000 2: 0000000001 3: 0000000010 4: 0000000011 5: 0000000100
6: 0000000101 7: 0000000110 8: 0000000111 9: 0000001000 10: 0000001001
*/ 

发现,关于 \(a\)\(\text{SG}\) 集合即为 \(a-1\) 的二进制表示中,值为 \(1\) 的位(\(s_i\) 等于 \(i-1\) 的二进制表示)。

比如 \(\{\text{SG}(c,d)\mid c+d=6\}=\{0,2\}\),则 \(\text{mex}(\{\text{SG}(c,d)\mid c+d=6\})=1\)(即二进制下最低位的 \(0\) 的位置)。

然后回到一个 \(\text{ICG}\) 游戏 \((a,b)\) 的初始局面。

\(\{\text{SG}(c,d)\mid c+d=a\}=(a-1)_2,\{\text{SG}(c,d)\mid c+d=b\}=(b-1)_2\)

\(\text{SG}(a,b)=\text{mex}(\{\text{SG}(c,d)\mid c+d=a\},\{\text{SG}(c,d)\mid c+d=b\})\)
\(=\text{mex}(\{(a-1)_2\},\{(b-1)_2\})=\text{mex}(\{((a-1)\ \text{or} \ (b-1))_2\})\)

利用上述结论,我们取 \((a-1)\ \text{or} \ (b-1)\) 在二进制表示下最低位的 \(0\) 的位置即为 \(\text{SG}(a,b)\)

最后异或一下就行了。

#include<bits/stdc++.h>
#define int long long
using namespace std;
int t,n,ans,a,b;
int mex(int x){    //求 x 二进制表示下最低位的 0 的位置 
    int cnt=0;
    while(x&1) x>>=1,cnt++;
    return cnt; 
}
signed main(){
    scanf("%lld",&t);
    while(t--){
        scanf("%lld",&n),ans=0;
        for(int i=1;i<=n;i+=2){
            scanf("%lld%lld",&a,&b);
            ans^=mex((a-1)|(b-1));    //SG(a,b)=mex((a-1)|(b-1))
        }
        puts(ans?"YES":"NO");
    }
    return 0;
}

九、博弈 DP 

1. AGC002E Candy Piles

题目大意:\(n\) 堆糖果,第 \(i\) 堆有 \(a_i\) 个。两人轮流操作,每人每次可以进行一下两个操作中的一个:

  • 1. 选择剩余糖果数量最多的一堆,然后吃掉该堆中的所有糖果。

  • 2. 从剩下的还有一个或多个糖果的每个堆中,吃一个糖果。

吃完最后一个糖果者负。问先手必胜还是后手必胜。\(1\leq n\leq 10^5,1\leq a_i\leq 10^9\)

Solution:

放在二维方格上表示。如图所示,按 \(a_i\) 从大到小排序,那么对于吃掉糖果数量最多的一堆,实际上就是消去最左边一行;对于取走每堆吃一个,实际上就是消去最下面一行。 可以看作,初始在位置 \((1,1)\),每次往右走或往上走一步。当走到边界时,所有糖果刚好被吃完。

考虑 \(\text{DP}\)。令 \(f_{i,j}\) 表示走到 \((i,j)\) 的胜负情况。

首先,若 \((i,j)\) 为边界,那它一定是必败态。其次,若 \((i,j)\) 的上面和右边都是必败态,那么当前的 \((i,j)\) 对于当前的执行者就是必败态;反之,当任意一个不是必败态时,对于当前的执行者就是必胜态。最后状态取反。

\(f_{i,j}=\neg(f_{i+1,j}\vee f_{i,j+1})\)。其中,\(\neg\) 是逻辑非,\(\vee\) 是逻辑或。

由于是从 \((1,1)\) 出发,我们要知道 \((1,1)\) 的胜负情况。若 \((1,1)\) 为必败态,则先手必胜,否则后手必胜。

显然直接这样做是不能通过这道题的。考虑打表找规律。打表代码:

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e3+5;
int n,a[N],f[N][N],vis[N][N],ans[N][N],mx;
bool cmp(int x,int y){
    return x>y; 
}
int dfs(int x,int y){
    if(~f[x][y]) return f[x][y];
    if(!vis[x+1][y]||!vis[x][y+1]||!vis[x+1][y+1]) return 0;    //边界情况 
    return f[x][y]=!(dfs(x+1,y)|dfs(x,y+1));    //转移 
}
signed main(){
    scanf("%lld",&n);
    for(int i=1;i<=n;i++)
        scanf("%lld",&a[i]),mx=max(mx,a[i]);
    sort(a+1,a+1+n,cmp);    //从大到小排序 
    memset(f,-1,sizeof(f));
    for(int i=1;i<=n;i++)
        for(int j=1;j<=a[i];j++) vis[j][i]=1;    //构造网格图,标记位置 
    for(int i=mx;i>=1;i--,puts(""))    //
        for(int j=1;j<=n;j++)    //
            if(vis[i][j]) printf("%lld",dfs(i,j));    //输出走到 (i,j) 的胜负情况 
    return 0;
}
/*
Input:
10
8 8 8 8 7 5 5 5 3 3
*/

容易发现,除了边界外,同一对角线上的点胜负情况相同。

所以 \((1,1)\) 的胜负状态等同于 \((i,i)\)。那么,若我们知道了 \((i,i)\) 的胜负情况,就知道了 \((1,1)\) 的胜负情况。

于是我们可以通过求 \(i\) 最大的 \((i,i)\) 的胜负状态,来求出 \((1,1)\) 的胜负状态。

观察一下打表代码的输出,可以发现,只要判一下 \(i\) 最大的 \((i,i)\) 向上/向右到边界的距离的奇偶性就可以知道 \((i,i)\) 的胜负状态。若其中一个方向的距离为奇数,则 \((i,i)\) 为必败态,否则为必胜态。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
int n,a[N],k1,k2;
bool cmp(int x,int y){
    return x>y;
}
signed main(){
    scanf("%lld",&n);
    for(int i=1;i<=n;i++)
        scanf("%lld",&a[i]);
    sort(a+1,a+1+n,cmp);
    for(int i=1;i<=n;i++)
        if(i+1>a[i+1]){    //找到 i 最大的 (i,i)
            k1=a[i]-i,k2=0;
            for(int j=i+1;j<=n;j++)
                if(a[j]==i) k2++;
            break;
        }
    if(k1&1||k2&1) puts("First");
    else puts("Second");
    return 0;
}

十、习题

  • SPOJ 11414 COT3 - Combat on a tree
  • Luogu P1199 三国游戏
posted @ 2020-09-24 15:40  maoyiting  阅读(822)  评论(0编辑  收藏  举报