Poly 杂记(三)
本章节以 Stirling Number 为中心展开。
小式子
\(p\) 是素数时
换句话说,由于
所以在模 \(p\) 意义下
证明:
根据 威尔逊定理,我们可以知道 \({p \brack 1} = (p-1)! \equiv -1 \pmod p\),前两者容易证明。
其余部分我们考虑直接构造两个多项式
容易发现两者都是以 \(0 \sim p-1\) 为根的,于是我们直接做差可以得到
有 \(p\) 个 \(0\) 点,而 \(G(x)\) 次数 \(\lt p\)(最高次项 \(x^p\) 抵消了)。
所以 \(G(x) = 0\)(根据拉格朗日定理/多项式推理法)。得证。
这个式子在 QOJ 1285 中有应用。
另外一个神秘之师
直接展开即可得证。
斯特林数的四种求法
有一回对我说道,“你读过书么?”我略略点一点头。他说,“读过书,……我便考你一考。组合数学里的斯特林数,怎样说的?”我想,AKIOI的人,我也配答么?便回过脸去,不再理会。田乙己等了许久,很恳切的说道,“不会罢?……我教给你,记着!这些数应该记着。将来做OIer的时候,省选要用。”我暗想我和省选的等级还很远呢,而且我们考纲里也没有斯特林数;又好笑,又不耐烦,懒懒的答他道,“谁要你教,不是将n个元素划分成m个集合或环排列的方案数嘛?”田乙己显出极高兴的样子,将两个指头的长指甲敲着鼠标,点头说,“对呀对呀!……斯特林数有四样求法,你知道么?”我愈不耐烦了,努着嘴走远。田乙己刚打开typora,想在电脑上打公式,见我毫不热心,便又叹一口气,显出极惋惜的样子。
第一类斯特林数 - 行
求 \({n \brack 0} \sim {n \brack n}\),转求 \(x^{\overline n}\) 的系数。
定义 \(F_n(x) = x^{\overline n}\),则
于是多项式平移 + 倍增即可。时间复杂度 \(\mathcal O(n \log n)\)。
模板:P5408 第一类斯特林数·行。代码。
Code
poly a,b;
poly Move(poly F,int c){
int n=(int)F.size();
a.resize(n),b.resize(n);
for(int i=0;i<n;i++) a[i]=mul(F[n-i-1],fac[n-i-1]),b[i]=mul(ifac[i],qpow(c,i));
a=a*b;
for(int i=0;i<n;i++) F[i]=mul(ifac[i],a[n-1-i]);
return F;
}
map<int,poly> mp;
poly solve(int n){
if(mp.count(n)) return mp[n];
if(n==1) return {0,1};
return mp[n]=solve(n>>1)*Move(solve(n-(n>>1)),n>>1);
}
int main(){
/*2024.7.22 H_W_Y P5408 第一类斯特林数·行 多项式平移*/
cin>>n;init(n);
if(n==0) cout<<1,exit(0);
F=solve(n);
for(int i=0;i<=n;i++) cout<<F[i]<<' ';
return 0;
}
第二类斯特林数 - 行
求 \({n \brace 0} \cdots {n \brace n}\)。
我们有如下恒等式
这个式子也是简单的,就是考虑看成把 \(n\) 个互不相同的球放入 \(m\) 个互不相同的盒子里面,后者就是枚举那些盒子是空的。
于是我们对上面的式子进行二项式反演
最后的式子是一个卷积形式,所以我们可以用多项式乘法完成 第二类斯特林数行 的求法。代码。
Code
void Mul(ull *F,ull *G,int n,int m){
int P=1,I;
for(;P<n+m;P<<=1);
Init(P),I=qpow(P);
NTT(F,P,1),NTT(G,P,1),Cro(F,G,P),NTT(F,P,-1);
for(int i=0;i<P;i++) F[i]=mul(F[i],I);
}
int main(){
/*2024.6.27 H_W_Y P5395 第二类斯特林数·行*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n;init();
for(int i=0;i<=n;i++){
F[i]=mul(qpow(i,n),ifac[i]);
if(i&1) G[i]=dec(0,ifac[i]);
else G[i]=ifac[i];
}
Mul(F,G,n+1,n+1);
for(int i=0;i<=n;i++) cout<<F[i]<<' ';
return 0;
}
我们还有另外一种求法,即将 \(x^n\) 展开成下降幂形式
转化为求 \(0 \sim n\) 的点值,再用之前下降幂系数的 Trick \(\mathcal O(n \log n)\) 求得。(在杂记(二)里面)。
第一类斯特林数 - 列
接下来我们考虑用生成函数来求。
求 \({0 \brack k} \sim {n \brack k}\)。
设 \(n\) 个元素放入一个圆排列,也就是 \({n \brack 1} = (n-1)!\),
于是我们有它的 EGF 是 \(\hat A(x) = \sum_{n \gt 0} \frac {x^n} n\)。
那么放入 \(k\) 个圆排列的 EGF 就是
前面乘上 \(\frac 1 {k!}\) 是因为圆排列是无序的。
直接用多项式快速幂解决即可。时间复杂度 \(\mathcal O(n \log n)\)。
模板:P5409 第一类斯特林数·列。代码。
Code
poly Qpow_plus(poly F,int k){
int n=(int)F.size(),id=0;
poly A;
for(int i=0;i<n;i++) if(F[i]!=0){
int I=qpow(F[i]);
A.resize(n-id);
for(int j=i;j<n;j++)
A[j-i]=mul(F[j],I);
id=i;break;
}
A=Qpow(A,k);
int nw=qpow(F[id],k);
id=mul(id,k);
for(int i=0;i<n;i++) F[i]=0;
for(int i=id;i<n&&i<(int)A.size()+id;i++) F[i]=mul(A[i-id],nw);
return F;
}
int main(){
/*2024.7.22 H_W_Y P5409 第一类斯特林数·列*/
cin>>n>>m;init(N-1);
F.resize(n+1);
for(int i=1;i<=n;i++) F[i]=inv[i];
F=Qpow_plus(F,m);
for(int i=0;i<=n;i++) cout<<mul(F[i],mul(fac[i],ifac[m]))<<' ';
return 0;
}
第二类斯特林数 - 列
求 \({0 \brace k} \sim {n \brace k}\)。
还是考虑生成函数。
同样的,我们把 \(n\) 个元素放入一个集合中,那么 \({n \brace 1} = 1\),于是其 EGF 为
那么放入 \(k\) 个集合中的答案就是
于是就做完了,时间复杂度 \(\mathcal O(n \log n)\)。
模板:P5396 第二类斯特林数·列。代码。
Code
int main(){
/*2024.7.22 H_W_Y P5396 第二类斯特林数·列*/
cin>>n>>m;init(N-1);
F.resize(n+1);
for(int i=1;i<=n;i++) F[i]=ifac[i];
F=Qpow_plus(F,m);
for(int i=0;i<=n;i++) cout<<mul(F[i],mul(fac[i],ifac[m]))<<' ';
return 0;
}
和上一道题的代码 \(\Delta = 3\),还是比较牛的。
P4091 [HEOI2016/TJOI2016] 求和 - 组合意义
现在他想计算这样一个函数的值:
\[f(n)=\sum_{i=0}^n\sum_{j=0}^i S(i,j)\times 2^j \times (j!) \]S(i, j)表示第二类斯特林数,递推公式为:
\(S(i, j) = j \times S(i - 1, j) + S(i - 1, j - 1), 1 \le j \le i - 1\)。
边界条件为:\(S(i, i) = 1(0 \le i), S(i, 0) = 0(1 \le i)\)
你能帮帮他吗?
\(1 \leq n \leq 10^5\)。
我们现在考虑求解
考虑其组合意义:
把 \(n\) 个球放入 \(i\) 个 box 中的方案数,每个 box 不相同且对 box 进行黑白染色。
上面的式子枚举的是球,我们考虑枚举 box,即枚举最后一个 box 放了多少个球,于是有
用 EGF 的常用套路展开一下就可以得到
于是设
上式就是
直接多项式求逆即可,时间复杂度 \(\mathcal O(n \log n)\)。代码。
Code
int main(){
/*2024.7.23 H_W_Y P4091 [HEOI2016/TJOI2016] 求和 Striling Number*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n;F.resize(n+1),init(n);
for(int i=1;i<=n;i++) F[i]=dec(0,mul(2,ifac[i]));
F[0]=1;Inv(F);
for(int i=0;i<=n;i++) add(ans,mul(F[i],fac[i]));
cout<<ans;
return 0;
}
P6620 [省选联考 2020 A 卷] 组合数问题 - 下降幂/Snake Oil
众所周知,小葱同学擅长计算,尤其擅长计算组合数。小葱现在希望你计算
\[\left(\sum_{k=0}^{n}f(k)\times x^k\times \binom{n}{k}\right)\bmod p \]的值。其中 \(n\), \(x\), \(p\) 为给定的整数,\(f(k)\) 为给定的一个 \(m\) 次多项式 \(f(k) = a_0 + a_1k + a_2k^2 + \cdots + a_mk^m\)。\(\binom{n}{k}\) 为组合数,其值为 \(\binom{n}{k} = \frac{n!}{k!(n-k)!}\)。
\(1\le n, x, p \le 10^9, 0\le a_i\le 10^9, 0\le m \le \min(n,1000)\)。
由于涉及求和,所以我们考虑 下降幂,即把 \(f\) 转化成下降幂形式。
考虑 \(f(k) = k ^{\underline m}\) 的情况,那么其它的情况我们都可以解决了。
于是式子就是
考虑一个神秘式子
进而我们可以优化上面的式子了
如果我们把 \(f(x)\) 表示成
那么可以得到答案就是
由于 \(m \le 1000\),所以我们可以用 \(\mathcal O(n^2)\) 的时间得到 \(f\) 的下降幂形式。
但是由于 \(p\) 是任意的整数,所以我们不能涉及求逆操作,于是普通幂到下降幂的转化可以直接用 第二类斯特林数 完成,也就是
于是用递推式预处理出第二类斯特林数就可以完成了。
时间复杂度 \(\mathcal O(m^2)\)。代码。
Code
void init(int n){
S[0][0]=1;
for(int i=1;i<=n;i++)
for(int j=1;j<=i;j++)
S[i][j]=adc(mul(j,S[i-1][j]),S[i-1][j-1]);
}
int main(){
/*2024.7.23 H_W_Y P6620 [省选联考 2020 A 卷] 组合数问题*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n>>x>>H>>m;init(m);
for(int i=0;i<=m;i++) cin>>a[i];
for(int i=0;i<=m;i++) for(int j=i;j<=m;j++) add(b[i],mul(S[j][i],a[j]));
for(int i=0,p1=1,p2=1;i<=m;i++){
add(ans,mul(mul(b[i],p1),mul(p2,qpow(x+1,n-i))));
p1=mul(p1,n-i),p2=mul(p2,x);
}
cout<<ans;
return 0;
}
不用 下降幂?
这道题我们还可以用另外一种方法推一推,似乎叫 Snake Oil。
直接把式子展开
我们设
于是需要敏锐地观察到 \(f\) 的 EGF 是可求的:
于是这个可以用多项式快速幂解决。但是实际上在这道题中不适用。
P6667 [清华集训2016] 如何优雅地求和 - 下降幂/PGF
有一个多项式函数 \(f(x)\),最高次幂为 \(x^m\),定义变换 \(Q\):
\[Q(f,n,x)=∑_{k=0}^{n}f(k)\binom nkx^k(1−x)^{n−k} \]现在给定函数 \(f\) 和 \(n,x\),求 \(Q(f,n,x)\bmod998244353\)。
出于某种原因,函数 \(f\) 由点值形式给出,即给定 \(a_0,a_1,⋯,a_m\) 共 \(m+1\) 个数,\(f(x)=a_x\)。可以证明该函数唯一。
\(1≤n≤10^9\),\(1≤m≤2×10^4\),\(0≤a_i,x<998,244,353\)。
出于某种原因?给连续点值该是提醒你用下降幂了/qd
和上一题基本一样,我们还是先考虑 \(f(k) = k^{\underline m}\) 的情况,这道题只是多了一个 \((1-x)^{n-k}\),那么
非常优美的式子啊,于是答案就是
现在问题就变成了求下降幂系数 \(b_i\) 了,这是杂记(二)中的经典问题,于是直接 \(\mathcal O(n \log n)\) 卷积就做完了。代码。
Code
int main(){
/*2024.7.23 H_W_Y P6667 [清华集训2016] 如何优雅地求和 下降幂*/
cin>>n>>m>>x;init(m);
F.resize(m+1),G.resize(m+1);
for(int i=0;i<=m;i++){
cin>>F[i],F[i]=mul(F[i],ifac[i]);
if(i&1) G[i]=dec(0,ifac[i]);
else G[i]=ifac[i];
}
F=F*G;
for(int i=0,p1=1,p2=1;i<=m;i++)
add(ans,mul(F[i],mul(p1,p2))),p1=mul(p1,n-i),p2=mul(p2,x);
cout<<ans;
return 0;
}
这道题还有一种用 概率生成函数 PGF 的方法。
我们稍微改一下原式的变量名 \(x \to p\),于是可以得到
这个不是和一个事件发生的概率看起来这么像?概率分布?
考虑一种实验,有 \(p\) 的概率成功,有 \(1-p\) 的概率失败。
而 \(X\) 是一种随机变量,表示 \(n\) 次实验中成功的次数,于是我们有
于是原式就是
同样的,我们设 \(f(k) = x^{\underline m}\),于是我们可以得到 \(E(x^{\underline m})\) 的 概率生成函数 PGF。
也就是一次实验的 PGF 是 \((pz + (1-p))\),则 \(X\) 的 PGF 是 \((pz+(1-p))^n\)。
然后你需要观察一个结论:\(X\) 的 PGF 为 \(G(x)\) 则
证明就是发现
所以我们有
证毕。而下一步的化简更为显然。
于是你又可以像上一种方法那样做了。
感觉这种方法不是很优?你需要观察到很多东西啊/jk
但是,这里有一道几乎一模一样的例题:CF1278F Cards。
直接把 \(x^k\) 用 Stirling 数展开成下降幂形式,然后用上述方法求解就可以了,这里 \(p=\frac 1 m\)。代码。
Code
#include <bits/stdc++.h>
using namespace std;
const int N=5e3+5,H=998244353;
int n,m,k,p,s[N][N],ans;
int adc(int a,int b){return a+b>=H?a+b-H:a+b;}
int mul(int a,int b){return 1ll*a*b%H;}
void add(int &a,int b){a=adc(a,b);}
int qpow(int a,int b=H-2){
int res=1;
while(b){if(b&1) res=mul(res,a);a=mul(a,a),b>>=1;}
return res;
}
void init(){
s[0][0]=1;
for(int i=1;i<=k;i++)
for(int j=1;j<=i;j++) s[i][j]=adc(s[i-1][j-1],mul(s[i-1][j],j));
p=qpow(m);
}
int main(){
/*2024.8.1 H_W_Y CF1278F Cards PGF*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n>>m>>k;init();
for(int i=0,p1=1,p2=1;i<=k;i++)
add(ans,mul(s[k][i],mul(p1,p2))),p1=mul(p1,n-i),p2=mul(p2,p);
cout<<ans;
return 0;
}
QOJ 1285 Stirling Number
给定四个整数 \(n,l,r,p\),求
\[\left( \sum_{k=l}^r {n \brack k} \right) \bmod p \]\(1 \le n \le 10^{18},0 \le l \le r \le n,2 \le p \le 10^6\),保证 \(p\) 是质数。
好题!
由于一行的第一类斯特林数是由 \(x^{\overline n}\),所以我们考虑优化这个部分,设
发现我们需要用 \(\bmod p\) 的性质,由于我们知道
所以设 \(n=qp+r\),于是有
那么我们考虑 \(n \brack k\) 的表示
注意,这里我们钦定 \(r \neq 0\),也就是 \(r=0\) 的情况在后面特殊处理,于是枚举的 \(i\) 是从 \(1\) 开始的。
发现系数一定是形如 \((p-1)i + j\) 的形式,于是我们设 \(k-q= a(p-1)+b\),其中 \(1 \le b \le r\),于是我们有
于是把最初的式子差分之后,我们的目标是计算第一类斯特林数的前缀和,也就是可以得到
由于 \(p\) 很小,所以我们考虑枚举 \(b\),设 \(M-q = m (p-1) + k(1 \le k \le p-1)\),于是可以得到
对于
我们可以直接用上指标反转之后平行求和,也就是可以得到
那么原式就是
那么现在问题又变成了求
一个最简单的做法是直接把这一行求出来,也就是用 任意模数 NTT 直接做,时间复杂度 \(\mathcal O(p \log p)\),但是我们存在 \(\mathcal O(p)\) 的线性做法,而且较为好写。
其中 \(r \lt p\),根据 EI 的这个(小质数单点S1) 我们可以用 \(f(1),f(g),f(g^2),\cdots,f(g^{p-2})\) 去 IDFT 出系数,这里的 \(f(x) = x^{\overline r}\),其中 \(g\) 是 \(p\) 意义下的原根。
也就是说,我们有式子
于是问题的式子就是
就可以在 \(\mathcal O(p)\) 的线性时间复杂度之内完成了。
我们似乎还落下了一个 \(r=0\) 的情况,也来小推一手,发现是容易的
也就是说这里需要 \((p-1) \mid k-q\),同样设 \(M-q = m(p-1) +k\),那么答案就是
于是这道题就做完了,时间复杂度 \(\mathcal O(p)\)。代码。
Code
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N=2e6+5;
int L,R,n,q,p,r,m,k,fac[N],ifac[N],f[N],prm[N],ct=0,g,pw[N],ipw[N];
int adc(int a,int b){return a+b>=p?a+b-p:a+b;}
int dec(int a,int b){return a<b?a-b+p:a-b;}
int mul(int a,int b){return 1ll*a*b%p;}
void add(int &a,int b){a=adc(a,b);}
void del(int &a,int b){a=dec(a,b);}
int qpow(int a,int b=p-2){
int res=1;
while(b){if(b&1) res=mul(res,a);a=mul(a,a),b>>=1;}
return res;
}
bool chk(int x){
if(qpow(x,p-1)!=1) return 0;
for(int i=1;i<=ct;i++)
if(qpow(x,(p-1)/prm[i])==1) return 0;
return 1;
}
int findrt(){
for(int i=2;i<p;i++) if(chk(i)) return i;
return -1;
}
void init(){
fac[0]=1;
for(int i=1;i<p;i++) fac[i]=mul(fac[i-1],i);
ifac[p-1]=qpow(fac[p-1]);
for(int i=p-1;i>=1;i--) ifac[i-1]=mul(ifac[i],i);
int x=p-1;
for(int i=2;i*i<=x;i++){
if(x%i==0){
prm[++ct]=i;
while(x%i==0) x/=i;
}
}
if(x!=1) prm[++ct]=x;
g=findrt();
pw[0]=1,f[0]=fac[r];
for(int i=1;i<=p-2;i++)
pw[i]=mul(pw[i-1],g),f[i]=mul(fac[pw[i]+r-1],ifac[pw[i]-1]);
}
int C(int n,int m){
if(m<0||n<m) return 0;
return mul(fac[n],mul(ifac[m],ifac[n-m]));
}
int binom(int n,int m){
if(m<0||n<m) return 0;
if(m==0) return 1;
return mul(C(n%p,m%p),binom(n/p,m/p));
}
int calc(int k){
if(k>=r) return fac[r];
int res=dec(0,mul(k+1,f[0]));
for(int j=1;j<=p-2;j++)
del(res,mul(f[j],mul(dec(pw[(p-1-j)*(k+1)%(p-1)],1),qpow(dec(pw[p-1-j],1)))));
return res;
}
int SOLVE(int M){
if(!M||M<q) return 0;
m=(M-q)/(p-1),k=(M-q)%(p-1);
if(!r){
if((q+m)&1) return dec(0,binom(q-1,m));
return binom(q-1,m);
}
if(!k) k=p-1,--m;
int res=0;
res=dec(mul(binom(q,m),calc(k)),mul(binom(q-1,m-1),fac[r]));
if((q+m)&1) return dec(0,res);
return res;
}
signed main(){
/*2024.7.25 H_W_Y QOJ 1285 Stirling Number*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n>>L>>R>>p;
q=n/p,r=n%p;init();
cout<<dec(SOLVE(R),SOLVE(L-1));
return 0;
}
CF932E Team Work - Stirling
给定 $ n,k $,求:
\[\sum_{i=1}^n\binom n i \times i^k \]$ 1 \leq k \leq 5000,1 \leq n \leq 10^9 $
求和?直接展开成下降幂形式,即用第二类斯特林数展开
于是直接 \(k^2\) 求第二类斯特林数然后暴力求就可以了。
时间复杂度 \(\mathcal O(k^2)\)。代码。
Code
#include <bits/stdc++.h>
using namespace std;
const int N=5e3+5,H=1e9+7;
int s[N][N],n,k,inv2,ans;
int adc(int a,int b){return a+b>=H?a+b-H:a+b;}
int mul(int a,int b){return 1ll*a*b%H;}
void add(int &a,int b){a=adc(a,b);}
int qpow(int a,int b=H-2){
int res=1;
while(b){if(b&1) res=mul(res,a);a=mul(a,a),b>>=1;}
return res;
}
void init(){
s[0][0]=1;
for(int i=1;i<=k;i++)
for(int j=1;j<=i;j++) s[i][j]=adc(s[i-1][j-1],mul(s[i-1][j],j));
inv2=qpow(2);
}
int main(){
/*2024.8.1 H_W_Y CF932E Team work Stirling*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n>>k;init();
for(int i=0,p1=qpow(2,n),p2=1;i<=k;i++) add(ans,mul(s[k][i],mul(p1,p2))),p1=mul(p1,inv2),p2=mul(p2,n-i);
cout<<ans;
return 0;
}
CF717A Festival Organization - Fibonacci + Stirling + 扩域
一个合法的串定义为:长度在 \([l,r]\) 之间,且只含
0,1,并且不存在连续 \(2\) 个或更多的 \(0\)。现在要选出 \(k\) 个长度相同的合法的串,问有几种选法,答案模 \(10^9+7\)。
不是所有 \(a^b\) 都有欧拉定理。好题。
首先,考虑普通 dp,设 \(f_{i,0/1}\) 表示长度为 \(i\) 最后一位是 \(0\) 还是 \(1\) 的方案数,那么容易有一下转移式子
一看就有斐波那契数列的感觉,进而,我们发现
其中 \(F_i\) 表示 Fibonacci 数列的第 \(i\) 项。
那么其实答案就是
我们令 \(L=l+2,R=r+2\),把组合数转化成下降幂形式,就可以得到
在这里,下降幂明显是不好处理的,所以我们考虑用 带符号第一类斯特林数 转化成幂形式
对于 Fibonacci,我们能说什么?
容易知道其生成函数为 \(\frac x {1-x-x^2}\),那么我们有其 通项公式
这里我们令 \(a = \frac 1 {\sqrt 5},b = - \frac 1 {\sqrt 5}\),那么其实
把它带入上面的式子,可以得到
最后的那一项可以用等比数列求和公式完成。于是这样我们似乎就做完了。
真的做完了吗?发现并非如此,因为 \(5\) 在 \(10^9+7\) 的意义下没有二次剩余。
所以我们考虑 扩域,把每个数都表示成 \(a+b \sqrt 5\),这样运算法则和复数类似,就可以完成了。
时间复杂度 \(\mathcal O(n^2)\)。代码。
Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N=205,H=1e9+7;
int fac[N],ifac[N],inv[N],k,S[N][N],C[N][N];
ll l,r;
int adc(int a,int b){return a+b>=H?a+b-H:a+b;}
int dec(int a,int b){return a<b?a-b+H:a-b;}
int mul(int a,int b){return 1ll*a*b%H;}
void add(int &a,int b){a=adc(a,b);}
void del(int &a,int b){a=dec(a,b);}
int qpow(int a,int b=H-2){
int res=1;
while(b){if(b&1) res=mul(res,a);a=mul(a,a),b>>=1;}
return res;
}
struct Nod{
int a,b;
Nod operator +(const Nod &x) const{return {adc(a,x.a),adc(b,x.b)};}
Nod operator +(const int &x) const{return {adc(a,x),b};}
Nod operator -(const Nod &x) const{return {dec(a,x.a),dec(b,x.b)};}
Nod operator -(const int &x) const{return {dec(a,x),b};}
Nod operator *(const Nod &x) const{return {adc(mul(a,x.a),mul(5,mul(b,x.b))),adc(mul(a,x.b),mul(b,x.a))};}
Nod operator *(const int &x) const{return {mul(a,x),mul(b,x)};}
Nod operator /(const Nod &x) const{
int res=qpow(dec(mul(x.a,x.a),mul(5,mul(x.b,x.b))));
return {mul(res,dec(mul(a,x.a),mul(5,mul(b,x.b)))),mul(res,dec(mul(b,x.a),mul(a,x.b)))};
}
};
Nod qpow(Nod a,ll b=H-2){
Nod res={1,0};
while(b){if(b&1) res=res*a;a=a*a,b>>=1;}
return res;
}
Nod Sum(Nod a,ll n){return (qpow(a,n+1)-1)/(a-1);}
Nod calc(Nod x,ll l,ll r){
if(x.a==1&&x.b==0) return x*((r-l+1)%H);
return Sum(x,r)-Sum(x,l-1);
}
void init(){
fac[0]=inv[1]=1;
for(int i=1;i<N;i++) fac[i]=mul(fac[i-1],i);
for(int i=2;i<N;i++) inv[i]=mul(inv[H%i],dec(0,H/i));
ifac[N-1]=qpow(fac[N-1]);
for(int i=N-1;i>=1;i--) ifac[i-1]=mul(ifac[i],i);
C[0][0]=1;
for(int i=1;i<N;i++){
C[i][0]=1;
for(int j=1;j<=i;j++) C[i][j]=adc(C[i-1][j],C[i-1][j-1]);
}
S[0][0]=1;
for(int i=1;i<N;i++)
for(int j=1;j<=i;j++) S[i][j]=dec(S[i-1][j-1],mul(i-1,S[i-1][j]));
}
void SOLVE(){
Nod a={0,inv[5]},b={0,dec(0,inv[5])},x={inv[2],inv[2]},y={inv[2],dec(0,inv[2])};
int ans=0;
for(int i=0;i<=k;i++){
Nod cur={0,0};
for(int j=0;j<=i;j++){
Nod nw=qpow(a,j)*qpow(b,i-j)*calc(qpow(x,j)*qpow(y,i-j),l,r)*C[i][j];
cur=cur+nw;
}
cur=cur*S[k][i];
add(ans,cur.a);
}
ans=mul(ans,ifac[k]);
cout<<ans<<'\n';
}
signed main(){
/*2024.8.1 H_W_Y CF717A Festival Organization Fibonacci + Stirling*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
init();
cin>>k>>l>>r,l+=2,r+=2;
SOLVE();
return 0;
}
也可以说是双倍经验吧。
容易发现其实 \(m=2\) 的情况就是这道题,直接贺。
而关于 \(m=3\),\(2 \nmid n\) 时显然是不合法的,于是我们设 \(f_i\) 表示长度为 \(2i\) 时的答案,有递推方程
不要忘记乘二!!!
于是就可以用类似于解 Fibonacci 的方法,用生成函数刻画得到
就和上面一道题一样了。代码。
CF961G Partitions - Stirling 数通项公式
给出 \(n\) 个物品,每个物品有一个权值 \(w_i\)。
定义一个集合 \(S\) 的权值 \(W(S)=|S|\sum\limits_{x\in S}w_x\)。
定义一个划分的权值为 \(W'(R)=\sum\limits_{S\in R}W(S)\)。
求将 \(n\) 个物品划分成 \(k\) 个集合的所有方案的权值和。答案对 \(10^9+7\) 取模。
\(n,k\le2\times10^5\),\(w_i\le10^9\)。
首先,容易想到去考虑每一种情况的贡献次数,也就是对于 \(w_i\),它贡献的次数是
怎么化简?
我们考虑把 第二类斯特林数 展开,也就是用通项公式
得到
考虑把 \(j\) 吸收公式吸进去,也就是得到
于是答案就是
就可以直接计算了。然而,其实后面的一块式子可以被表示成
这可能就是组合意义的解释吧。
时间复杂度 \(\mathcal O(k \log k)\),快速幂的 \(\log\)。代码。
Code
#include <bits/stdc++.h>
using namespace std;
const int N=2e5+5,H=1e9+7;
int n,k,sum,fac[N],ifac[N],ans=0;
int adc(int a,int b){return a+b>=H?a+b-H:a+b;}
int dec(int a,int b){return a<b?a-b+H:a-b;}
int mul(int a,int b){return 1ll*a*b%H;}
void add(int &a,int b){a=adc(a,b);}
void del(int &a,int b){a=dec(a,b);}
int qpow(int a,int b=H-2){
int res=1;
while(b){if(b&1) res=mul(res,a);a=mul(a,a),b>>=1;}
return res;
}
void init(){
fac[0]=1;
for(int i=1;i<N;i++) fac[i]=mul(fac[i-1],i);
ifac[N-1]=qpow(fac[N-1]);
for(int i=N-1;i>=1;i--) ifac[i-1]=mul(ifac[i],i);
}
int main(){
/*2024.8.2 H_W_Y CF961G Partitions Stirling*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n>>k;init();
for(int i=1,x;i<=n;i++) cin>>x,add(sum,x);
if(n==1){
if(k==1) cout<<sum<<'\n';
else cout<<"0\n";exit(0);
}
for(int i=0;i<k;i++){
int tmp=mul(mul(ifac[i],ifac[k-1-i]),mul(qpow(k-i,n-2),k-i-1+n));
if(i&1) del(ans,tmp);
else add(ans,tmp);
}
ans=mul(ans,sum);
cout<<ans;
return 0;
}
CF960G Bandit Blues
给你三个正整数 \(n\),\(a\),\(b\),定义 \(A\) 为一个排列中是前缀最大值的数的个数,定义 \(B\) 为一个排列中是后缀最大值的数的个数,求长度为 \(n\) 的排列中满足 \(A = a\) 且 \(B = b\) 的排列个数。\(n \le 10^5\),答案对 \(998244353\) 取模。
挺烫的。
考虑那个数既有前缀最大值又有后缀最大值:只有 \(n\)。
于是 \(n\) 相当于把序列划分成了两个区间,前面满足前缀最大值个数为 \(a-1\),后面满足后缀最大值个数为 \(b-1\),两者互不干扰。
我们考虑从大往小插入,设前 \(i\) 大的数构成前缀最大值 \(j\) 个的方案数,那么有
简直和第一类斯特林数的转移一模一样,于是其实
那么我们就得到答案
直接贺第一类斯特林数列,然后冲过。代码。
Code
poly Stirling(int n,int m){
poly F(n+1);
for(int i=1;i<=n;i++) F[i]=inv[i];
F=Qpow_plus(F,m);
for(int i=0;i<=n;i++) F[i]=mul(F[i],mul(fac[i],ifac[m]));
return F;
}
int binom(int n,int m){
if(n<m||m<0) return 0;
return mul(fac[n],mul(ifac[m],ifac[n-m]));
}
int main(){
/*2024.8.2 H_W_Y P5409 第一类斯特林数·列*/
cin>>n>>a>>b;init(N-1);
if(!a||!b) cout<<0<<'\n',exit(0);
--a,--b;
poly A=Stirling(n,a),B=Stirling(n,b);
for(int i=1;i<=n;i++) add(ans,mul(binom(n-1,i-1),mul(A[i-1],B[n-i])));
cout<<ans;
return 0;
}
其实想想上面这个式子的组合意义,我们还可以把它化简。
答案就是
完全胜利!
但似乎还是要分治 NTT 或者第一类斯特林数行求,还是 \(\mathcal O(n \log n)\),常数会小一点。

浙公网安备 33010602011771号