1. 程式人生 > >[jzoj 6080] [GDOI2019模擬2019.3.23] IOer 解題報告 (數學構造)

[jzoj 6080] [GDOI2019模擬2019.3.23] IOer 解題報告 (數學構造)

排列 列數 超過 ima 編號 == show def \n

題目鏈接:

https://jzoj.net/senior/#main/show/6080

題目:

技術分享圖片

題意:

給定$n,m,u,v$

設$t_i=ui+v$

求$\sum_{k_1+k_2+...+k_m=n}t_1^{k_1}t_2^{k_2}...t_m^{k_m}(k_1,k_2,...,k_m∈N)$

算法一:

對於$m=1$的點,顯然答案就是$t_1^n$,快速冪計算即可

獲得$5$分

算法二:

對於$m=2$的點,$\sum_{k1+k2=n}t_1^{k_1}t_2^{k_2}=\frac{t_1^{n+1}-t_2^{n+1}}{t1-t2}$

結合算法一獲得$15$分

算法三:

這顯然可以用生成函數,不妨設$f_i(x)=\sum_{k=0}^{n}t_i^kx^k$

把$f_1(x),f_2(x),...,f_m(x)$全部卷起來,第$n$次項的系數就是答案

用$ntt$優化多項式乘法,時間復雜度$O(Tmn logn)$,結合算法一和算法二得分$40$分

代碼

#include<algorithm>
#include<cstring>
#include<cstdio>
#include<iostream>
#include<cmath>
using namespace std;
typedef 
long long ll; const int N=4e6+15; const ll mo=998244353; inline ll read(){ char ch=getchar();ll s=0,f=1; while (ch<0||ch>9) {if (ch==-) f=-1;ch=getchar();} while (ch>=0&&ch<=9) {s=(s<<3)+(s<<1)+ch-0;ch=getchar();} return s*f; } ll qpow(ll a,ll b) { a
%=mo; ll re=1; for (;b;b>>=1,a=a*a%mo) if (b&1) re=re*a%mo; return re; } ll wn[31]; void pre() { for (int i=0;i<=30;i++) { ll t=1ll<<i; wn[i]=qpow(3,(mo-1)/t); } } int r[N]; void ntt(int limit,ll *a,int type) { for (int i=0;i<limit;i++) if (i<r[i]) swap(a[i],a[r[i]]); for (int len=1,id=0;len<limit;len<<=1) { ++id; for (int k=0;k<limit;k+=(len<<1)) { ll w=1; for (int l=0;l<len;l++,w=w*wn[id]%mo) { ll Nx=a[k+l],Ny=w*a[k+len+l]%mo; a[k+l]=(Nx+Ny)%mo; a[k+len+l]=((Nx-Ny)%mo+mo)%mo; } } } if (type==1) return; reverse(a+1,a+limit); ll inv=qpow(limit,mo-2); for (int i=0;i<limit;i++) a[i]=a[i]*inv%mo; } ll n,m,u,v; ll f[N],t[N],g[N]; int main() { freopen("ioer.in","r",stdin); freopen("ioer.out","w",stdout); int T=read(); while (T--) { n=read();m=read();u=read();v=read(); if (m==1) { ll t1=(u+v)%mo; printf("%lld\n",qpow(t1,n)); continue; } if (m==2) { ll t1=(u+v)%mo,t2=(2*u+v)%mo; ll inv=qpow(((t1-t2)%mo+mo)%mo,mo-2); ll R1=qpow(t1,n+1),R2=qpow(t2,n+1); printf("%lld\n",((R1-R2)%mo+mo)%mo*inv%mo); continue; } for (int i=1;i<=n;i++) t[i]=(u*i%mo+v)%mo; for (int i=0;i<=n;i++) f[i]=qpow(t[1],i); pre(); int limit=1,L=0; while (limit<=((n+1)<<1)) limit<<=1,L++; for (int i=0;i<limit;i++) r[i]=(r[i>>1]>>1)|((i&1)<<(L-1)); for (int i=2;i<=m;i++) { for (int j=0;j<=n;j++) g[j]=qpow(t[i],j); for (int j=n+1;j<limit;j++) g[j]=0; ntt(limit,f,1);ntt(limit,g,1); for (int j=0;j<limit;j++) f[j]=f[j]*g[j]%mo; ntt(limit,f,-1); for (int j=n+1;j<limit;j++) f[j]=0; } printf("%lld\n",f[n]); } return 0; }

算法四:

算法三可以優化

設$f_i(x)=\sum_{k>=0}^{n}t_i^kx^k=\frac{1}{1-t_ik}$

那麽求出$\frac{1}{f_1(x)},\frac{1}{f_2(x)},...,\frac{1}{f_m(x)}$的乘積,可以用分治$ntt$在$O(m log^2m)$的時間復雜度內求出

求出後在$\mod x^{n+1}$下多項式求逆,得到的結果的$n$次項系數即為答案

結合算法一,二得分$60$分

算法五:

之前都沒有用到$t_i=ui+v$這個條件

不妨構造下面這麽一個問題

假設你有一些球,每個球上標有一個不超過$m$的正整數。標有相同數字
的球可能顏色不同,兩個球被認為是相同的,當且僅當它們的數字和它們的
顏色都相同。

