组合数学进阶小总结

组合数学 (一点点进阶)

1.组合数相关公式

下降阶乘幂:\(n^{\underset {\_}{k}}=n\times(n-1)\times...\times (n-k+1)\)

\(C_n^k=\frac{n!}{k!(n-k)!}=\frac{n^{\underset {\_}{k}}}{k!}\) 可以用下降阶乘幂表示

组合恒等式

对称恒等式:\(C_n^m=C_{n}^{n-m}\)

吸收恒等式:\(C_r^k=\frac{r}{k}C_{r-1}^{k-1}\) ( \(r\in R\)\(k\in N^{+}\))

相伴恒等式:\((r-k)C_r^k=rC_{r-1}^k\) (\(r\in R\)\(k\in N\) )

加法公式: \(C_n^m=C_{n-1}^{m-1}+C_{n-1}^m\)

上指标求和:\(\sum_{i=m}^nC_i^m=C_{n+1}^{m+1}\)

平行恒等式:\(\sum_{i=0}^nC_{m+i}^{i}=C_{m+n+1}^n\)

上指标翻转:\(C_n^k=(-1)^kC_{n-k-1}^k\)

三项式系数恒等式:\(C_n^mC_m^k=C_{n-k}^{m-k}C_{n}^k\)

范德蒙卷积:\(\sum_{k=0}^nC_r^kC_s^{n-k}=C_{r+s}^n\)

组合数求和\(\sum_{i=0}^nC_n^i=2^n\)

2.二项式反演

恰好和至多的转换

\(f_i\)表示恰好\(k\)个的方案数,\(g_i\)表示至多\(k\)的方案数,则有 \(g_k=\sum_{i=0}^kC_k^i\times f_i\)

根据二项式反演,就有 \(f_k=\sum_{i=0}^k(-1)^{k-i}\times C_k^i\times g_i\)

恰好和至少的转换

\(f_i\)表示恰好\(k\)个的方案数,\(g_i\)表示至少\(k\)的方案数,则有 \(g_k=\sum_{i=k}^nC_i^k\times f_i\)

根据二项式反演,就有 \(f_k=\sum_{i=k}^n(-1)^{i-k}\times C_i^k\times g_i\)

3.相关题目

\(1.集合计数(bzoj2839)\)

题意:一个有N个元素的集合有\(2^N\)个不同子集(包含空集),现在要在这\(2^N\)个集合中取出若干集合(至少一个),使得它们的交集的元素个数为\(K\),求取法的方案数,答案模\(10^9+7\)

Sol:记\(f_k\)为交集的元素个数恰好为\(k\)的方案数,\(g_k\)表示交集的元素个数至少为\(k\)的方案数,那么\(f_k=\sum_{i=k}^n(-1)^{i-k}\times C_i^k\times g_i\)

因此现在只需考虑如何计算\(g_i\),不妨先从\(n\)个中选出\(i\)个,方法数为\(C_n^i\),之后还剩下\(n-i\)个元素,将他们划分为非空集合有\(2^{n-i}\)种方法,对于每一种集合划分,我们都把\(i\)个元素加到每一个集合,这样就满足要求了,然后从这\(2^{n-i}\)个集合中取出若干种组合的总方法数是:

\(C_{2^{n-i}}^1+C_{2^{n-i}}^2+...+C_{2^{n-i}}^{2^{n-i}}=2^{2^{n-i}}-1\),因此总的方法数是\(g_i=C_n^i\times (2^{2^{n-i}}-1)\)

#include <bits/stdc++.h>

