「算法筆記」博弈論入門


一、公平組合游戲 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\)\(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
*/

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 三國游戲


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM