CF140E New Year Garland

同机房大佬minxu讲解了这个题,使蒟蒻我受益匪浅。

再加上蒟蒻写的计数DP题不超过 0 道,所以遇到这种好题赶紧写题解加深印象。

Solution

因为每一层之间是有互相影响的,所以不能直接用组合数求解,考虑使用计数DP。

我们先处理只在一行内的彩球的方案数

\(g_{i,j}\) 表示有 \(i\) 个小球串起来,一共用了 \(j\) 种颜色的方案数。

因为每种颜色是等价的,所以我们可以强制给每个颜色一个编号,并使得编号小的第一次出现在前面,大的在后面。因为这样可以使得转移的时候简单一些,最后求答案的时候乘上一个阶乘即可(他们都说这叫最小表示法)。

现在我们可以写转移方程:

\[g_{i,j}=g_{i-1,j-1}+(j-1)\cdot g_{i-1,j} \]

意思是可以在 \(i-1\) 个小球后加一个新颜色的小球或者加一个和第 \(i-1\) 个颜色不一样的小球,有 \(j-1\) 种颜色选择。

再思考行与行之间的影响

\(f_{i,j}\) 为前 \(i\) 行,其中第 \(i\) 行选了 \(j\) 种颜色的方案数。

如果没有题目中的那个限制,是有一个比较显然的转移方程:

\[f_{i,j}=\dbinom mjg_{l_i,j}\times \sum_{k=1}^{\min(l_{i-1},m)} f_{i-1,k} \]

意思就是在前 \(i-1\) 层可能的状态基础上,从 \(m\) 种颜色中选出 \(j\) 种来构成第 \(i\)

那么加上了限制,就是:

\[f_{i,j}=\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} \]

其实就是减去了一个 \(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

  1. 当你敲着敲着发现:因为一个没确定的 \(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!\) 即可。

  2. 现在你打完了整个程序,满心欢喜的交上去,会更欢喜发现它T飞了,所以我们还得优化时间复杂度。

    观察方程可以发现 \(\sum_{k=1}^{\min(l_{i-1},m)}f_{i-1,k}\) 才是让你时间复杂度变成 \(O((\sum l_i)^2)\) 的罪魁祸首。优化方式是在 \(i-1\) 层记录前缀和,这样就可以做到 \(O(1)\) 转移。

  3. 终于,你开开心心的加上了前缀和,又交了上去,正准备和机房同学炫耀的时候,发现它又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^/

posted @ 2020-10-19 21:30  jasony_sam  阅读(102)  评论(0编辑  收藏  举报