constexpr int P = 1e9+7;
using i64 = long long;
// assume -P <= x < 2P
int norm(int x) {
    if (x < 0) {
        x += P;
    }
    if (x >= P) {
        x -= P;
    }
    return x;
}
template<class T>
T power(T a, int b) {
    T res = 1;
    for (; b; b /= 2, a *= a) {
        if (b % 2) {
            res *= a;
        }
    }
    return res;
}
struct Z {
};//模数封装,减少代码量就不贴了
int main()
{
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    int n,k;
    std::cin>>n>>k;
    std::vector<Z>fac(n+1),infac(n+1),g(n+1),pow2(n+1);
    fac[0]=1,pow2[0]=2;
    for(int i=1;i<=n;i++) fac[i]=fac[i-1]*i,pow2[i]=pow2[i-1]*pow2[i-1];
    infac[n]=power(fac[n],P-2);
    for(int i=n;i>=1;i--) infac[i-1]=infac[i-1]*i;
    auto C=[&](int n,int m)->Z{
        return fac[n]*power(fac[n-m],P-2)*power(fac[m],P-2);
    };
  
    Z p=2;
    for(int i=k;i<=n;i++) g[i]=C(n,i)*(pow2[n-i]-1);
    Z res;
    for(int i=k;i<=n;i++)
        if((i-k)&1) res-=C(i,k)*g[i];
        else res+=C(i,k)*g[i];
    std::cout<<res.val()<<'\n';
    return 0;
}

\(2. [Jsoi2011] 分特产\)

题意:给定\(n\)个同学和\(m\)种特产,第\(i\)种特产品有\(a_i\)件并且没有差别,问有多少种方法使得每个同学至少分的一件特产。

Sol:令\(f_i\)表示恰好\(i\)个同学没有分到特产,\(g_i\)表示至少\(i\)个同学没有分到特产。那么\(f_k=\sum_{i=k}^n(-1)^{i-k}C_i^kg_i\)

最终答案就是\(f_0\),考虑计算\(g_i\)。首先先选出\(i\)个同学让他们一定没有特产方法数是\(C_n^i\) ,然后对剩下\(n-i\)个同学分配特产,假设当前分的特产是第\(j\)种特产,那么分配的方法数有\(C_{n-i+a_j-1}^{a_j-1}\),这里使用添元素隔板法,因为是至少,所以这\(n-i\)个同学中是可以有同学没有被分配到特产的,可以插空板,所以\(g_i=C_n^i\prod_{j=1}^mC_{a_j+n-i-1}^{n-i-1}\)。综上,最终答案就是\(f_0=\sum_{i=0}^n(-1)^iC_i^0g_i\)

#include <bits/stdc++.h>

constexpr int P = 1e9+7;
using i64 = long long;
// assume -P <= x < 2P
int norm(int x) {
    if (x < 0) {
        x += P;
    }
    if (x >= P) {
        x -= P;
    }
    return x;
}
template<class T>
T power(T a, int b) {
    T res = 1;
    for (; b; b /= 2, a *= a) {
        if (b % 2) {
            res *= a;
        }
    }
    return res;
}
struct Z {
};
int main()
{
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    int n,m;
    std::cin>>n>>m;
    std::vector<int>a(m+1);
    int mx=0;
    for(int i=1;i<=m;i++) std::cin>>a[i],mx=std::max(mx,a[i]);

    std::vector<Z>fac(n+mx+1),infac(n+mx+1),g(n+1);
    fac[0]=1;
    for(int i=1;i<=n+mx;i++) fac[i]=fac[i-1]*i;
    infac[n+mx]=power(fac[n+mx],P-2);
    for(int i=n+mx;i>=1;i--) infac[i-1]=infac[i]*i;

    auto C=[&](int x,int y)->Z{
        if(x<y||y<0) return (Z)0;
        if(y==0) return (Z)1;
        return fac[x]*infac[y]*infac[x-y];
    };
  

    for(int i=0;i<=n;i++)
    {
        g[i]=C(n,i);
        for(int j=1;j<=m;j++) g[i]*=C(n-i+a[j]-1,n-i-1);
    }
    
    Z res=0;
    for(int i=0;i<=n;i++)
        if(i&1) res-=g[i];
        else res+=g[i];
    std::cout<<res.val()<<'\n';
    return 0;
}

\(3.已经没有什么好害怕的了\)

题意:有两个长度为\(n\)的序列\(A\)\(B\),问有多少种配对方法(一一配对)使得\(A_i>B_i\)的对数恰好比\(A_i<B_i\)的对数多\(k\)对。

