CF140E New Year Garland
同机房大佬minxu讲解了这个题,使蒟蒻我受益匪浅。
再加上蒟蒻写的计数DP题不超过 0 道,所以遇到这种好题赶紧写题解加深印象。
Solution
因为每一层之间是有互相影响的,所以不能直接用组合数求解,考虑使用计数DP。
我们先处理只在一行内的彩球的方案数
设 \(g_{i,j}\) 表示有 \(i\) 个小球串起来,一共用了 \(j\) 种颜色的方案数。
因为每种颜色是等价的,所以我们可以强制给每个颜色一个编号,并使得编号小的第一次出现在前面,大的在后面。因为这样可以使得转移的时候简单一些,最后求答案的时候乘上一个阶乘即可(他们都说这叫最小表示法)。
现在我们可以写转移方程:
意思是可以在 \(i-1\) 个小球后加一个新颜色的小球或者加一个和第 \(i-1\) 个颜色不一样的小球,有 \(j-1\) 种颜色选择。
再思考行与行之间的影响
设 \(f_{i,j}\) 为前 \(i\) 行,其中第 \(i\) 行选了 \(j\) 种颜色的方案数。
如果没有题目中的那个限制,是有一个比较显然的转移方程:
意思就是在前 \(i-1\) 层可能的状态基础上,从 \(m\) 种颜色中选出 \(j\) 种来构成第 \(i\) 层
那么加上了限制,就是:
其实就是减去了一个 \(i-1\) 层和 \(i\) 层颜色完全相同的情况
此时再把一直没加上的阶乘加上就是 \(f_{i,j}=j!\cdot\left(\binom mjg_{l_i,j}\times \sum_{k=1}^{\min(l_{i-1},m)} f_{i-1,k}-g_{l_i,j}\times f_{i-1,j}\right)\)
而最后的答案是 \(ans=\sum_{i=1}^{\min(m,l_n)}f_{n,i}\)
完结撒发
等等,不会你以为这就完了吧
last but not least
-
当你敲着敲着发现:因为一个没确定的 \(p\) , \(\binom mj\) 仿佛一个毒瘤。你想起了 \(exLucas\) 这种代码可能比DP转移方程代码还长的东西。
但是我们其实不需要。
因为将 \(j!\) 扔回括号里,原式就会变成 \(f_{i,j}=A_m^j\times g_{l_i,j}\times \sum_{k=1}^{\min(l_{i-1},m)} f_{i-1,k}-j!\times g_{l_i,j}\times f_{i-1,j}\)
此时预处理 \(A_m^j\) 和 \(j!\) 即可。
-
现在你打完了整个程序,满心欢喜的交上去,会
更欢喜发现它T飞了,所以我们还得优化时间复杂度。观察方程可以发现 \(\sum_{k=1}^{\min(l_{i-1},m)}f_{i-1,k}\) 才是让你时间复杂度变成 \(O((\sum l_i)^2)\) 的罪魁祸首。优化方式是在 \(i-1\) 层记录前缀和,这样就可以做到 \(O(1)\) 转移。
-
终于,你开开心心的加上了前缀和,又交了上去,正准备和机房同学炫耀的时候,发现它又M飞了,仔细一看内存是 \(250MB\) ,所以还得优化空间复杂度。
继续看方程吧。又发现 \(g\) 数组在 \(f\) 转移的时候有大用,但是 \(f\) 只和它的前一层有关系,所以将可以将它的前一维抹去。
还有细微的小优化,因为 \(A_m^j\) 中底下的 \(m\) 是不变的,只有 \(j\) 会变,所以这也是个一维数组(●'◡'●)。
时间复杂度: \(O(\sum l_i)+O(l_i^2)\) ,空间复杂度: \(O(l_i^2+m)\)
代码
#include<cmath>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define ll long long
using namespace std;
const int L=5010,N=1e6+10;
int n,m,p;
ll A[N],l[N],f[L],fac[L],tmp[L];//tmp就是滚动数组
int g[L][L];
inline int read(){
int x=0,f=1;
char ch=getchar();
while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
while(isdigit(ch)){x=x*10+(ch^48);ch=getchar();}
return x*f;
}
inline void init(){
A[0]=1;
for(int i=1;i<=m;i++)
A[i]=A[i-1]*(m-i+1)%p;
g[0][0]=1;
for(int i=1;i<=5000;i++)
for(int j=1;j<=min(i,m);j++){
g[i][j]=(ll)g[i-1][j]*(j-1)%p+g[i-1][j-1];
g[i][j]%=p;
}
fac[0]=1;
for(int i=1;i<=5000;i++) fac[i]=fac[i-1]*i%p;
}
int main(){
n=read();m=read();p=read();
init();
for(int i=1;i<=n;i++) l[i]=read();
ll sum=1,res;
for(int i=1;i<=n;i++){
res=0;
for(int j=1;j<=l[i];j++)
f[j]=(sum*A[j]%p*g[l[i]][j]%p-g[l[i]][j]*tmp[j]%p*fac[j]%p+p)%p,
//防止减法出现负数,所以+p
res=(res+f[j])%p;
sum=res;
for(int j=1;j<=l[i-1];j++) tmp[j]=0;
for(int j=1;j<=l[i];j++) tmp[j]=f[j];
}
printf("%lld\n",sum);
return 0;
}
看到这里的小伙伴一定发现我直接把最后一层的 \(sum\) 输出了。
观察我前缀和优化时间复杂度的式子 \(\sum_{k=1}^{\min(l_{i-1},m)}f_{i-1,k}\) 和最后答案的式子 \(\sum_{i=1}^{\min(l_n,m)}f_{n,i}\) ,是不是惊奇的发现他们长得一样。
其实是因为虚无的第 \(n+1\) 层没有小球,也没有颜色,第 \(n\) 层怎么选都行^o^/