1. 程式人生 > 實用技巧 >樹上子樹顏色詢問——C++14版本

樹上子樹顏色詢問——C++14版本

有一段時間沒記錄知識類的部落格了,這篇部落格就說一下SG函式和SG定理

SG函式是用於解決博弈論公平組合遊戲Impartial Combinatorial Games,ICG)問題的一種方法。

什麼是組合遊戲

在競賽中,組合遊戲的題目一般有以下特點

  1. 題目描述一般為Alice、Bob 2人做遊戲
  2. Alice、Bob交替進行某種遊戲規定的操作,每操作一次,選手可以在有限的操作(操作必須合法)集合中任選一種。
  3. 對於遊戲的任何一種可能的局面,合法的操作集合只取決於這個局面本身,不取決於其它因素(跟選手,以前的所有操作無關)
  4. 如果當前選手無法進行合法的操作,則為負

舉個例子現在有一個數0,小明小紅2人每次可以輪流在當前數加 1~3,誰先湊到21誰就贏

這個描述就符合上面的條件:

  • 小明小紅(滿足1)
  • 每次輪流在當前數上加1~3(滿足2)
  • 當前能進行的操作只取決於這個數本身(也就是這個局面),如果這個數為20,可操作的集合為+{1},如果為12,可操作的集合為+{1,2,3}(滿足3)
  • 如果數字已經為21了,則不可能往上在加數字,可操作集合為\(Φ\),當前選手為負(滿足4)

必勝點和必敗點的概念

  • 必敗點(P點) 前一個(previous player)選手將取勝的點稱為必敗點
  • 必勝點(N點) 下一個(next player)選手將取勝的點稱為必勝點

比如現在數字已經為18了,那麼當前操作人只要給數字+3則必勝,我們就把在此位置稱為必勝點(正常操作情況下,別槓說都18偏要+2。。。。)
必勝點和必敗點的性質:


- 所有的終結點都是必敗點
- 從任何必勝點操作,至少有一種方式進入必敗點
- 無論如何操作, 從必敗點都只能進入必勝點.

Sprague-Grundy(SG)定理

遊戲和的SG函式等於各個遊戲SG函式的Nim和。這樣就可以將每一個子遊戲分而治之,從而簡化了問題。而Bouton定理就是Sprague-Grundy定理在Nim遊戲中的直接應用,因為單堆的Nim遊戲 SG函式滿足 SG(x) = x。

Nim和 : 各個數相異或的結果

SG函式

先定義mex(minimal excludant)運算,這是施加於一個集合的運算,表最小的不屬於這個集合的非負整數。例如\(mex\{0,1,2,4\}=3、mex\{2,3,5\}=0、mex\{\}=0\)