Sol:打乱\(A\)并且打乱\(B\)后再配对其实和只打乱\(A\)后配对是一样,所以只考虑\(A\)的配对情况即可。可以发现\(n-k\)是奇数显然是无解的,因为假设\(A_i>B_i\)的对数是\(x\)\(A_i<B_i\)的对数是\(y\),从而有等式\(x-y=k\)\(x+y=n\)\(x=\frac{n+k}{2},y=\frac{n-k}{2}\)。所以只考虑\(n-k\)为偶数。

所以\(A_i>B_i\)的对数就是\(\frac{n+k}{2}\)。同样,我们设\(f_i\)\(A_i>B_i\)恰好\(i\)对,\(g_i\)\(A_i>B_i\)至少\(k\)对。\(f_{\frac{n+k}{2}}=\sum_{i=\frac{n+k}{2}}^n(-1)^{i-{\frac{n+k}{2}}}C_i^{\frac{n+k}{2}}g_i\),接下来考虑如何计算\(g_i\)即可。

先将\(A,B\)按照从小到大排好序后,设\(dp[i][j]\)表示现在到\(A_i\)并且有\(j\)\(A>B\)成功。

考虑转移,如果\(A_i\)不对\(A>B\)有贡献,那么\(dp[i][j]=dp[i-1][j]\)

如果有贡献,那么\(dp[i][j]=dp[i-1][j-1]\times (cnt[i]-(j-1))\) ,后面的\(cnt[i]\)表示\(B\)中有多少个数小于\(A_i\),由于是按照从小到大的顺序,所以若\(B_j<A_i\),那么\(B_j<A_k(k<i)\),考虑到前面已经配对了\(j-1\)个,而且前面\(j-1\)配对的可能有比\(A_i\)小的\(B\),所以还有减去\(j-1\)。所以\(g_i=(n-i)!\times dp[n][i]\),保证先有\(i\)对配对成功后剩下\(n-i\)个数随便排,即使出现配对也可以,因为是至少。

综上\(f_{\frac{n+k}{2}}=\sum_{i=\frac{n+k}{2}}^n(-1)^{i-{\frac{n+k}{2}}}C_i^{\frac{n+k}{2}}g_i=\sum_{i=\frac{n+k}{2}}^n(-1)^{i-{\frac{n+k}{2}}}C_i^{\frac{n+k}{2}}(n-i)!\times dp[n][i]\)

#include <bits/stdc++.h>

constexpr int P = 1e9+9;
using i64 = long long;
// assume -P <= x < 2P
int norm(int x) {
    if (x < 0) {
        x += P;
    }
    if (x >= P) {
        x -= P;
    }
    return x;
}
template<class T>
T power(T a, int b) {
    T res = 1;
    for (; b; b /= 2, a *= a) {
        if (b % 2) {
            res *= a;
        }
    }
    return res;
}
struct Z {
};
int main()
{
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    int n,k;
    std::cin>>n>>k;
    std::vector<int>A(n+1),B(n+1);
    for(int i=1;i<=n;i++) std::cin>>A[i];
    for(int i=1;i<=n;i++) std::cin>>B[i];
    sort(A.begin(),A.end());
    sort(B.begin(),B.end());
    if((n-k)%2)
    {
        std::cout<<0<<'\n';
    }
    else
    {
        k=(n+k)>>1;
        std::vector<Z>fac(n+1),infac(n+1);
        fac[0]=1;
        for(int i=1;i<=n;i++) fac[i]=fac[i-1]*i;
        infac[n]=power(fac[n],P-2);
        for(int i=n;i>=1;i--) infac[i-1]=infac[i]*i;
        std::vector<std::vector<Z>> dp(n+1,std::vector<Z>(n+1));
             
        for(int i=0;i<=n;i++) dp[i][0]=1;
        int ptr=0;
        for(int i=1;i<=n;i++)
        {
              while(B[ptr+1]<A[i]&&ptr<n) ptr++;
              for(int j=1;j<=n;j++)
              dp[i][j]=dp[i-1][j]+dp[i-1][j-1]*(ptr-j+1);
        }
        auto C=[&](int x,int y)->Z{
            if(x<y||y<0) return (Z)0;
            if(y==0) return (Z)1;
            return fac[x]*infac[y]*infac[x-y];
        };
      
        std::vector<Z>g(n+1);
        for(int i=k;i<=n;i++)
        g[i]=fac[n-i]*dp[n][i];
        Z res=0;
        for(int i=k;i<=n;i++)
        {
            if((i-k)&1) res-=C(i,k)*g[i];
            else res+=C(i,k)*g[i];
        }
        std::cout<<res.val()<<'\n';
    }
   
}

