[luogu5363] [SDOI2019R2 d2t2] 移動金幣 - 博弈 - 階梯nim - 組合計數 - 數位dp

傳送門:https://www.luogu.org/problemnew/show/P5363

題目大意:1×n的棋盤上有m個棋子,兩個人輪流操作,每次可以將一枚棋子向左移動,不能跨過前面的棋子,也不能與其他棋子重疊,不能操作者輸。給定n,m,求有多少種局面先手必勝。

說實話看到這個題的我是懵逼的,喵喵喵?這不是原題嗎?我團隊裏還有這個題的說。再仔細一看,喵喵喵?數據範圍這麼小?原題n<=1e18,m<=8000啊。

印象中sd二輪放原題的畫風應該是數據範圍+9個0纔對啊(比如反迴文串),然而這波是什麼操作?難道是我的打開方式有問題?難道有什麼不爲人知的坑?不管了,先寫一發再說……然後20分鐘就a了……

閒話少說進入正題。

有一個經典的階梯nim模型:

有n堆石子排成一排,兩人輪流操作,每次可以從某一堆石子中取出一些,放進前一堆石子中,不能操作者輸,問誰必勝。

結論非常簡單:只需忽略掉所有下標爲偶數的石子堆(假設下標從0開始),把所有奇數位置的石子拿出來,當成普通的nim遊戲即可。

證明非常簡單:如果先手去操作偶數堆的石子,後手可以完全模仿先手的動作,即將先手剛移動的那些石子再往前移動一步。凡是這種“對方可以完全模仿”的操作,在博弈論中我們通常都可以直接忽略掉。

而操作奇數堆的石子,就是將一些石子變成“可以忽略掉”的狀態,這與直接拿走等價。

而nim遊戲的結論是衆所周知的:只需將所有石子的數量xor起來,爲0則先手必敗,否則先手必勝。

而這個題呢?只需要將兩個棋子之間的空格當成石子,每次移動就相當於把一些石子移動到右側的一堆中。這就是一個從右往左的階梯nim遊戲!

換句話說,我們只需要從右往左把所有奇數位置的空格數量xor起來判斷是否爲0可。

由此一來我們就能想到一些方法去計數。不妨統計所有先手必敗的狀態,再用總狀態數c(n,m)減去即可。

一個暴力dp的思路是設f(i,j,k)表示從右往左考慮了前i個格子,已經放了j個棋子且最後一個格子一定放棋子,已經形成的空隙的xor是k的方案數。

轉移時直接枚舉下一個棋子放在哪裏即可。

由於常數小,這樣就可以獲得50分的好成績了。

如何更進一步?

注意到xor有一個優秀的性質:每一位是獨立的。因此我們不妨從高到低逐位考慮每個數該填什麼。

我們有m+1個數,要讓它們的總和爲n-m。

記g(i,j)表示從高到低考慮到了前i位,當前所有數的和是j的方案數。

枚舉這一位填多少個1,要求所有奇數位置的空格必須填偶數個1,偶數位置的空格任意。可以預處理出hi表示某一位填i個1的方案數。

這樣複雜度是mnlogn,對於這道題來說已經足夠了。

當然這題其實還有複雜度更優秀的算法:

其實記錄所有數的和這一維也是沒必要的,dp時也會發現其中有大量狀態是無用的。

我們從進位的角度考慮。

比如考慮n的最高位,這一位顯然是1,此時我們考慮所有的數是否有在這一位填1的?如果有,就正常地做下去;否則,這一位的1該從何而來?當然是從下一位進位而來!

於是我們記f(i,j)表示從高到低考慮到第i位,當前還需要向更高位進位j位(也就是所有數後i位的和應該是n的後i位+j×(1<<i))的方案數。

注意到這個j最大是m,否則後面的位即使填到最大也進不動位了。

轉移時枚舉下一位填多少個1,同樣可以預處理轉移係數。

這樣複雜度就變成了m^2logn,最後一步用ntt優化(好吧,mtt,畢竟原題和這道題模數都不是ntt模數)即可做到mlogmlogn。

上代碼(這裏的代碼是mnlogn的):

#include<bits/stdc++.h>
using namespace std;
#define gc getchar()
#define pc putchar
#define li long long
inline li read(){
    li x = 0,y = 0,c = gc;
    while(!isdigit(c)) y = c,c = gc;
    while(isdigit(c)) x = (x << 1) + (x << 3) + (c ^ '0'),c = gc;
    return y == '-' ? -x : x;
}
inline void print(li q){
    if(q < 0) pc('-'),q = -q;
    if(q >= 10) print(q / 10);
    pc(q % 10 + '0');
}
int n,m;
const int mo = 1000000009;
inline li ksm(li q,li w){
    li as = 1;
    while(w){
        if(w & 1) as = as * q % mo;
        q = q * q % mo;
        w >>= 1;
    }
    return as;
}
li jc[160010],nj[160010],f[20][160010],tp[100];
inline li c(int q,int w){
    return w < 0 || w > q ? 0 : jc[q] * nj[w] % mo * nj[q - w] % mo;
}
inline li wk(int n,int m){
    register int i,j,k;
    int p1 = m >> 1,p0 = m - p1;
    for(i = 0;i <= m;++i){
        for(j = 0;j <= i;j += 2) (tp[i] += c(p1,j) * c(p0,i - j)) %= mo;
    }
    f[18][0] = 1;
    for(i = 18;i;--i){
        for(j = 0;j <= n;++j) if(f[i][j]){
            for(k = 0;k <= m && (j + k * (1 << i - 1) <= n);++k) (f[i - 1][j + k * (1 << i - 1)] += f[i][j] * tp[k]) %= mo;
        }
    }
    return f[0][n];
}
int main(){
    int i;
    n = read();m = read();
    if(n <= m){
        pc('0');pc('\n');return 0;
    }
    jc[0] = 1;for(i = 1;i <= n + m;++i) jc[i] = jc[i - 1] * i % mo;
    nj[n + m] = ksm(jc[n + m],mo - 2);
    for(i = n + m - 1;i >= 0;--i) nj[i] = nj[i + 1] * (i + 1) % mo;
    print((c(n,m) - wk(n - m,m + 1) + mo) % mo);
    return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章