【洛谷5363】[SDOI2019] 移动金币(动态规划)
大致题意: 有\(n\)个格子,让你摆放\(m\)个金币。二人博弈,每次选择一个金币向左移任意格,无法移动者输。问有多少种方案使先手必胜。
阶梯\(Nim\)
阶梯\(Nim\)的基本模型,就是有\(n\)层楼梯(从\(0\sim n-1\)编号),每层楼梯上有若干石子,每次可以取任一层楼梯上任意多个石子到下一层,无法移动者输。
它的解决方法就是,去掉所有编号为偶数的楼梯,然后对剩下的这些编号为奇数的楼梯当成普通\(Nim\)来做。
原理是,如果一人移动编号为偶数的楼梯上的石子到下一层,如移动第\(2n\)层的\(a\)个石子到第\(2n-1\)层,那么无论何时对方都可以把这\(a\)个石子接着从第\(2n-1\)层移动到第\(2n-2\)层,因为\(2n-2\)层是必然存在的。
所以,移动偶数层的石子相当于是无效的,就可以直接忽略。
此题转化
对于这道题,我们可以发现,如果把每两个金币之间的空格当作一堆石子,这就是一个典型的阶梯\(Nim\)。
也就是说,若要先手必胜,就要满足奇数堆石子个数异或值不为\(0\)。
这显然不好求,所以我们可以转而求异或值为\(0\)的方案数,再用总方案数\(C_n^m\)减去它即为答案。
动态规划
考虑如何求异或值为\(0\)的方案数。
可以在二进制下逐位\(DP\)。
我们设\(f_{i,j}\)表示满足二进制下从最高位至右数第\(i\)位异或值为\(0\),剩余石子总数为\(j\)的方案数。
那么对于\(f_{i,j}\)的转移,我们可以枚举有\(k\)个奇数堆石子个数二进制下第\(i\)位为\(1\),其中因为要使异或值为\(0\),因此\(k\)为偶数。
转移方程为:
其中\(t1\)表示奇数堆的个数。
计算答案
我们可以枚举满足所有奇数堆每一位异或值为\(0\)时所剩的石子个数\(i\),这就是偶数堆的石子个数。
则用插板法就可以求出异或值为\(0\)的方案数为:
其中\(t0\)表示偶数堆的个数。
最后用\(C_n^m\)减去这一方案数就是答案。
代码
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 150000
#define M 50
#define LN 20
#define X 1000000009
#define Qinv(x) Qpow(x,X-2)
#define C(x,y) (1LL*Fac[x]*IFac[y]%X*IFac[(x)-(y)]%X)//组合数
using namespace std;
int n,m,Fac[N+5],IFac[N+5],f[LN+5][N+5];
I int Qpow(RI x,RI y) {RI t=1;W(y) y&1&&(t=1LL*t*x%X),x=1LL*x*x%X,y>>=1;return t;}
int main()
{
RI i,j,k,t0,t1,res=0;scanf("%d%d",&n,&m),t0=(m>>1)+1,t1=m+1>>1;//计算偶数堆和奇数堆个数
for(k=max(n,2*m),Fac[0]=i=1;i<=k;++i) Fac[i]=1LL*Fac[i-1]*i%X;//初始化阶乘
for(IFac[k]=Qinv(Fac[k]),i=k-1;~i;--i) IFac[i]=1LL*IFac[i+1]*(i+1)%X;//初始化阶乘逆元
for(f[LN][n-m]=1,i=LN-1;~i;--i) for(j=0;j<=n-m;++j)//动态规划
for(k=0;k<=t1&&j+(k<<i)<=n-m;k+=2) f[i][j]=(C(t1,k)*f[i+1][j+(k<<i)]+f[i][j])%X;
for(i=0;i<=n-m;++i) res=(C(i+t0-1,t0-1)*f[0][i]+res)%X;//计算异或值为0的方案数
return printf("%d",(C(n,m)-res+X)%X),0;//输出答案
}