3.子集反演

\(f(S)\)表示恰好是集合\(S\)的答案数量,\(g(S)\)表示是\(S\)的子集的答案的数量。
于是有\(g(S)=\sum_{T \subseteq S}f(T)\)
子集反演有\(f(S)=\sum_{T \subseteq S}(-1)^{|S|-|T|}g(T)\)

\(待更\)

\(牛客暑期多校\ 2021 No.10 G Game of Death\)

题意\(n\) 个人, 每人等概率选择一个除自己以外的人开枪, 有\(p\) 的概 率打死, 问打完后 (所有人同时开枪) 还剩 \(k\) 个人的概率。答案模\(998244353\)\(n\le 3\times 10^5\)

4.Min-Max容斥

\(max(S),min(S)\)分别表示集合\(S\)的最大,最小元素,则有一下式子:

\[max(S)=\sum_{T\subseteq S}(-1)^{|T|+1}min(T)\\ min(S)=\sum_{T\subseteq S}(-1)^{|T|+1}max(T) \]

同时,在期望的意义下,这个式子任然成立,即

\[E[max(S)]=\sum_{T\subseteq S}(-1)^{|T|+1}E[min(T)]\\ E[min(S)]=\sum_{T\subseteq S}(-1)^{|T|+1}E[max(T)] \]

扩展到第\(k\)大,有

\[k_{th}max(S)=\sum_{T\subseteq S}(-1)^{|T|-k}C_{|T|-1}^{k-1} min(T)\\ E[k_{th}max(S)]=\sum_{T\subseteq S}(-1)^{|T|-k}C_{|T|-1}^{k-1} E[min(T)] \]

例题

1.HDu4436 Card Collector

题意:有\(n\)张卡牌,每秒有\(p_i\)的概率抽到卡牌\(i\),求得到每个卡牌至少一张的期望时间。

