兩個需要求 sg 函式的樹上博弈問題

maple276發表於2024-08-10

這是兩道切樹的博弈題,方向正好相反,第一道是切子樹,第二道是切向上到根的鏈。

[AGC017D] Game on Tree

題意:給你一棵1為根的樹,Alice 和 Bob 輪流操作,每次切掉一個子樹(不能以1為根),切到只剩根1一個點結束,誰會獲勝?

這題洛谷上有 remote judge 渠道:AGC017D


Solution:

我們計算每棵樹的 sg 值,注意到不能把自己整個切掉,頂多切連向兒子的邊。可以視作每個兒子是一個獨立的子問題,而多個子問題並行博弈,就是把所有子問題的 sg 值異或起來。

這個子問題 sg 值是什麼呢,並不是兒子那棵子樹的 sg 值,因為現在我可以把兒子的這個子樹整個切掉了,但我們每個子樹計算的 sg 值是不能把自己全切掉的。

聰明的你會發現,這個子問題相比兒子的 sg 值,其實就是多了一個操作空間:把兒子整個切掉。那這就相當於我往兒子這棵子樹根上再掛一個父親,然後這個新樹的 sg 值就是子問題的 sg 值了。

“注意到” 一個結論,就是任意一棵樹,我往根上再加一個父親結點,形成的新樹 sg 值等於原來的 sg 值 +1。

那這就好做了,子問題 sg 就是子樹 sg+1。那麼下面這一個式子就能把所有子樹的 sg 值算出來了:($\bigoplus $ 是異或和)

\[sg[fa] = \bigoplus (sg[son]+1) \]

這個結論是怎麼 “注意到” 的呢?

我們的注意力肯定不是憑空產生的,我們從最簡單的情況開始推:

只有一個點,sg 值為 0;兩個點,我可以切掉兒子子樹,取 mex 得 sg 為 1;

用數學歸納的思想,假設在樹點數較少時,加一個父親結點會使 sg 值 +1 這個結論成立。那麼對於任意一棵樹,我再加一個父親結點形成新樹,新樹有兩種操作,一是把兒子整個切掉,剩下一個點 sg=0;二是切兒子內的子樹,就是模仿兒子 sg 值的計算過程,切完的剩餘部分 sg 值本應該分佈於 \([0,sg[son]-1]\), 但是現在根部都多了一個父親結點,根據我們的歸納,在點數少時這個剩餘部分的新 sg 值應該是原 sg 值 +1,因此新值分佈於 \([1,sg[son]]\)。兩種操作取 mex ,新樹的 sg 值為 sg[son]+1,所以歸納成功。

#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
#define FOR() int le=e[u].size();for(int i=0;i<le;i++)
#define QWQ cout<<"QwQ\n";
#define ll long long
#include <vector>
#include <queue>
#include <map>

using namespace std;
const int N=105050;
const int qwq=N*23;
const int inf=0x3f3f3f3f;

inline int read() {
    int sum = 0, ff = 1; char c = getchar();
    while(c<'0' || c>'9') { if(c=='-') ff = -1; c = getchar(); }
    while(c>='0'&&c<='9') { sum = sum * 10 + c - '0'; c = getchar(); }
    return sum * ff;
}

int n;
int sg[N];
vector <int> e[N];

void DFS(int u,int fa) {
    for(int v : e[u]) {
        if(v==fa) continue;
        DFS(v,u);
        sg[u] ^= (sg[v]+1);
    }
}

int main() {
    int x,y;
    n = read();
    for(int i=1;i<n;i++) {
        x = read(); y = read();
        e[x].push_back(y);
        e[y].push_back(x);
    }
    DFS(1,1);
    if(sg[1]) cout<<"Alice";
    else      cout<<"Bob";
    return 0;
}

這題程式碼倒是很短很簡單,但是式子的證明還是有點思考價值的。

[SPOJ11414] COT3 - Combat on a tree

題意:給你一棵黑白樹,給出每個點初始顏色,Alice 和 Bob 輪流操作,每次選擇一個白點,把它所有祖先和它自己都塗黑,問:Alice 先手第一步塗哪些點可以獲勝?

這題洛谷上也有 remote judge 渠道:SP11414


Solution:

發現和上一題一樣,每個子樹的 sg 值是固定的,只要這棵子樹裡的所有點還沒有被操作。

一次操作是選擇樹內的一個點,將該點到根的鏈塗黑,也就相當於砍掉這條鏈,畫圖會發現這樣做會切出來很多子樹,變成一個森林,每棵切出來的子樹問題獨立,sg 值取異或,算出森林的 sg 值,將所有操作形成的森林求 mex 就是原來那棵樹的 sg 值了。

最終要求先手操作哪些點可獲勝,也就是以1為根的樹裡,哪些點切出來的森林 sg=0,說明後手操作此森林必敗。

