[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;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值