Sol:很容易发现,期望时间由最后一张被抽到的卡牌的决定,就是求每张卡牌被抽到的概率的期望时间的最大值。而\(Min-Max\)容斥的套路就是化\(max\)为为\(min\)。所以令\(max(S)\)表示集合\(S\)中最后一张卡牌被抽到的期望时间的最大值,\(min(T)\)表示第一张卡牌被抽到的时间的最小值。那么就有\(max(S)=\sum_{T\subseteq S}(-1)^{|T|+1}min(T)\)。只需要考虑如何求\(min(T)\)即可:\(T\)集合中的卡牌在第\(i\)秒被抽到的概率为\(P=\sum_{i\in T}p_i\),因为\(\sum_{i=1}^np_i=1\),所以第\(i\)秒获得的卡牌不是集合\(T\)内的就是集合\(T\)外的,那么期望时间就是\(min(T)=E(X)=\sum_{i=1}^{\infty}i(1-P)^{i-1}P=P\sum_{i=1}^{\infty}i(1-P)^{i-1}=P(-\sum_{i=0}^{\infty }(1-P)^i)^{'}=P\frac{1}{P^2}=\frac{1}{P}\)

2.ABC242 Ex

题意:有\(N\)个格子一开始都是白色,现在有\(M\)个区间\([L_i,R_i]\),每次可以等概率地从这\(M\)个区间中选择一个然后把这个区间的颜色染黑。求把所有个格子染黑的期望次数。\(1\le N,M\le 400\)

Sol:记\(E[max(S)]\)为把\(S\)中所有格子染黑需要的最多操作次数,\(E[min(T)]\)为把\(T\)中的格子至少染黑一个最少需要的操作次数。那么根据\(Min-Max\)容斥,那么答案就是\(E[max(S)]=\sum_{T\subset S}(-1)^{|T|+1}E[min(T)]\),其中\(S\)代表所有格子。那么考虑计算\(E[min(T)]\),根据\(E[min(T)]\)的定义,我们某一次把\(T\)中至少染黑一个格子的概率就是求出总共有多少条线段至少包含\(T\)中的一个格子,假设有\(k\)条,那么\(E[min(T)]=\sum_{i=1}^{\infty}i(1-\frac{k}{M})^{i-1}(\frac{k}{M})=\frac{M}{k}\),所以答案就是\(E[max(S)]=\sum_{T\subset S}(-1)^{|T|+1}\frac{k}{M}\),但\(N\)有点大,枚举所有\(T\)的话有\(2^N\)方种,考虑优化,我们可以枚举\(k\),然后计算有多少个集合\(T\)\(k\)是当前枚举的\(k\)的值,就是\(E[max(S)]=\sum_{k=1}^M\frac{M}{k}f(k)\),其中\(f(k)\)是前面提到的满足条件的集合的贡献。考虑\(dp[i][j][t]\)表示确定了前\(i-1\)个位置选或者不选,第\(i\)个一定选,覆盖了选的位置的线段有\(j\)条,选的位置的集合大小的奇偶性,这样可以枚举上一个选的位置是什么就可以实现转移。但是这样转移覆盖的线段不太好处理,因为可能存在线段覆盖多个点,所以考虑有多少条线段不覆盖集合\(T\)中的任何点,这样的集合数量其实和原来要求的是一样的。最后求答案时就是\(E[max(S)]=\sum_{k=1}^M\frac{M}{M-k}f(k)\)

#include<bits/stdc++.h>
using namespace std;
const int N=405;
constexpr int P = 998244353;
using i64 = long long;
// assume -P <= x < 2P
int norm(int x) {
    if (x < 0) {
        x += P;
    }
    if (x >= P) {
        x -= P;
    }
    return x;
}
template<class T>
T power(T a, int b) {
    T res = 1;
    for (; b; b /= 2, a *= a) {
        if (b % 2) {
            res *= a;
        }
    }
    return res;
}
struct Z {
};
Z dp[N][N][2];
int cnt[N][N];
int sum[N];
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int n,m;
    cin>>n>>m;
    for(int i=1;i<=m;i++)
    {
       int l,r;
       cin>>l>>r;
       cnt[l][r]++;
       /*
       for(int i=l;i<=r;i++)
        for(int j=i;j<=r;j++) ++cnt[i][j];
        */
    }
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++) cnt[i][j]+=cnt[i][j-1];
    dp[0][0][1]=1;
    for(int i=1;i<=n+1;i++)
    {
       int s=0;
      for(int j=i-1;j>=0;j--)
      { 
        s+=cnt[j+1][i-1];//覆盖[j+1,i-1]的线段
        for(int k=s;k<=m;k++)
        {
            for(int t=0;t<2;t++)
                  dp[i][k][t]+=dp[j][k-s][t^1];
        }
      }
    }
    Z ans=0;

    for(int i=0;i<m;i++)
    {       
         ans+=dp[n+1][i][1]*m/(m-i);
         ans-=dp[n+1][i][0]*m/(m-i);
    }
    
    cout<<ans.val()<<'\n';
    return 0;
}

