zoukankan      html  css  js  c++  java
  • 「算法笔记」博弈论

    一、公平组合游戏 ICG

    1. 公平组合游戏的定义

    若一个游戏满足:

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

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

    2. 一些说明

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

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

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

    二、Nim 博弈

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

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

    结论:( ext{Nim}) 博弈先手必胜,当且仅当 (a_1oplus a_2oplus cdots oplus a_n eq 0)。

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

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

    • 2. 对于任意一个局面,若 (a_1oplus a_2oplus cdots oplus a_n eq 0),一定 能 得到一个 (a_1oplus a_2oplus cdots oplus a_n=0) 的局面。

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

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

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

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

    综上所述,(a_1oplus a_2oplus cdots oplus a_n eq 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) 表示一个非负整数集合。定义 ( ext{mex}(S)) 为求出不属于集合 (S) 的最小非负整数的运算,即:

    ( ext{mex}(S)=minlimits_{xin mathbb{N},x otin S}{x})

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

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

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

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

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

    五、有向图游戏的和

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

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

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

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

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

    可以这样理解:

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

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

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

    六、Nim 博弈的变种

    1. 阶梯 Nim

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

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

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

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

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

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

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

    POJ 1704 Georgia and Bob

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

    Solution:

    把相邻两个石子之间的距离看作一堆石子中的石子个数,向左移动石子就等价于把第 (i) 堆石子移动到第 (i+1) 堆里。反一下,就转化为了阶梯 ( ext{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 博弈

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

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

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

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

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

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

    对于第二种情况:

    • (a_1oplus a_2oplus cdots oplus a_n eq 0) 时:

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

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

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

    • (a_1oplus a_2oplus cdots oplus a_n=0) 时:这样的话至少有两堆的石子数大于 (1)。那么先手决策完之后,必定能得到一个 (a_1oplus a_2oplus cdots oplus a_n eq 0) 的局面,这样便到了先手必胜局(上一个情况)。由于是先手决策完后到了先手必胜局,相当于是把先手必胜局交给了后手。所以当 ( ext{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),则先手必败,否则先手必胜。

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

    七、其他

    1. 斐波那契博弈

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

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

    2. 无向图删边游戏

    无向图删边游戏

    八、SG 函数博弈

    1. 小练习

    Exercise 1

    题目大意:有一堆石子,共有 (n) 个。两人轮流操作,每人每次可以从这堆石子里面取 (lsim 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?'
    ':' ');
        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
    */

    容易发现 ( ext{SG}(i)=lfloor frac{imod (l+r)}{l} floor)

    Exercise 2

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

    Solution:

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

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

    ( ext{SG}(i-j)) 是从石子中选 (j) 个,( ext{SG}(i-j)oplus ext{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?'
    ':' ');
        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
    */

    Exercise 3

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

    Solution:

    ( ext{SG}(i)= ext{mex}( ext{SG}(i-j))),其中 (jmid 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?'
    ':' ');
        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
    */

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

    Exercise 4

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

    Solution:

    ( ext{SG}(i)= ext{mex}( ext{SG}(i-j))),其中 ( ext{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?'
    ':' ');
        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
    */

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

    Exercise 5

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

    Solution:

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

    2. LOJ 10243 移棋子游戏

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

    Solution:

    ( ext{DFS}) 出每个节点的 ( ext{SG}) 值,最后整个游戏的 ( ext{SG}) 函数值就是每一个棋子的 ( ext{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) 满足 (1leq nleq 10^6),有多少个值可以使得先手必胜。

    Solution:

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

    所以 ( ext{SG}(i)= ext{mex}( ext{SG}(j)oplus ext{SG}(i-j-2)))。特别地,( ext{SG}(1)=0, ext{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?'
    ':' ');
        return 0;
    } 
    //Input: 1000 

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

    前两行共 (34 imes 2=68) 个数中,( ext{SG}) 值大于 (0) 的共有 (55) 个。后面的每一行,即每 (34) 个数中,( ext{SG}) 值大于 (0) 的共有 (29) 个。((1000000-68)mod 34=26),循环节的前 (26) 个数中,( ext{SG}) 值大于 (0) 的共有 (22) 个,所以答案为 (55+lfloorfrac{1000000-68}{34} floor imes 29+22=852938)

    4. POJ2311 Cutting Game

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

    Solution:

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

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

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

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

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

    其中 ( ext{SG}(i,m) oplus ext{SG}(n-i,m)) 为沿着第 (i) 行下边的格线剪开,( ext{SG}(n,i)oplus ext{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:

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

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

    • ( ext{SG}(m,n)= ext{mex}( ext{SG}(m,n-m), ext{SG}(m,n-2m),cdots, ext{SG}(m,nmod m)))

    • ( ext{SG}(m,n-m)= ext{mex}( ext{SG}(m,n-2m),cdots, ext{SG}(m,nmod m)))

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

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

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

    因此,只需计算 ( ext{SG}(m,nmod 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) 堆石子,编号为 (1sim 2n)。将第 (2k-1) 堆与第 (2k) 堆 ((1leq kleq n))视为同一组。 一次分割操作指的是,任取一堆石子,将其移走,然后分割它同一组的另一堆石子,从中取出若干个石子放在被移走的位置,组成新的一堆。操作完成后,所有堆的石子数必须保证大于 (0)。两人轮流进行分割操作,无法操作者负。

    Solution:

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

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

    打个表:(用二进制表示。可以用 ( ext{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?' ':'
    ');
        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)( ext{SG}) 集合即为 (a-1) 的二进制表示中,值为 (1) 的位((s_i) 等于 (i-1) 的二进制表示)。

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

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

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

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

    利用上述结论,我们取 ((a-1) ext{or} (b-1)) 在二进制表示下最低位的 (0) 的位置即为 ( ext{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. 从剩下的还有一个或多个糖果的每个堆中,吃一个糖果。

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

    Solution:

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

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

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

    (f_{i,j}= eg(f_{i+1,j}vee f_{i,j+1}))。其中,( eg) 是逻辑非,(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 三国游戏
  • 相关阅读:
    pymysql模块
    爬虫之requests请求库
    URI&URL
    C#在数据层过滤属性中的主键
    利用反射将Datatable、SqlDataReader转换成List模型
    反射+泛型+缓存 ASP.NET的数据层通用类
    Excel 2007 批量删除隐藏的文本框[转]
    JavaScript基础--DOM对象(十三):(windows对象:historylocation avigatorscreenevent)
    JavaScript基础--DOM对象加强篇(十四)
    JavaScript基础--小案例:在网页指定位置弹出错误信息(十二)
  • 原文地址:https://www.cnblogs.com/maoyiting/p/13684525.html
Copyright © 2011-2022 走看看