多项式泛做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!}\)
那么所对应的答案矩阵为
然后每一列提出个\(\frac{1}{i!}\)出来,再把上下约分一下
以第一行为例
\((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\)的形式,于是
然后每行提一个公因式出来
到这里就很清楚了,后面的行列式是一个范德蒙行列式
于是用\(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;
}