【WC2017模拟1.22】简单题

题目大意

给定n,k,求把n!拆分成k个不同的正整数的乘积的方案数。(一种方案的排列仍是一种方案)。答案对 109+7 取模。

n≤10000 k≤30

时限为4s

分析

这是一道容斥好题。

首先可以不管算重,最终答案除以 k! 即可。

接下来考虑如何容斥。k个数互不相同,其实就相当于 k(k1)2 个限制条件。如果一个方案不满足x个条件,那么它要乘上的容斥系数为 (1)x

如果直接枚举不满足哪些,显然会超时。不妨转换一下:已知条件是形如(x,y)的,表示第x个数≠第y个数。把k个数看成k个点,限制(x,y)看成连接x,y的边。那么一个选择方案相当于选择一些边,使其变成许多个联通块。

好了,现在可以尝试枚举每个联通块的大小。这就相当于对k的整数拆分。为了避免算重,当前枚举的这一个数必须不小于前一个。跑出来,30能有5000多种拆法。那么整数拆分的方案数就可以接受了。接下来到下一步。

现在已经确定了k的一个整数拆分方案(即若干个联通块大小)。假设有m个联通块,第i个大小为 si 。它对答案的贡献是多少?这要考虑三个东西:
1. 把k个点分配到这些联通块的方案数。
2. 对应的分配边的所有方案对答案的贡献
3. 拆分 n! ,满足形成这些联通块的方案数。
它对答案的贡献就是三个数的乘积。现在一个个来考虑。

首先是第1部分

这个比较简单。首先总的排列数有 k! 个。设在当前的整数拆分方案中,数字i出现了 pi 次,即 ipi=k 。那么对于拆出来的每个联通块 si ,这表示有 si 个数相同,就要除掉 si! ,接下来由于i出现了 pi 次,又要除掉 pi! 。最终得到:

Ans1=k!ki=1((i!)aiai!)

第2部分

对于每个联通块分别考虑。因为一条边的意义是两个数相同,所以一个联通块里的所有数都是相等的。那就可以直接考虑容斥系数的总和了。
S(n) 表示n个点的联通图的集合, e(G)G 。对于第i个联通块(大小为 si ),它对答案的贡献就是 GS(si)(1)e(G)
g(n) 等于n个点的联通图对答案的贡献。这个可以考虑用容斥计算,即全集减去不合法方案数
首先考虑不合法的。其实就是n个点的不联通图。它形成了至少2个联通块。枚举编号为1的点所在的联通块大小,那么:

g(n)=f(n)i=1n1Cin1f(ni)g(i)

其中 f(n) 表示n个点的 全集大小。

考虑 f(n) 的值。显然f(1)=1。当n>1,就会有 n(n1)2 条可能出现的边。如果其中一条边出现了,容斥系数就乘-1,否则乘1。所有方案加起来后发现,当n>1时,f(n)=0!
上面的式子变成:

g(n)={0(n1)g(n1),1,n>1n=1

那么一个整数拆分方案第2部分的贡献为
Ans2=i=1mg(si)

最后是第3部分

首先给n!分解质因数。每个质因子都是相对独立的,就可以分开考虑。
对于一个质因子,设它共出现了 cnt 次,现在要把它分配到 m 个联通块里。一个联通块里的数要满足相同,每个质因子出现个数也一定要相同。所以对于si,它要在cnt份里占掉si的倍数份(可以是0)。这就是个完全背包问题了。

优化

为了提高运行效率,可能需要一些优化。
1. 递归对k进行拆分,然后做背包时相当于有m个物品。回溯的时候,前m-1个物品的答案要保留,不用每个方案都重新做一次dp
2. 假设k当前剩下x没有拆分,先枚举到x/2,再直接整个拆分
3. 适当去掉一些模运算

我的程序极限数据跑了3s左右

#include 
#include 
#include 

#define Min(a,b) ((a)<(b)?(a):(b))

using namespace std;

const int N=10005,M=31,mo=1e9+7,NN=100000;

typedef long long LL;

int n,m,Fac[N],Inv[N],F_Inv[N],g[N],tot,p[N],f[M][NN],cnt[N],st[N],sum,ans;

bool bz[N];

void init()
{
    scanf("%d%d",&n,&m);
    for (int i=2;i<=n;i++)
    {
        if (!bz[i]) p[tot++]=i;
        for (int j=0;j*p[j]<=n;j++)
        {
            bz[i*p[j]]=1;
            if (i%p[j]==0) break;
        }
    }
    for (int i=2;i<=n;i++)
        for (int j=0,x=i;jfor (;x%p[j]==0;x/=p[j]) cnt[j]++;
    for (int i=1;i<=tot;i++) st[i]=st[i-1]+cnt[i-1]+1;
    Fac[0]=F_Inv[0]=Fac[1]=Inv[1]=F_Inv[1]=1;
    for (int i=2;i<=n;i++)
    {
        Fac[i]=(LL)Fac[i-1]*i%mo;
        Inv[i]=(LL)Inv[mo%i]*(mo-mo/i)%mo;
        F_Inv[i]=(LL)F_Inv[i-1]*Inv[i]%mo;
    }
}

void dfs(int x,int y,int m,int sum,int t)
{
    if (m==0)
    {
        for (int i=0;i*f[x-1][st[i+1]-1]%mo;
        ans=(ans+sum)%mo;
        return;
    }
    if (y>m) return;
    for (int i=0;ifor (int j=st[i];jy;j++) f[x][j]=f[x-1][j];
        for (int j=st[i]+y;j1];j++) f[x][j]=(f[x-1][j]+f[x][j-y])%mo;
    }
    dfs(x+1,y,m-y,(LL)sum*F_Inv[y]%mo*Inv[t+1]%mo*g[y]%mo,t+1);
    if (y==m) return;
    for (int k=y+1;k*2<=m;k++)
    {
        for (int i=0;ix][st[i]+k-1]=f[x-1][st[i]+k-1];
            for (int j=st[i]+k;j1];j++) f[x][j]=(f[x-1][j]+f[x][j-k])%mo;
        }
        dfs(x+1,k,m-k,(LL)sum*F_Inv[k]%mo*g[k]%mo,1);
    }
    for (int i=0;ifor (int j=st[i];jm;j++) f[x][j]=f[x-1][j];
        for (int j=st[i]+m;j1];j++) f[x][j]=(f[x-1][j]+f[x][j-m])%mo;
    }
    dfs(x+1,m,0,(LL)sum*F_Inv[m]%mo*g[m]%mo,1);
}

void work()
{
    g[1]=1;
    for (int i=2;i<=m;i++) g[i]=(-(LL)(i-1)*g[i-1])%mo;
    ans=0;
    for (int i=0;i0][st[i]]=1;
    dfs(1,1,m,Fac[m],0);
    ans=(LL)(ans+mo)%mo*F_Inv[m]%mo;
    printf("%d\n",ans);
}

int main()
{
    //freopen("jdt.in","r",stdin); freopen("jdt.out","w",stdout);
    init();
    work();
    fclose(stdin); fclose(stdout);
    return 0;
}

你可能感兴趣的:(容斥原理,组合数)