• 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} imes sum_{k=1}^{min(l_{i-1},m)} f_{i-1,k} ]

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

    那么加上了限制,就是:

    [f_{i,j}=inom mjg_{l_i,j} imes sum_{k=1}^{min(l_{i-1},m)} f_{i-1,k}-g_{l_i,j} imes f_{i-1,j} ]

    其实就是减去了一个 (i-1) 层和 (i) 层颜色完全相同的情况

    此时再把一直没加上的阶乘加上就是 (f_{i,j}=j!cdotleft(inom mjg_{l_i,j} imes sum_{k=1}^{min(l_{i-1},m)} f_{i-1,k}-g_{l_i,j} imes f_{i-1,j} ight))

    而最后的答案是 (ans=sum_{i=1}^{min(m,l_n)}f_{n,i})

    完结撒发

    等等,不会你以为这就完了吧

    last but not least

    1. 当你敲着敲着发现:因为一个没确定的 (p)(inom mj) 仿佛一个毒瘤。你想起了 (exLucas) 这种代码可能比DP转移方程代码还长的东西。

      但是我们其实不需要。

      因为将 (j!) 扔回括号里,原式就会变成 (f_{i,j}=A_m^j imes g_{l_i,j} imes sum_{k=1}^{min(l_{i-1},m)} f_{i-1,k}-j! imes g_{l_i,j} imes 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
    ",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^/

  • 相关阅读:
    shapely and geos break在distance方法
    linux运维
    【未完待补充】linux 设置So动态库链接路径
    用python建立最简单的web服务器
    nginx + keepalived 双机热备
    nagios监控linux主机监控内存脚本
    linux普通用户权限设置为超级用户权限方法、sudo不用登陆密码
    zato server启动后自动关闭问题解决
    Linux下几种文件传输命令 sz rz sftp scp
    python风味之大杂烩
  • 原文地址:https://www.cnblogs.com/jasony/p/13843247.html
Copyright © 2020-2023  润新知