數字為$i(i<m)$的球各有u中不同的顏色

數字為m的球有u+v中不同顏色

考慮滿足一下條件的序列的數量

• 每個元素都是一個球

• 序列長度為 n + m − 1。

• 所有小於 m 的正整數都在序列中某個球上出現過

• 設從左到右第一個數字為 $i(i < m)$ 的球在序列上的位置為 $p_i$(序列上
位置從左到右,從 $1$ 開始編號),對於任意的$ i < j < m$,滿足$p_i<p_j$

為了方便描述,設$p_0=0,p_m=n+m$

枚舉 $p1, p2, · · · , p_{m−1}$,位置 $pi(1 ≤ i < m)$ 上的球數字只能是 $i$,在$ p_1$
之前的位置數字只能是 $m$,在 $p_2$ 之前的數字只能是 $m $或$ 1$...... 可以得到
滿足條件的序列數為

$\sum_{0<p_1<p_2<...<p_m-1<=n+m-1}(u+v)^{p_1-1}u(2u+v)^{p_2-1-p_1}...u(mu+v)^{n+m-1-p_{m-1}}$

設$k_i=p_i-1-p_{i-1}$,上式可以化簡為

$u^{m-1}\sum_{k_1+k_2+...+k_m=n} (u+v)^{k_1}(2u+v)^{k_2}...(mu+v)^{k_m}(k1,k2,...,km∈N)$

上式即題目中給出的問題的答案的 $u^{m−1}$ 倍。只要求出滿足條件的序列數就能快速得到原問題的答案。對於每個小於 $m$ 的數字,標有這個數字的球顏色種數都是 $u$,所以小於 $m$ 的數字可以看作是等價的。也就是說,設 $a$ 是 $1,...,m−1$ 的任意一個排列,如果把之前所說的這個序列要滿足的第四個條件改為:對於任意$i < j < m$,滿足$p_{a_i} < p_{a_j}$,滿足條件的序列數仍是 $u^{m−1} \sum_{k_1+···+k_m=n}(u +v)^{k_1} (2u + v)^{k_2}...(mu + v)^{k_m}(k_1, · · · , k_m ∈ N)$

因此,只滿足前三個條件的序列數,可以看作是 $a$ 取遍所有 $(m − 1)!$ 種排列,滿足對於任意 $i < j < m,p_{a_i} < p_{aj}$ 和前三個條件的序列數的和,即:
$(m-1)!u^{m−1} \sum_{k_1+···+k_m=n}(u +v)^{k_1} (2u + v)^{k_2}...(mu + v)^{k_m}(k_1, · · · , k_m ∈ N)$
所以我們只要算出滿足前三個條件的序列數,就可以快速求出原問題的答案。

滿足前三個條件序列數可以用容斥原理算出,也就是

$\sum_{k=0}^{m-1}\dbinom{m-1}{k}(-1)^k(mu+v-ku)^{n+m-1}$

所以所求問題的答案為

$\frac{\sum_{k=0}^{m-1}\dbinom{m-1}{k}(-1)^k(mu+v-ku)^{n+m-1}}{(m-1)!u^{m-1}}$

預處理階乘

時間復雜度$O(m+Tm logn)$,可以得到$100$分

思路清新,代碼簡單

代碼

#include<algorithm>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<iostream>
using namespace std;
typedef long long ll;

const int M=2e5+15;
const ll mo=998244353;
ll n,m,u,v;
ll fac[M],finv[M];
inline ll read()
{
    char ch=getchar();ll s=0,f=1;
    while (ch<0||ch>9) {if (ch==-) f=-1;ch=getchar();}
    while (ch>=0&&ch<=9) {s=(s<<3)+(s<<1)+ch-0;ch=getchar();}
    return s*f;
}
ll qpow(ll a,ll b)
{
    a%=mo;
    ll re=1;
    for (;b;b>>=1,a=a*a%mo) if (b&1) re=re*a%mo;
    return re;
}
void pre()
{
    fac[0]=1;
    for (int i=1;i<M;i++) fac[i]=fac[i-1]*i%mo;
    finv[M-1]=qpow(fac[M-1],mo-2);
    for (int i=M-2;i>=0;i--) finv[i]=finv[i+1]*(i+1)%mo;
}
ll C(ll a,ll b)
{
    return fac[a]*finv[b]%mo*finv[a-b]%mo;
}
int main()
{
    freopen("ioer.in","r",stdin);
    freopen("ioer.out","w",stdout);
    pre();
    int T=read();
    while(T--)
    {
        n=read();m=read();u=read();v=read();
        ll ans=0;
        for (int k=0;k<m;k++)
        {
            if (k&1) ans=(ans-C(m-1,k)*qpow(m*u+v-k*u,n+m-1)%mo+mo)%mo;
            else ans=(ans+C(m-1,k)*qpow(m*u+v-k*u,n+m-1)%mo)%mo;
        }
        ll o=qpow(u,m-1);
        ans=ans*finv[m-1]%mo*qpow(o,mo-2)%mo;
        printf("%lld\n",ans);
    } 
    return 0;
}

[jzoj 6080] [GDOI2019模擬2019.3.23] IOer 解題報告 (數學構造)