5.指数型生成函数

  • 用途:相比于普通型生成函数解决组合计数的问题,指数型生成函数可以解决带有排列的组合问题。

  • 一般形式:\(e^x=\sum_{n=0}^{\infty}\frac{x^n}{n!}=1+x+\frac{x^2}{2!}+..+\frac{x^n}{n!}+...\)

  • 应用:有\(n\)种物品,已知每种物品的数量为\(k_1,k_2,...,k_n\)个,从中选出\(m\)个物品的排列数。假设第\(i\)个物品选了\(m_i\)个,那么答案很好求,就是\(\frac{n!}{\prod_{i=1}^nm_i!}\)。相应的生成函数就是\(G(x)=(1+x+\frac{x^2}{2!}+...+\frac{x^{k_1}}{k_1!})(1+x+\frac{x^2}{2!}+...+\frac{x^{k_2}}{k_2!})...(1+x+\frac{x^2}{2!}+...+\frac{x^{k_n}}{k_n!})=\sum_{i=0}^pa_i\frac{x^i}{i!}\),其中\(p=\sum_{i=1}^nk_i\),\(a_i\)为选出\(i\)种物品的方案数。

  • 例题:构造一个序列\(a_1,a_2,...,a_n\),满足\(1\le a_i\le m\),其中满足偶数出现的次数是偶数,其中\(1\le n \le 10^5,m\le 2\times 10^5\)

    Sol:设\(m\)个数中有\(a\)个奇数,\(b\)个偶数。奇数可以随便去,偶数只能去偶数个,有因为是排列,元素可重复,所以构造指数型生成函数\(G(x)=(1+x+\frac{x^2}{2!}+\frac{x^3}{3!}+...)^a(1+\frac{x^2}{2!}+\frac{x^4}{4!}+...)^b=e^{xa}(\frac{e^x+e^{-x}}{2})^b\)

    \(m\)为偶数,则\(a=b\)\(G(x)=\frac{1}{2^b}(e^{2x}+1)^b=\frac{1}{2^b}\sum_{r=0}^bC_b^r(e^{2xr})=\frac{1}{2^b}\sum_{r=0}^bC_b^r\sum_{i=0}^{\infty}\frac{(2xr)^i}{i!}\)\(ans=n!\times[x^n]G(x)=n!\times \frac{1}{2^b}\sum_{r=0}^bC_b^r\frac{(2r)^n}{n!}x^n=\frac{1}{2^b}\sum_{r=0}^bC_b^r(2r)^n\)

    \(m\)为奇数,\(a=b+1\),则\(G(x)=\frac{1}{2^b}e^{xa}\sum_{r=0}^bC_b^re^{x(b-r)}e^{-xr}=\frac{1}{2^b}\sum_{r=0}^bC_b^re^{x(a+b-r)}e^{-xr}=\frac{1}{2^b}\sum_{r=0}^bC_b^re^{x(m-2r)}=\frac{1}{2^b}\sum_{r=0}^bC_b^r\sum_{i=0}^{\infty}\frac{(m-2r)^i}{i!}x^i\),答案就是\(ans=n!\times [x^n]G(x)=\frac{1}{2^b}\sum_{r=0}^bC_b^r(m-2r)^n\)

    #include <bits/stdc++.h>
    #define int long long
    using namespace std;
    const int P=1000000007;
    const int N=2e5+10;
    int fac[N],infac[N];
    int power(int x,int y)
    {
        int res=1;
        while(y)
        {
            if(y&1) res=1LL*res*x%P;
            x=1LL*x*x%P;
            y>>=1;
        }
        return res;
    }
    int C(int x,int y)
    {
        if(x<y) return 0;
        return 1LL*fac[x]*infac[y]%P*infac[x-y]%P;
    }
    signed main()
    {
       ios::sync_with_stdio(false);
       cin.tie(nullptr);
       fac[0]=1;
       for(int i=1;i<=200000;i++)fac[i]=1LL*fac[i-1]*i%P;
        infac[200000]=power(fac[200000],P-2);
       for(int i=200000;i>=1;i--)infac[i-1]=1LL*infac[i]*i%P;
    
       int T;
       cin>>T;
       while(T--)
       {
          int n,m;
          cin>>n>>m;
          if(m&1)
          {
              int b=m/2;
              int ans=0;
              for(int r=0;r<=b;r++)
                  ans=(ans+1LL*C(b,r)*power(m-2*r,n)%P)%P;
              ans=1LL*ans*power(power(2,b),P-2)%P;
              cout<<ans<<'\n';
          }
          else
          {
            int b=m/2;
              int ans=0;
              for(int r=0;r<=b;r++)
                  ans=(ans+1LL*C(b,r)*power(2*r,n)%P)%P;
              ans=1LL*ans*power(power(2,b),P-2)%P;
              cout<<ans<<'\n';
          }
       }
    }
    

