[JZOJ5728]简单计数||

题目描述

这里写图片描述
这里写图片描述

解题思路

乍一看不是很会。
先考虑不是环怎么做。
考虑分类地计数,即把方案归到某一类型里,再分别计算每一个类型的数量来求答案。
最终一种方案肯定有若干段相同颜色段,我们可以直接考虑每一种颜色的划分贡献,然后再算出他们组合起来的方案数。
具体地,我们计算出f(i,j)表示把i个相同的球分成j段的贡献和,一种方案贡献为每段大小乘积。然后,我们把同颜色的i个球缩成j个,然后问题就变成要计算有多少种不同的排列方案,使得相同颜色的球不相邻,设为F
设a[i]表示颜色i分成的段数,那么答案就是 a[1..n]if(c[i],a[i])F(a[1..n]) ∑ a [ 1.. n ] ∏ i f ( c [ i ] , a [ i ] ) ∗ F ( a [ 1.. n ] ) ,其中F就是排列方案。
考虑F怎么求,我们可以做容斥。对于一种a[1..n],考虑枚举一些相邻的同颜色球再缩起来,设剩下每种球个数b[1..n],这些球可以乱放,那么此时容斥贡献是 (b[i])!Cb[i]1a[i]1(1)a[i]b[i]1b[i]! ( ∑ b [ i ] ) ! ∗ ∏ C a [ i ] − 1 b [ i ] − 1 ∗ ( − 1 ) a [ i ] − b [ i ] ∗ 1 b [ i ] ! 。这个容斥我们可以DP地计算,每次枚举a[i]和b[i]即可,我们还可以在中途把 f(c[i],a[i]) f ( c [ i ] , a [ i ] ) 也算进去,这样是没有问题的。设F[i][j]表示做到颜色i, k=1..ib[k]=j ∑ k = 1.. i b [ k ] = j 的贡献和,最后再乘 j! j !
这样不是环的做法就出来了,转移的时候优化一下。

现在考虑环怎么办。我们可以用最小表示法,固定颜色1在开头,且1不能结尾。算出所有方案再乘 c[i] ∑ c [ i ] 。固定开头我们只需要让最后的 j! j ! 变成 (j1)! ( j − 1 ) ! ,而枚举b[1]的时候乘 1(b[1]1)! 1 ( b [ 1 ] − 1 ) ! 。固定结尾类似。不过这样有个问题,如果颜色1分成k段,dp完乘 c ∑ c 的时候,这种方案会被算k次,我们在开始的时候除即可。
然后就做完了。

实现的时候没有减掉开头和结尾都是1的方案,不知道为什么减掉错了,估计是已经容斥掉了吧…

代码

#include<cstdio> 
#include<algorithm>
#include<cstring>
#include<cmath>
#include<map>
#include<bitset>
//开 O2!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 
using namespace std;
#define fo(i,j,k) for(i=j;i<=k;i++)
#define fd(i,j,k) for(i=j;i>=k;i--)
#define cmax(a,b) (a=(a>b)?a:b)
#define cmin(a,b) (a=(a<b)?a:b)
typedef long long ll;
typedef double db;
const int N=5000+5,mo=1e9+7;
int fac[N],rev[N],f[105][105],F[55][N],sum[N],c[N],A,B,i,j,k,ans,re[N],n,trs[N];
int ksm(int x,int y)
{
    if (x==-1) return (y%2)?-1:1;
    int ret=1;
    while (y)
    {
        if (y&1) ret=1ll*ret*x%mo;
        y>>=1;
        x=1ll*x*x%mo;
    }
    return ret;
}
void predo(int n)
{
    fac[0]=1;
    fo(i,1,n) fac[i]=1ll*fac[i-1]*i%mo;
    rev[n]=ksm(fac[n],mo-2);
    fd(i,n,1) rev[i-1]=1ll*rev[i]*i%mo;
    re[0]=1;
    fo(i,1,n) re[i]=1ll*rev[i]*fac[i-1]%mo;
}
int C(int m,int n)
{
    return 1ll*fac[m]*rev[n]%mo*rev[m-n]%mo;
}
int main()
{
    freopen("number.in","r",stdin);
    //freopen("number.out","w",stdout);
    scanf("%d",&n);
    fo(i,1,n) 
    {
        scanf("%d",c+i);
        sum[i]=sum[i-1]+c[i];
    }
    predo(5e3);
    f[0][0]=1;
    fo(i,1,100)
        fo(j,1,i)
            fo(k,1,i)
                f[i][j]=(f[i][j]+1ll*f[i-k][j-1]*k)%mo;
    F[0][0]=1;
    fo(i,0,n)
    {
        fo(B,1,c[i+1])
        {
            trs[B]=0;
            fo(A,B,c[i+1])
            {
                trs[B]=(trs[B]+1ll*f[c[i+1]][A]*C(A-1,B-1)%mo*ksm(-1,A-B)*rev[B])%mo;
            }
        }
        fo(j,0,sum[i])
            fo(B,1,c[i+1])
                F[i+1][j+B]=(F[i+1][j+B]+1ll*F[i][j]*trs[B])%mo;
    }
    fo(j,0,sum[n])
        ans=(ans+1ll*F[n][j]*fac[j-1])%mo;
    printf("%d\n",1ll*ans*sum[n]%mo);
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值