可惜我們發現求每一棵樹的 sg 值,都需要遍歷這棵樹的所有操作,然後暴力算 mex,這兩步每一個都是 n^2 的複雜度。

但是求 sg 值竟然是可以最佳化。

在我們求完某個子樹的 sg 值後,那些操作森林的 sg 值先不要清空,而是可以批次繼承給父親。我們發現計算它父親的 sg 值時,選取子樹內同樣的點來操作,切出來的森林與兒子切出來的森林相比只是多出來了它所有兄弟子樹。那麼兒子內所有操作的森林 sg 值,異或上它所有兄弟的 sg 值,就是父親點所有操作的 sg 值了。

然後對於父親結點,它要合併所有來自兒子子樹內的操作,以及它本身(如果是它自己就是白點的話,操作後森林為所有兒子子樹)。因此我們有一個森林 sg 值合併的需求。

當我們把父親結點所有操作合併起來,也就是得到了所有操作的森林 sg 值,求出它們的 mex 即為父親這棵樹的 sg 值。

整體異或,合併,求mex值,我們發現可以用一個01trie全部搞定。

不知道是不是經典問題,反正完全能夠應用於這道樹形博弈題上,給人的感覺還是很妙的。至少我在寫完 n^2 的暴力之後沒想到求 sg 值的過程還能資料結構最佳化。

#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
#define FOR() int le=e[u].size();for(int i=0;i<le;i++)
#define QWQ cout<<"QwQ\n";
#define ll long long
#include <vector>
#include <queue>
#include <map>

using namespace std;
const int N=105050;
const int qwq=N*23;
const int inf=0x3f3f3f3f;

inline int read() {
    int sum = 0, ff = 1; char c = getchar();
    while(c<'0' || c>'9') { if(c=='-') ff = -1; c = getchar(); }
    while(c>='0'&&c<='9') { sum = sum * 10 + c - '0'; c = getchar(); }
    return sum * ff;
}

int n;
int a[N];
int sg[N];
vector <int> e[N];
int sons[N];
int ans[N],cnt;
int tot,ch[qwq][2],rt[N];
int tag[qwq];
int siz[qwq];

inline void pushdown(int now,int k) {
    if((tag[now]>>(k-1))&1) swap(ch[now][0],ch[now][1]);
    if(ch[now][0]) tag[ch[now][0]] ^= tag[now];
    if(ch[now][1]) tag[ch[now][1]] ^= tag[now];
    tag[now] = 0;
}

int merge(int r1,int r2,int k) {
    if(!r1 || !r2) return r1 + r2;
    if(k==0) { siz[r1] |= siz[r2]; return r1; }
    pushdown(r1,k);
    pushdown(r2,k);
    ch[r1][0] = merge(ch[r1][0],ch[r2][0],k-1);
    ch[r1][1] = merge(ch[r1][1],ch[r2][1],k-1);
    siz[r1] = siz[ch[r1][0]] + siz[ch[r1][1]];
    return r1;
}

int ask(int now) {
    int res = 0;
    for(int k=20;k>=0;k--) {
        pushdown(now,k);
        if(siz[ch[now][0]]!=(1<<k)) now = ch[now][0];
        else res += (1<<k), now = ch[now][1];
    }
    return res;
}

void DFS(int u,int fa) {
    for(int v : e[u]) {
        if(v==fa) continue;
        DFS(v,u);
        sons[u] ^= sg[v];
    }
    if(!a[u]) {
        int now = rt[u];
        siz[now]++;
        for(int k=20;k>=0;k--) {
            pushdown(now,k);
            int c = (sons[u]>>k)&1;
            if(!ch[now][c]) ch[now][c] = ++tot;
            now = ch[now][c];
            siz[now]++;
        }
    }
    for(int v : e[u]) {
        if(v==fa) continue;
        tag[rt[v]] ^= sons[u]^sg[v];
        rt[u] = merge(rt[u],rt[v],21);
    }
    sg[u] = ask(rt[u]);
    // cout<<"sg["<<u<<"] = "<<sg[u]<<endl;
}

void calc(int u,int fa,int val) {
    if(!a[u] && !val) ans[++cnt] = u;
    for(int v : e[u]) {
        if(v==fa) continue;
        calc(v,u,val^sg[v]^sons[v]);
    }
}

int main() {
    int x,y;
    n = read(); tot = n;
    for(int i=1;i<=n;i++) a[i] = read(), rt[i] = i;
    for(int i=1;i<n;i++) {
        x = read(); y = read();
        e[x].push_back(y);
        e[y].push_back(x);
    }
    DFS(1,1);
    calc(1,1,sons[1]);
    if(!cnt) {
        cout<<"-1";
        return 0;
    }
    sort(ans+1,ans+cnt+1);
    for(int i=1;i<=cnt;i++) cout<<ans[i]<<"\n";
    return 0;
}

相關文章