6.图上的计数问题综合多项式

\(1.[集训队作业2013]城市规划\)

题意:求\(n\)个点的简单有标号无向连通图的数量。

Sol:设\(g_n\)表示\(n\)个点的无向图的个数(不一定连通),\(f_n\)表示\(n\)个点无向连通图的个数(连通)。

\(f_n\)不容易直接计算,但\(g_n\)容易计算:\(g_n=2^{C_n^2}\),表示可以从\(n\)个点中任意选\(2\)个点,它们之间可以连边也可以不连边。

常见思路:我们枚举1号点所在的连通块大小。

\(g_n=\sum_{i=1}^nC_{n-1}^{i-1}f_ig_{n-i}\)

然后把组合数拆开\(\frac{g_n}{(n-1)!}=\sum_{i=1}^n\frac{f_i}{(i-1)!}\frac{g_{n-i}}{(n-i)!}\)

\(a_n=\frac{g_n}{(n-1)!},\) \(b_n=\frac{f_n}{(n-1)!},\) \(c_n=\frac{g_n}{n!}\) ,观察形式很想生成函数\(EGF=\sum_{i=1}^\infty \frac{f(0)}{i!}x^i\)

则令\(A(x)=\sum_{i=0}^{\infty}a_ix^i,B(x)=\sum_{i=0}^{\infty}b_ix^i,C(x)=\sum_{i=0}^{\infty}c_ix^i\)

\(A(x)=B(x)C(x)\),从而\(B(x)=A(x)C(x)^{-1}\),因为我们要求\(f_n\),所以只需求出在模\(x^{n+1}\)下的答案即可,最后答案就是\(b_n(n-1)!\)

//#pragma GCC optimize(2)
#include <bits/stdc++.h>
#define pb push_back
using namespace std;
typedef long long ll;

const int N = 5e6+10;
const int p = 998244353, gg = 3, ig = 332738118, img = 86583718;//1004535809 
const int mod =  1004535809;


template <typename T>void read(T &x)
{
    x = 0;
    register int f = 1;
    register char ch = getchar();
    while(ch < '0' || ch > '9') {if(ch == '-')f = -1;ch = getchar();}
    while(ch >= '0' && ch <= '9') {x = x * 10 + ch - '0';ch = getchar();}
    x *= f;
}

ll qpow(ll a, ll b)
{
    ll res = 1;
    while(b) {
        if(b & 1) res = 1ll * res * a % mod;
        a = 1ll * a * a % mod;
        b >>= 1;
    }
    return res;
}
namespace Poly
{
    #define mul(x, y) (1ll * x * y >= mod ? 1ll * x * y % mod : 1ll * x * y)
    #define minus(x, y) (1ll * x - y < 0 ? 1ll * x - y + mod : 1ll * x - y)
    #define plus(x, y) (1ll * x + y >= mod ? 1ll * x + y - mod : 1ll * x + y)
    #define ck(x) (x >= mod ? x - mod : x)//取模运算太慢了

    typedef vector<ll> poly;
    const int G = 3;//根据具体的模数而定,原根可不一定不一样!!!
    //一般模数的原根为 2 3 5 7 10 6
    const int inv_G = qpow(G, mod - 2);
    int RR[N], deer[2][21][N], inv[N];