對於任意狀態 x , 定義 SG(x) = mex(S),其中 \(S\)\(x\) 後繼狀態的 \(SG\)函式值的集合。如 x 有三個後繼狀態分別為 \(SG(a),SG(b),SG(c)\) ,那麼 \(SG(x) = mex\{SG(a,SG(b),SG(c)\}\) 。這樣集合 \(S\)

的終態必然是空集,所以\(SG\) 函式的終態為 \(SG(x) = 0\) ,當且僅當x 為必敗點P時

取石子問題

有1堆n個的石子,每次只能取{ 1, 3, 4 }個石子,先取完石子者勝利,那麼各個數的SG值為多少?

SG[0]=0,f[]={1,3,4},

x=1 時,可以取走1 - f{1}個石子,剩餘{0}個,所以 SG[1] = mex{ SG[0] }= mex{0} = 1;

x=2 時,可以取走2 - f{1}個石子,剩餘{1}個,所以 SG[2] = mex{ SG[1] }= mex{1} = 0;

x=3 時,可以取走3 - f{1,3}個石子,剩餘{2,0}個,所以 SG[3] = mex{SG[2],SG[0]} = mex{0,0} =1;

x=4 時,可以取走4- f{1,3,4}個石子,剩餘{3,1,0}個,所以 SG[4] = mex{SG[3],SG[1],SG[0]} = mex{1,1,0} = 2;

x=5 時,可以取走5 - f{1,3,4}個石子,剩餘{4,2,1}個,所以SG[5] = mex{SG[4],SG[2],SG[1]} =mex{2,0,1} = 3;

以此類推…

x 0 1 2 3 4 5 6 7 8…

SG[x] 0 1 0 1 2 3 2 0 1…

由上述例項我們就可以得到SG函式值求解步驟,那麼計算1~n的SG函式值步驟如下:

1、使用 陣列f 將 可改變當前狀態 的方式記錄下來。

2、然後我們使用 另一個數組 將當前狀態x 的後繼狀態標記。

3、最後模擬mex運算,也就是我們在標記值中 搜尋 未被標記值 的最小值,將其賦值給SG(x)。

4、我們不斷的重複 2 - 3 的步驟,就完成了 計算1~n 的函式值。
模板如下:

//f[N]:可改變當前狀態的方式,N為方式的種類,f[N]要在getSG之前先預處理
//SG[]:0~n的SG函式值
//S[]:為x後繼狀態的集合
int f[N],SG[MAXN],S[MAXN];
void  getSG(int n){
    int i,j;
    memset(SG,0,sizeof(SG));
    //因為SG[0]始終等於0,所以i從1開始
    for(i = 1; i <= n; i++){
        //每一次都要將上一狀態 的 後繼集合 重置
        memset(S,0,sizeof(S));
        for(j = 0; f[j] <= i && j <= N; j++)
            S[SG[i-f[j]]] = 1;  //將後繼狀態的SG函式值進行標記
        for(j = 0;; j++) if(!S[j]){   //查詢當前後繼狀態SG值中最小的非零值
            SG[i] = j;
            break;
        }
    }
}

其實不難發現,Nim遊戲就是一個很典型的用SG定理解決的問題,因為Nim遊戲在一堆n個石子中可以取1-n個石子,所以單獨這一堆石子的SG值為\(mex(n−1,n−2,n−3,...,n−n)=n\),根據SG定理,每一堆石子總數相互異或即為答案

20.11.4 update:

另一個石頭問題

([洛谷P2148)SDOI2009]E&D

小 E 與小 W 進行一項名為 E&D 遊戲。

遊戲的規則如下:桌子上有 \(2n\) 堆石子,編號為 \(1∼2n\)。其中,為了方便起見,我們將第 \(2k-1\) 堆與第 \(2k\) 堆(\(1≤k≤n\))視為同一組。第 \(i\) 堆的石子個數用一個正整數 \(S_i\)表示。

一次分割操作指的是,從桌子上任取一堆石子,將其移走。然後分割它同一組的另一堆石子,從中取出若干個石子放在被移走的位置,組成新的一堆。操作完成後,所有堆的石子數必須保證大於 \(0\)。顯然,被分割的一堆的石子數至少要為 \(2\)。兩個人輪流進行分割操作。如果輪到某人進行操作時,所有堆的石子數均為 \(1\),則此時沒有石子可以操作,判此人輸掉比賽。

小 E 進行第一次分割。他想知道,是否存在某種策略使得他一定能戰勝小 W。因此,他求助於小 F,也就是你,請你告訴他是否存在必勝策略。例如,假設初始時桌子上有 \(4\) 堆石子,數量分別為\(1,2,3,1\)。小 E 可以選擇移走第 1 堆,然後將第 2 堆分割(只能分出 1 個石子)。接下來,小 W 只能選擇移走第 4 堆,然後將第 3 堆分割為 1 和 2。最後輪到小 E,他只能移走後兩堆中數量為 1 的一堆,將另一堆分割為 1 和 1。這樣,輪到小 W 時,所有堆的數量均為 1,則他輸掉了比賽。故小 E 存在必勝策略。

很顯然這是一個ICG,且可以看作 \(n\) 個子遊戲的組合,顯然每一組兩堆石子組成最小單位。我們直接打表列出值\(SG\)(按照定義計算,可以寫程式或手算)

這張表顯然很有規律,例如當 \(a\)\(b\) 是奇數時 \(s(a,b) = 0\) ,還有 \(sg(i,j) = sg(j,i)\),等等。還很容易發現,偶數列(或行)都是幾個幾個一組的。在此基礎上進一步觀察,可以發現第 \(2n\) 列的值與第 \(n\) 列是有關聯的。

總結成公式就是:當 \(a\) 為偶數時,

。於是可以遞迴求解,程式碼如下:

// Author : RioTian
// Time : 20/11/04
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int count(ll n) {
    int cnt = 0;
    while (n % 2 == 0) n /= 2, cnt++;
    return cnt;
}
int sg(ll a, ll b) {
    if (a % 2 && b % 2)
        return 0;
    else if (a % 2 == 0)
        return sg(a / 2, (b - 1) / 2 + 1) + 1;
    else
        return sg(b, a);
}
int main() {
    // freopen("in.txt", "r", stdin);
    ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    ll t, x, y, n;
    cin >> t;
    while (t--) {
        int res = 0;
        cin >> n;
        for (int i = 0; i < n / 2; ++i) cin >> x >> y, res ^= sg(x, y);
        cout << (res ? "YES\n" : "NO\n");
    }
}

我們並沒有嚴格地證明觀察出結論,但這就是這類題的通常做法:打表找規律。嚴格證明需要花的時間,對ACM來說太奢侈了。以下這個mex函式可能會經常用於打表:

int mex(auto v) {  // v可以是vector、set等容器
    unordered_set<int> S;
    for (auto e : v) S.insert(e);
    for (int i = 0;; ++i)
        if (S.find(i) == S.end()) return i;
}

本文是參考其他博文+自己理解,整理而來,現附上參考博文連結:
https://blog.csdn.net/luomingjun12315/article/details/45555495

https://blog.csdn.net/SM_545/article/details/77340690

https://zhuanlan.zhihu.com/p/257013159