多项式泛做2

多项式泛做2

卷积变换技巧

目标:将两个相乘的下标之和变为定值

  • \(1.\) 若存在\(\sum_{i=0}^n f[i]g[i]\)这样的式子

    \(f^{'}[i]=f[n-i]\),则\(f^{'}[n-i]=f[i]\),也就是把\(f[]\)翻转

    ​于是:\(\sum_{i=0}^n f[i]g[i]=\sum_{i=0}^nf^{'}[n-i]g[i]\),下标和为n为定值,可以利用\(FFT\)加速卷积

  • \(2.\)计算\(a[i]-b[j]\),转换为多项式中\(x^{a[i]-b[j]}\),把\(-b[j]\)变为\(-b[j]+M(M=max\{b\})\),保证指数为正

  • \(3.\)计算\(a[1]b[x],a[2]b[x+1],a[3][x+2]...a[n][x-1]\)这样循环的式子,

    \(a[n+1]=a[1],a[n+2]=a[2],...,a[2n]=a[n]\)

    \(b^{'}[1]=b[n],b^{'}[2]=b[n-1],...,b^{'}[n]=b[1]\)

    则指数从\(n+1\)\(2n\)的所有系数即为所有这样循环式子的结果。

  • \(4.\)计算\(k\)阶前缀或差分

    \(A=a_1+a_2x+..+a_n^{n-1}\),\(B=1+x+x^2...+x^\infty=\frac{1}{1-x}\)

    前缀:$c_k=\sum_{i=0}^ka_i=\sum_{i+j=k}a_ib_j(b_j=1) $

    \(1\)阶前缀:\(A*B\) \(k\)阶前缀\(A*B^k=A*\frac{1}{(1-x)^k}\)

    \(B^k\)\(i\)项的贡献为\(C_{k+i-1}^i\)

    后缀:$c_k\sum_{i+j=k}a_ib_j(b_0=1,b_{i>0}=-1) $

    \(i\)项贡献为\(C_{k}^i*(-1)^i\)

AT2064 [AGC005F] Many Easy Problems(NTT)

Problem

给定一棵无根树,定义 \(f(i)\),对于所有大小为 \(i\) 的点集,求出能够包含它的最小连通块大小之和。对于 \(i=1 \to n\) 的所有 \(i\),求出 \(f(i)\),答案对\(924844033\)取模。(\(1\le N\le 200000\))

Sol

考虑对于一个点\(u\),若移除这个点,就可以将原树分成若干个连通块,现在考虑点\(u\)对大小为\(k\)的连通块的贡献。点\(u\)包含在大小为\(k\)的连通块里,当且仅当其他\(k-1\)个点存在至少\(2\)个在不同的连通块中,若\(k\)个点都在\(u\)分出的某一个连通块中,则点\(u\)肯定不会对这样的连通块有贡献。总结一下就是大小为\(k\)的点集不包含\(u\)当且仅当所有\(k\)个点都在以\(u\)为根的一个子树中。根据容斥,那么\(f(k)=\sum_{u=1}^n(C_n^k-\sum_{v\in son_u}C_{sz_v}^k)=n\times C_n^k-\sum_{u=1}^n\sum_{v\in son_u}C_{sz_v}^k=n\times C_n^k-\sum_{j=k}^nC_j^k\sum_{v\in son_u}[sz_v==j]\)。设\(g(k)=\sum_{j=k}^nC_j^k\sum_{v\in son_u}[sz_v==j]=\sum_{j=k}^n\frac{j!}{k!(j-k)!}\sum_{v\in son_u}[sz_v==j]\),令\(A_i=i!\sum_{v\in son_u}[sz_v==i]\)\(B_i=\frac{1}{i!}\),那么\(g(k)=\frac{1}{k!}\sum_{i=k}^nA_iB_{i-k}\),经典的变换技巧。

//大坑,这题的模数不是998244353,是924844033,这个原根是5
#include <bits/stdc++.h>
#define ll long long
#define pb push_back
using namespace std;

const int N = 1100000;
const int p = 924844033, gg = 3, ig = 332738118, img = 86583718;//998244353 1004535809  
//924844033=>gg=5
const int M=18e5;
const int mod=924844033;
ll qpow(ll a, int 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{
  
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    //freopen("gen.in","r",stdin);
    Poly::init(20);
    ll n;
    cin>>n;

    vector<ll>fac(n+1),infac(n+1);
    fac[0]=1;
    for(ll i=1;i<=n;i++) fac[i]=fac[i-1]*i%mod;
    infac[n]=qpow(fac[n],mod-2);
    for(ll i=n;i>=1;i--)infac[i-1]=infac[i]*i%mod;
    vector<vector<int>>E(n+1);
    
    for(int i=1;i<n;i++)
    {
        int u,v;
        cin>>u>>v;
        E[u].push_back(v);
        E[v].push_back(u);
    }
    vector<ll>sz(n+1),cnt(n+1);
    auto dfs=[&](auto self,int u,int fa)->void{
        sz[u]=1;
        for(auto v:E[u])
        {
            if(v==fa) continue;
            self(self,v,u);
            sz[u]+=sz[v];
            cnt[sz[v]]++;
        }
        cnt[n-sz[u]]++;
    };
    dfs(dfs,1,0);
    
    auto binmo=[&](int x,int y)->ll{
         if(x<y) return 0;
         return fac[x]*infac[y]%mod*infac[x-y]%mod;
    };

    vector<ll>C(n+1);
    for(int i=1;i<=n;i++) C[i]=binmo(n,i)*n%mod;
    Poly::poly A(n+1),B(n+1);
    for(int i=1;i<=n;i++) A[i]=1LL*cnt[i]*fac[i]%mod;
    for(int i=0;i<=n;i++) B[i]=infac[i];

    reverse(A.begin(), A.end());
    Poly::poly G=Poly::poly_mul(A,B);
    G.resize(n+1);
    reverse(G.begin(), G.end());
    for(int i=1;i<=n;i++) G[i]=infac[i]*G[i]%mod;
    for(int i=1;i<=n;i++)
    cout<<(C[i]-G[i]+mod)%mod<<'\n';
    
    return 0;
    

}

Loj6503-「雅礼集训 2018 Day4」Magic【分治NTT]

Problem

桌面上摆放着\(m\)种魔术卡,共\(n\) 张,第\(i\)种魔术卡数量为\(a_i\),魔术卡顺次摆放,形成一个长度为\(n\)的魔术序列,在魔术序列中,若两张相邻魔术卡的种类相同,则它们被称为一个魔术对。
两个魔术序列本质不同,当且仅当存在至少一个位置,使得两个魔术序列这个位置上的魔术卡的种类不同,求本质不同的恰好包含\(k\)个魔术对的魔术序列的数量,答案对\(998244353\)取模。

Sol

题目要求恰好\(k\)个,套路地进行而二项式反演,令\(f_i\)表示恰好\(i\)对,\(g_i\)表示至少\(i\)对,那么\(f_k=\sum_{i=k}^n(-1)^{i-k}C_i^kg_i\),接下来考虑如何求\(g_i\)。对于第\(i\)种魔术卡,如果不把它们分开,那么组合的时候就会贡献\(a_i-1\)对魔术对,现在考虑把第\(i\)种魔术卡切成几段,每切一段就会少\(1\)个魔术对,那么假设第\(i\)种魔术卡切了\(k_i\)刀,切完后把颜色相同的段看成一个整体,然后就有\(n-\sum_{i=1}^mk_i\)个整体,把它们全排列,因为求的是至少,所以就算有相同颜色的段排在相邻位置也没有问题,由于是无标号的可重集,注意除相应的阶乘。生成函数\(F(x)=\prod_{i=1}^m\sum_{j=1}^{a_i}\frac{C_{a_i-1}^{j-1}}{j!}x^j\),则\(g_i=(n-i)!\times [x^{n-i}]F(x)\)。这里\(F(x)\)统计的是减少的魔术对的个数,本来如果不切的话有\(n-m\)个魔术对,现在切了\(i-m\)次,就减少\(n-i\)个魔术对。这里为什么\(i\)要减去\(m\),是因为在算生成函数的时候算的是\(C_{a_i-1}^{j-1}x^j\),也就是说第\(j\)项是切\(j-1\)刀,总共有\(m\)种,最后的乘完的幂次\(i\)其实对应的是\(i-m\),就减少\(n-i\)个魔术对

//#pragma GCC optimize(2)
#include <bits/stdc++.h>
#define ll long long
#define pb push_back
using namespace std;
const int N = 1100000;
const int p = 998244353, gg = 3, ig = 332738118, img = 86583718;//1004535809 
const int M=18e5;
const int mod=998244353;
template <typename T>void rd(T &x)
{
    x = 0;
     int f = 1;
     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, int 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;
        }
    }
    inline 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;
    }
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    Poly::init(19);
    int m,n,k;
    cin>>m>>n>>k;
    vector<int>a(m+1);
    for(int i=1;i<=m;i++) cin>>a[i];
    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;

    vector<vector<ll>>g(m+1);
    auto C=[&](int x,int y)->ll{
      if(x<y) return 0;
      return fac[x]*infac[y]%mod*infac[x-y]%mod;
    };
    for(int i=1;i<=m;i++)
    {
        g[i].push_back(0);
        for(int j=1;j<=a[i];j++)
            g[i].push_back(C(a[i]-1,j-1)*infac[j]%mod);
    }
    auto CDQ=[&](auto self,int l,int r)->Poly::poly{

        if(l==r)  return g[l];
        int mid=l+r>>1;
        Poly::poly L=self(self,l,mid);
        Poly::poly R=self(self,mid+1,r);
        Poly::poly Q=Poly::poly_mul(L,R);
        return Q;
    };
    Poly::poly F=CDQ(CDQ,1,m);
    Poly::poly G((int)F.size());
    for(int i=0;i<=n;i++) G[i]=F[n-i]*fac[n-i]%mod;
    ll ans=0;
    for(int i=k;i<=n;i++)
    {
        if((i-k)&1) ans=(ans+mod-C(i,k)*G[i]%mod)%mod;
        else ans=(ans+C(i,k)*G[i]%mod)%mod;
    }
    cout<<ans<<'\n';
}

P4491 [HAOI2018]染色(二项式反演+NTT)

Problem

为了报答小 \(C\) 的苹果, 小 \(G\) 打算送给热爱美术的小 \(C\) 一块画布, 这块画布可 以抽象为一个长度为 \(N\) 的序列, 每个位置都可以被染成 \(M\) 种颜色中的某一种.然而小C只关心序列的 \(N\) 个位置中出现次数恰好为 \(S\) 的颜色种数, 如果恰好出现了 \(S\) 次的颜色有 \(K\) 种, 则小 C 会产生 \(W_k\)的愉悦度.小C希望知道对于所有可能的染色方案, 他能获得的愉悦度的和对 \(1004535809\) 取模的结果是多少.

Sol

二项式反演,设\(f_i\)表示恰好出现了\(i\)次,\(g_i\)表示至少出现了\(i\)次,则\(f_k=\sum_{i=k}^{m}(-1)^{i-k}C_i^kg_i\)。考虑求\(g_i\),由组合数学的思想算出\(g_i=C_m^{i}\frac{A_n^{S\times i}}{(S!)^{S\times i}}(m-i)^{n-S\times i}\),组合意义就是先从\(m\)种颜色取\(i\)种,使得每一种涂\(S\)次,然后考虑选排列,从\(n\)个物品种选出\(S\times i\)个,然后全排列,由于每种颜色有多个,相当于是可重复集的排列,每种颜色要除以\(S!\),总共要除\((S!)^{i}\),然后剩下的\(m-i\)种颜色随便涂在剩下的\(n-S\times i\)个物品上。注意到\(f_k=\sum_{i=k}^{m}(-1)^{i-k}C_i^kg_i=\sum_{i=k}^{m}(-1)^{i-k}\frac{i!}{k!(i-k)!}g_i\),令\(A_i=i!g_i\)\(B_i=\frac{(-1)^i}{i!}\),那么\(f_k=\frac{1}{k!}\sum_{i=k}^mA_iB_{i-k}\),卷积形式,这样就可加快运算,最后答案就是\(\sum_{i=1}^{min(m,\lfloor \frac{n}{S} \rfloor)}w_if_i\)

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

const int N = 1100000;
const int p = 1004535809, gg = 3, ig = 332738118, img = 86583718;//998244353 1004535809 
const int M=18e5;
const int mod=1004535809;
template <typename T>void rd(T &x)
{
    x = 0;
     int f = 1;
     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, int 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{
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    Poly::init(19);
    int n,m,s;
    cin>>n>>m>>s;
    int L=max(n,m);
    vector<ll>w(m+1);
    for(int i=0;i<=m;i++) cin>>w[i];
    vector<ll>fac(L+1),infac(L+1);
    fac[0]=1;
    for(int i=1;i<=L;i++) fac[i]=fac[i-1]*i%mod;
    infac[L]=qpow(fac[L],mod-2);
    for(int i=L;i>=1;i--) infac[i-1]=infac[i]*i%mod;

    auto C=[&](int x,int y)->ll{
        if(x<y) return 0;
        return fac[x]*infac[x-y]%mod*infac[y]%mod;
    };
    auto P=[&](int x,int y)->ll{
        if(x<y) return 0;
        return fac[x]*infac[x-y]%mod;
    };
    int L_=min(m,n/s);
    Poly::poly A(L_+1),B(L_+1);
    auto g=[&](int i)->ll{
        return C(m,i)*P(n,s*i)%mod*qpow(infac[s],i)%mod*qpow(m-i,n-s*i)%mod;
    };
    for(int i=0;i<=L_;i++)
    {
        A[i]=fac[i]*g(i)%mod;
        if(i&1) B[i]=(mod-infac[i])%mod;
        else B[i]=infac[i];
    }
    
    reverse(A.begin(), A.end());
    Poly::poly F=Poly::poly_mul(A,B);
    F.resize(L_+1);
    reverse(F.begin(), F.end());
    ll ans=0;
    for(int i=0;i<=L_;i++)
    ans=(ans+infac[i]*F[i]%mod*w[i]%mod)%mod;
    if(ans<0) ans=(ans+mod)%mod;
    cout<<ans<<'\n';

    return 0;
    

}

P5644 [PKUWC2018]猎人杀

Problem

一开始有\(n\)个猎人,第\(i\)i个猎人有仇恨度\(w_i\) ,每个猎人只有一个固定的技能:死亡后必须开一枪,且被射中的人也会死亡。然而向谁开枪也是有讲究的,假设当前还活着的猎人有\([i_1,...,i_m]\),那么有\(\frac{w_{i_k}}{\sum_{j=1}^nw_{i_j}}\)的概率向猎人\(i_k\)开枪。一开始第一枪由你打响,目标的选择方法和猎人一样,由于开枪导致的连锁反应,所有猎人最终都会死亡,现在 \(1\) 号猎人想知道它是最后一个死的的概率。答案对 \(998244353\) 取模。

Sol

首先对题意进行一波转换,设\(S=\sum_{i=1}^nw_i\),则题意可以转为为:每个人被打中的概率为\(\frac{w_i}{S}\),不断开枪直至打中一个活着的人,则这个人就会死去。什么意思呢?就是说就算每轮我们允许向一个死去的人开枪,但最终第\(i\)个活着的人被射中的概率是不变的,这样就可以不用处理每次射击分母都改变的情况了,证明:

假设当前还存活的猎人的仇恨度为\(P\),并且第\(i\)个活着的猎人被射中的概率为\(p\),题意转换前\(p=\frac{w_i}{P}\),题意转换后\(p=\sum_{i=1}^{\infty}(\frac{S-P}{S})^{i-1}\frac{w_i}{S}=\frac{w_i}{S}\frac{1}{1-\frac{S-P}{S}}=\frac{w_i}{S}\frac{S}{P}=\frac{w_i}{P}\)

接下来考虑容斥,枚举在\(1\)号猎人之后死的猎人的集合\(T\),并且该集合猎人总的仇恨值为\(P\),全部猎人总的仇恨值为\(S\),那么\(ans=\sum_{T}(-1)^{|T|}\frac{w_1}{S}\sum_{i=1}^{\infty}(1-\frac{P+w_1}{S})^i=\sum_{T}(-1)^{|T|}\frac{w_1}{S}\frac{1}{\frac{P+w_1}{S}}=\sum_{T}(-1)^{|T|}\frac{w_1}{P+w_1}\)

由于\(\sum_{i=1}^nw_i\le 100000\),所以考虑枚举\(P\),问题就只需要在已知\(P\)的情况下如何快速求出有多少个集合的仇恨值和为\(P\),考虑生成函数\(F(x)=\prod_{i=2}^n(1-x^{w_i})\)\(P\)的贡献就是\([x^P]F(x)\),所以答案就是\(ans=\sum_{P=0}^{S-w_1}[x^P]F(x)\frac{w_1}{P+w_1}\)

//#pragma GCC optimize(2)
#include <bits/stdc++.h>
#define ll long long
#define pb push_back
using namespace std;
const int N = 5100000;
const int p = 998244353, gg = 3, ig = 332738118, img = 86583718;//998244353 1004535809  
//924844033=>gg=5
const int M=18e5;
const int mod=998244353;
ll qpow(ll a, int 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{   
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    //freopen("gen.in","r",stdin);
    Poly::init(20);
    ll n;
    cin>>n;

    vector<int>w(n+1);
    int S=0;
    for(int i=1;i<=n;i++) cin>>w[i],S+=w[i];
    auto CDQ=[&](auto self,int l,int r)->Poly::poly{
        if(l==r)
        {
            Poly::poly F(w[l]+1);
            F[0]=1,F[w[l]]=mod-1;
            return F;
        }
        int mid=l+r>>1;
        Poly::poly L=self(self,l,mid);
        Poly::poly R=self(self,mid+1,r);
        return Poly::poly_mul(L,R);
    };
    Poly::poly F=CDQ(CDQ,2,n);
    ll ans=0;
    for(int i=0;i<=S-w[1];i++)
    if(F[i]<0) ans=(ans+mod+F[i]*w[1]%mod*qpow(i+w[1],mod-2)%mod)%mod;
    else ans=(ans+F[i]*w[1]%mod*qpow(i+w[1],mod-2)%mod)%mod;
    cout<<ans<<'\n';
    return 0;
}

CF960G-Bandit Blues[第一类斯特林数,分治,NTT]

Problem

给你三个正整数 \(n,a,b\),定义 \(A\) 为一个排列中是前缀最大值的数的个数,定义 \(B\) 为一个排列中是后缀最大值的数的个数,求长度为 \(n\) 的排列中满足 \(A = a\)\(B = b\) 的排列个数。\(n \le 10^5\) ,答案对 \(998244353\)取模。

Sol

考虑最大数放的位置为\(i\),这时候排列被分成\(2\)部分,然后在剩下的\(n-1\)个数选\(i-1\)个放左边,选\(n-i\)个放右边,方法数为\(C_{n-1}^{i-1}\)。然后先考虑左边,由于最大的数一定会同时存在于前缀最大和后缀最大,所以前\(i-1\)个数有\(A-1\)个最大前缀,考虑一种构造方法,先把这\(i-1\)个数分成\(A-1\)组,然后每组把最大的数放到最左边,最后把按照每组最左边数的大小按照从小到大的顺序放置。也可以理解为就是一种圆排列,因为是循环的,所以我们每次把这个圆环从最大的数断开,方法数是\(i-1 \brack A-1\)。同理右边就是\(n-i\brack B-1\),最后答案就是\(\sum_{i=1}^n\binom{n-1}{i-1}{i-1 \brack A-1} {n-i\brack B-1}\)

2021牛客暑期多校训练营9 C Cells

题意

给定n个点,分别在\((0,a_1),(0,a_2)...(0,a_n)\),现在这\(n\)个点只能向右或向下移动,要求这\(n\)个点分别走到\((1,0),(2,0)...(n,0)\)且路径两两不相交的总方案数,注意\((0,a_i)\)可以走到\((1,0),(2,0)...(n,0)\)中的任何一个。

Sol

首先不相交路径数我们优先考虑\(LGV\)引理(当时不会,大雾)
在这里插入图片描述

根据\(LGV\)​,由于每个\((0,a_i)\)​到\((1,0),(2,0)...(n,0)\)​的路径数分别为\(C_{a_i+1}^1,C_{a_i+2}^2,...,C_{a_i+n}^n\)

也即\(\frac{(a_i+1)!}{1!a_i!},\frac{(a_i+2)!}{2!a_i!},...,\frac{(a_i+n)!}{n!a_i!}\)

那么所对应的答案矩阵为

\[M= \left[ \begin{matrix} \frac{(a_1+1)!}{1!a_1!}& \frac{(a_1+2)!}{2!a_1!} & \cdots & \frac{(a_i+n)!}{n!a_n!}\\ \frac{(a_2+1)!}{1!a_2!} & \frac{(a_2+2)!}{2!a_2!} & \cdots & \frac{(a_2+n)!}{n!a_2!}\\ \vdots & \vdots & \ddots & \vdots\\ \frac{(a_n+1)!}{1!a_n!} & \frac{(a_n+2)!}{2!a_n!} & \cdots & \frac{(a_n+n)!}{n!a_n!} \end{matrix} \right] \tag{5} \]

然后每一列提出个\(\frac{1}{i!}\)出来,再把上下约分一下

\[M=\prod_{i = 1}^{n}\frac{1}{i!} \left[ \begin{matrix} (a_1+1)& (a_1+1)(a_1+2) & \cdots & (a_1+1)(a_1+2)...(a_1+n)\\ (a_2+1) & (a_2+1)(a_2+2) & \cdots & (a_2+1)(a_2+2)...(a_2+n)\\ \vdots & \vdots & \ddots & \vdots\\ (a_n+1) &(a_n+1)(a_n+2) & \cdots & (a_n+1)(a_n+2)...(a_n+n) \end{matrix} \right] \tag{5} \]

以第一行为例

\((a_1+1)(a_1+2)-(a_1+1)=(a_1+1)^2\)

\((a_1+1)(a_1+2)(a_1+3)-3(a_1+1)(a_1+2)+(a_1+1)=(a_1+1)^3\)

以此类推:每一列都可以通过前几列华为\((a_1+1)^i\)的形式,于是

\[M=\prod_{i = 1}^{n}\frac{1}{i!} \left[ \begin{matrix} (a_1+1)& (a_1+1)^2 & \cdots & (a_1+1)^n\\ (a_2+1) & (a_2+1)^2 & \cdots & (a_2+1)^n\\ \vdots & \vdots & \ddots & \vdots\\ (a_n+1) &(a_n+1)^2 & \cdots & (a_n+1)^n \end{matrix} \right] \tag{5} \]

然后每行提一个公因式出来

\[M=\prod_{i = 1}^{n}\frac{1}{i!}\prod_{i = 1}^{n}(a_i+1) \left[ \begin{matrix} 1& (a_1+1) & \cdots & (a_1+1)^{n-1}\\ 1 & (a_2+1) & \cdots & (a_2+1)^{n-1}\\ \vdots & \vdots & \ddots & \vdots\\ 1 &(a_n+1) & \cdots & (a_n+1)^{n-1} \end{matrix} \right] \tag{5} \]

到这里就很清楚了,后面的行列式是一个范德蒙行列式

\[M=\prod_{i = 1}^{n}\frac{1}{i!}\prod_{i = 1}^{n}(a_i+1)\prod_{1\leq i<j \leq n}^{n}((a_j+1)-(a_i+1)) \\=\prod_{i = 1}^{n}\frac{1}{i!}\prod_{i = 1}^{n}(a_i+1)\prod_{1\leq i<j \leq n}^{n}(a_j-a_i) \]

于是用\(O(n)\)处理处\(\prod_{i = 1}^{n}\frac{1}{i!}\)\(\prod_{i = 1}^{n}(a_i+1)\)

后面差的形式如果暴力则\(O(n^2)\),显然不可,考虑用卷积\((FFT/NTT)\)​ ,优化到\(O(nlogn)\)

为什么可以这样?一般的卷积是求系数相乘,怎么才能求减法呢?

由于卷积相乘幂次是相加的,于是可以把\(a_i和-a_i\)当做幂次,这样卷积完之后,遍历幂次的所有范围,如果系数不为0,说明这个幂次存在也即\((a_j-a_i)\)存在。

下面再考虑一个问题,卷积求的是任意两个的乘积,而我们要求的是有顺序的\({1\leq i<j \leq n}\)

首先由于存在负数,于是我们把负数加上一个偏移量(\(a_i\)的最大值记为\(M\)),于是我们幂次有\(a_i\)
\(M-a_i\) ,而我们最后卷积完会得到的所有幂次范围为\([0,2*M]\) ,这是注意题目有个条件\({1\leq i<j \leq n},a_i<a_j\)​,于是这样卷完\((a_j-a_i)\)等价于\(a_j+M-a_i>M\) 于是所有符合的幂次我们就从\(M+1\)\(2*M\)枚举,其对应的系数就为这个差值出现的次数,快速幂求一下就可以了。

#include <bits/stdc++.h>
#define ll long long
#define lll __int128
using namespace std;
const  int P = 998244353,G=3; // P=1004535809 ord(P)=3
const int pi=acos(-1);
inline ll read()
{
     ll x = 0, f = 1;
     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();}
    return x * f;
}

const int N=1e6+10,M=1e6;
ll n,m;
ll rev[1<<22],bit,tot;
ll a[1<<22],b[1<<22];
ll qmi(ll x,ll y)
{
    ll res=1;
    while(y)
    {
        if(y&1) res=res*x%P;
        x=x*x%P;
        y>>=1;
    }
    return res;
}
ll inv(ll x)
{
    return qmi(x,P-2);
}
void NTT(ll a[],int type)
{
    for(int i=0;i<tot;i++)
    if(i<rev[i]) swap(a[i],a[rev[i]]);

    for(int mid=1;mid<tot;mid<<=1)
    {
        ll wn=qmi(G,(P-1)/(mid*2));
        if(type==-1) wn=qmi(wn,P-2);
        for(int i=0,len=mid<<1;i<tot;i+=len)
        {
            ll w=1;
            for(int j=0;j<mid;j++,w=w*wn%P)
            {
                ll x=a[i+j],y=w*a[i+j+mid]%P;
                a[i+j]=(x+y)%P,a[i+j+mid]=(x-y+P)%P;
            }
        }
    }
    if(type==-1)
    {
        ll inv_=inv(tot);
        for(int i=0;i<tot;i++)
        a[i]=a[i]*inv_%P;
    }
}
int main()
{
     n=read();
    ll ans=1;
    for(int i=1;i<=n;i++)
    {
        ll x;
        x=read();
        ans=ans*(x+1)%P;
        a[x]=1,b[M-x]=1;
    }
    ll k1=1,k2=1;
    for(int i=1;i<=n;i++)
    {
        k1=k1*i%P;
        k2=k2*k1%P;
    }
    ans=ans*inv(k2)%P; 
    //cout<<ans<<endl;

    tot=1<<21,bit=21;
    for(int i=0;i<tot;i++) 
    rev[i]=(rev[i>>1]>>1)|((i&1)<<(bit-1));
    NTT(a,1),NTT(b,1);
    for(int i=0;i<tot;i++) a[i]=a[i]*b[i]%P;
    NTT(a,-1);
      
    for(int i=1e6+1;i<=M*2;i++)
    {
        if(a[i])
        {  
            ans=ans*qmi(i-1e6,a[i])%P; //求的是 (a_j - a_i ) (0<i<j<=n&&a_i<a_j) 
                                       //所以枚举>M的数就是(a_j-a_i)
        }
    }
    cout<<ans<<endl;
    puts("");
    
    return 0;

    
}

posted @ 2022-04-07 16:49  Arashimu  阅读(30)  评论(0编辑  收藏  举报