    void init(const int t) {//预处理出来NTT里需要的w和wn,砍掉了一个log的时间
        for(int p = 1; p <= t; ++ p) {
            int buf1 = qpow(G, (mod - 1) / (1 << p));
            int buf0 = qpow(inv_G, (mod - 1) / (1 << p));
            deer[0][p][0] = deer[1][p][0] = 1;
            for(int i = 1; i < (1 << p); ++ i) {
                deer[0][p][i] = 1ll * deer[0][p][i - 1] * buf0 % mod;//逆
                deer[1][p][i] = 1ll * deer[1][p][i - 1] * buf1 % mod;
            }
        }
        inv[1] = 1;
        for(int i = 2; i <= (1 << t); ++ i)
            inv[i] = 1ll * inv[mod % i] * (mod - mod / i) % mod;
    }

    int NTT_init(int n) {//快速数论变换预处理
        int limit = 1, L = 0;
        while(limit <= n) limit <<= 1, L ++ ;
        for(int i = 0; i < limit; ++ i)
            RR[i] = (RR[i >> 1] >> 1) | ((i & 1) << (L - 1));
        return limit;
    }

    void NTT(poly &A, int type, int limit) {//快速数论变换
        A.resize(limit);
        for(int i = 0; i < limit; ++ i)
            if(i < RR[i])
                swap(A[i], A[RR[i]]);
        for(int mid = 2, j = 1; mid <= limit; mid <<= 1, ++ j) {
            int len = mid >> 1;
            for(int pos = 0; pos < limit; pos += mid) {
                int *wn = deer[type][j];
                for(int i = pos; i < pos + len; ++ i, ++ wn) {
                    int tmp = 1ll * (*wn) * A[i + len] % mod;
                    A[i + len] = ck(A[i] - tmp + mod);
                    A[i] = ck(A[i] + tmp);
                }
            }
        }
        if(type == 0) {
            for(int i = 0; i < limit; ++ i)
                A[i] = 1ll * A[i] * inv[limit] % mod;
        }
    }

    poly poly_mul(poly A, poly B) {//多项式乘法
        int deg = A.size() + B.size() - 1;
        int limit = NTT_init(deg);
        poly C(limit);
        NTT(A, 1, limit);
        NTT(B, 1, limit);
        for(int i = 0; i < limit; ++ i)
            C[i] = 1ll * A[i] * B[i] % mod;
        NTT(C, 0, limit);
        C.resize(deg);
        return C;
    }
    poly poly_inv(poly &f, int deg) {//多项式求逆
        if(deg == 1)
            return poly(1, qpow(f[0], mod - 2));

        poly A(f.begin(), f.begin() + deg);
        poly B = poly_inv(f, (deg + 1) >> 1);
        int limit = NTT_init(deg << 1);
        NTT(A, 1, limit), NTT(B, 1, limit);
        for(int i = 0; i < limit; ++ i)
            A[i] = B[i] * (2 - 1ll * A[i] * B[i] % mod + mod) % mod;
        NTT(A, 0, limit);
        A.resize(deg);
        return A;
    }
    //多项式牛顿迭代:求g(f(x))=0mod(x^n)中的f(x)
}

using namespace Poly;


int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    Poly::init(20);//2^21 = 2,097,152,根据题目数据多项式项数的大小自由调整,注意大小需要跟deer数组同步(21+1=22)
    int n;
    cin>>n;
    vector<ll>fac(n+1),infac(n+1);
    fac[0]=1;
    for(int i=1;i<=n;i++) fac[i]=fac[i-1]*i%mod;
    infac[n]=qpow(fac[n],mod-2);
    for(int i=n;i>=1;i--) infac[i-1]=infac[i]*i%mod;


    poly A(n+5),B(n+5),C(n+5);
    C[0]=1;
    
    for(int i=1;i<=n;i++)
    {
        C[i]=qpow(2,(1ll*i*(i-1)/2)%(mod-1))*infac[i]%mod;
        A[i]=C[i]*i%mod;
    }

    C=poly_inv(C,n);
    B=poly_mul(A,C);
    cout<<B[n]*fac[n-1]%mod<<'\n';
    return 0;
}

\(待更\)

参考链接:
二项式反演及其应用
『组合数学进阶』
P4841 [集训队作业2013]城市规划

posted @ 2022-02-14 17:46  Arashimu  阅读(419)  评论(1)    收藏  举报