今天的 CSP-S 模拟比 NOI 模拟难。。
为了方便,我们记 g ( n ) = ( n B ) g(n)=\binom n B g(n)=(Bn)。
题目要我们求的是:
a n s = ∑ i = 1 n ∑ j = 1 m g ( gcd ( i , j ) ) ans=\sum_{i=1}^n\sum_{j=1}^mg(\gcd(i,j)) ans=i=1∑nj=1∑mg(gcd(i,j))
那么,先按照莫反的套路化简一波(令 u p = min ( n , m ) up=\min(n,m) up=min(n,m)):
a n s = ∑ k = 1 u p g ( k ) ∑ i = 1 ⌊ n k ⌋ ∑ j = 1 ⌊ m k ⌋ [ gcd ( i , j ) = 1 ] = ∑ k = 1 u p g ( k ) ∑ i = 1 ⌊ n k ⌋ ∑ j = 1 ⌊ m k ⌋ ∑ d ∣ i , d ∣ j μ ( d ) = ∑ k = 1 u p g ( k ) ∑ d = 1 ⌊ u p k ⌋ μ ( d ) ⌊ n k d ⌋ ⌊ m k d ⌋ \begin{aligned} ans&=\sum_{k=1}^{up}g(k)\sum_{i=1}^{\lfloor\frac n k\rfloor}\sum_{j=1}^{\lfloor\frac m k\rfloor}[\gcd(i,j)=1]\\ &=\sum_{k=1}^{up}g(k)\sum_{i=1}^{\lfloor\frac n k\rfloor}\sum_{j=1}^{\lfloor\frac m k\rfloor}\sum_{d|i,d|j}\mu(d)\\ &=\sum_{k=1}^{up}g(k)\sum_{d=1}^{\lfloor\frac{up}k\rfloor}\mu(d)\lfloor\frac n {kd}\rfloor\lfloor\frac m {kd}\rfloor \end{aligned} ans=k=1∑upg(k)i=1∑⌊kn⌋j=1∑⌊km⌋[gcd(i,j)=1]=k=1∑upg(k)i=1∑⌊kn⌋j=1∑⌊km⌋d∣i,d∣j∑μ(d)=k=1∑upg(k)d=1∑⌊kup⌋μ(d)⌊kdn⌋⌊kdm⌋
令 T = k d T=kd T=kd,则:
a n s = ∑ k = 1 u p ( ∑ k ∣ T g ( k ) μ ( T k ) ) ⌊ n T ⌋ ⌊ m T ⌋ ans=\sum_{k=1}^{up}\left(\sum_{k|T}g(k)\mu(\frac T k)\right)\lfloor\frac{n}{T}\rfloor\lfloor\frac{m}{T}\rfloor ans=k=1∑up⎝⎛k∣T∑g(k)μ(kT)⎠⎞⌊Tn⌋⌊Tm⌋
记 f ( n ) = ∑ k ∣ n g ( k ) μ ( n k ) f(n)=\sum_{k|n}g(k)\mu(\frac n k) f(n)=∑k∣ng(k)μ(kn),也即 f = g ∗ μ f=g*\mu f=g∗μ。
由于在整除分块的时候,我们需要知道一段区间的 f f f 值之和,那么不妨考虑算 f f f 的前缀和。
然后 n ≤ 1 0 10 n\le10^{10} n≤1010,自然能想到杜教筛。由于 f ∗ 1 = g ∗ μ ∗ 1 = g ∗ ϵ = g f*1=g*\mu*1=g*\epsilon=g f∗1=g∗μ∗1=g∗ϵ=g,所以(记 S S S 为 f f f 的前缀和):
∑ i = 1 n ∑ d ∣ i f ( i d ) = ∑ i = 1 n g ( i ) ∑ d = 1 n ∑ i = 1 ⌊ n d ⌋ f ( i ) = ∑ i = 1 n g ( i ) ∑ d = 1 n S ( ⌊ n d ⌋ ) = ∑ i = 1 n g ( i ) S ( n ) = ∑ i = 1 n g ( i ) − ∑ d = 2 n S ( ⌊ n d ⌋ ) \begin{aligned} \sum_{i=1}^n\sum_{d|i}f(\frac i d)&=\sum_{i=1}^ng(i)\\ \sum_{d=1}^n\sum_{i=1}^{\lfloor\frac n d\rfloor}f(i)&=\sum_{i=1}^ng(i)\\ \sum_{d=1}^nS(\lfloor\frac n d\rfloor)&=\sum_{i=1}^ng(i)\\ S(n)&=\sum_{i=1}^ng(i)-\sum_{d=2}^nS(\lfloor\frac n d\rfloor) \end{aligned} i=1∑nd∣i∑f(di)d=1∑ni=1∑⌊dn⌋f(i)d=1∑nS(⌊dn⌋)S(n)=i=1∑ng(i)=i=1∑ng(i)=i=1∑ng(i)=i=1∑ng(i)−d=2∑nS(⌊dn⌋)
然后就是要快速求 g g g 的前缀和了,组合数有一个性质,即 ∑ i = 1 n ( i m ) = ( n + 1 m + 1 ) \sum_{i=1}^n\binom im=\binom{n+1}{m+1} ∑i=1n(mi)=(m+1n+1)(证明可以参考这里)。
又由于模数小,用 Lucas 定理求即可。
然后时间复杂度据 zxyoi 说是 O ( n 3 4 ) O(n^{\frac34}) O(n43) 的,可以用积分估计。
#include
using namespace std;
typedef long long ll;
const int N=3e6+5,P=9990017,inv2=4995009;
int B,ans,fac[P],ifac[P];
int add(int x,int y) {return x+y>=P?x+y-P:x+y;}
int dec(int x,int y) {return x-y< 0?x-y+P:x-y;}
int mul(int x,int y) {return 1ll*x*y>=P?1ll*x*y%P:x*y;}
int power(int a,int b){
int ans=1;
for(;b;b>>=1,a=mul(a,a)) if(b&1) ans=mul(ans,a);
return ans;
}
int C(int n,int m) {return n<m?0:mul(fac[n],mul(ifac[m],ifac[n-m]));}
int Lucas(ll n,int m){
if(n<P&&m<P) return C(n,m);
return mul(Lucas(n/P,m/P),C(n%P,m%P));
}
void prework(){
fac[0]=fac[1]=1;
for(int i=2;i<P;++i) fac[i]=mul(fac[i-1],i);
ifac[P-1]=power(fac[P-1],P-2);
for(int i=P-2;~i;--i) ifac[i]=mul(ifac[i+1],i+1);
}
int sum,prime[N],mark[N],mu[N],f[N];
void linear_sieves(){
mu[1]=1;
for(int i=2;i<N;++i){
if(!mark[i]) prime[++sum]=i,mu[i]=P-1;
for(int j=1;j<=sum&&i*prime[j]<N;++j){
mark[i*prime[j]]=1;
if(i%prime[j]) mu[i*prime[j]]=P-mu[i];
else {mu[i*prime[j]]=0;break;}
}
}
for(int i=1;i<N;++i)
for(int j=1;i*j<N;++j) f[i*j]=add(f[i*j],mul(C(i,B),mu[j]));
for(int i=1;i<N;++i) f[i]=add(f[i],f[i-1]);
}
unordered_map<ll,int>Sum;
int S(ll n){
if(n<N) return f[n];
if(Sum.count(n)) return Sum[n];
int ans=Lucas(n+1,B+1);
for(ll i=2,j;i<=n;i=j+1){
j=n/(n/i);
ans=dec(ans,mul((j-i+1)%P,S(n/i)));
}
return Sum[n]=ans;
}
ll n,m,up;
int main(){
scanf("%lld%lld%d",&n,&m,&B);
prework(),linear_sieves(),up=min(n,m);
for(ll i=1,j;i<=up;i=j+1){
j=min(n/(n/i),m/(m/i));
ans=add(ans,mul(dec(S(j),S(i-1)),mul((n/i)%P,(m/i)%P)));
}
printf("%d\n",ans);
return 0;
}
这道题的建图思路非常妙。
对于一个右端点,找到最近的左端点使得他们构成一个合法区间,并且定义他的父亲为最近左端点左边的点。
那么我们会建出一个森林,那么这个树形结构有什么性质呢。
考虑一个合法区间,他要么是一个极小合法区间,要么是多个合法区间的并。那在这棵树上,该右端点到根路径上的点都可以是合法左端点,那么他的答案就是他的深度。
那么如果有两个右端点,那就是要找他们的合法区间交集的数量,求个 lca 即可。
时间复杂度 O ( n log n ) O(n\log n) O(nlogn)。
#include
using namespace std;
namespace IO{
const int Rlen=1<<22|1;
char buf[Rlen],*p1,*p2;
inline char gc(){
return (p1==p2)&&(p2=(p1=buf)+fread(buf,1,Rlen,stdin),p1==p2)?EOF:*p1++;
}
template<typename T>
inline T Read(){
char c=gc();T x=0,f=1;
while(!isdigit(c)) f^=(c=='-'),c=gc();
while( isdigit(c)) x=(x+(x<<2)<<1)+(c^48),c=gc();
return f?x:-x;
}
inline int in() {return Read<int>();}
}
using IO::in;
const int N=2e6+5;
int n,m;
int p[N],L[N],R[N],rt[N],dep[N],fa[N][21],father[N];
stack<int>stk;
vector<int>e[N];
void Max(int &x,int y) {if(x<y)x=y;}
void Min(int &x,int y) {if(x>y)x=y;}
void dfs(int x,int root){
rt[x]=root;
for(int i=1;i<=20;++i) fa[x][i]=fa[fa[x][i-1]][i-1];
for(int &to:e[x]){
dep[to]=dep[x]+1,fa[to][0]=father[to];
dfs(to,root);
}
}
void build(){
for(int i=1;i<=(n<<1);++i){
L[i]=min(i,p[i]),R[i]=max(i,p[i]);
while(!stk.empty()&&stk.top()>=L[i]){
int x=stk.top();stk.pop();
Min(L[i],L[x]),Max(R[i],R[x]);
}
stk.push(i);
}
for(int i=0;i<=(n<<1);++i) rt[i]=father[i]=-1;
for(int i=1;i<=(n<<1);++i) if(R[i]==i){
father[i]=L[i]-1;
e[L[i]-1].push_back(i);
}
for(int i=0;i<=(n<<1);++i) if(!e[i].empty()&&rt[i]==-1) dep[i]=1,dfs(i,i);
}
int LCA(int x,int y){
if(dep[x]<dep[y]) swap(x,y);
for(int i=20;~i;--i) if(dep[fa[x][i]]>=dep[y]) x=fa[x][i];
if(x==y) return x;
for(int i=20;~i;--i) if(fa[x][i]!=fa[y][i]) x=fa[x][i],y=fa[y][i];
return fa[x][0];
}
int Query(int r1,int r2){
return (rt[r1]==-1||rt[r2]==-1||rt[r1]!=rt[r2])?0:dep[LCA(r1,r2)]-1;
}
int main(){
n=in(),m=in();
for(int i=1;i<=(n<<1);++i) p[i]=in();
build();
while(m--){
int r1=in(),r2=in();
printf("%d\n",(!r1&&!r2)?0:Query(r1,r2));
}
return 0;
}
设 f [ i ] [ j ] f[i][j] f[i][j] 表示给 i i i 个点染 j j j 种颜色的方案数,那么:
f [ i ] [ j ] = f [ i − 1 ] [ j − 1 ] + f [ i − 1 ] [ j ] × ( j − 1 ) f[i][j]=f[i-1][j-1]+f[i-1][j]\times(j-1) f[i][j]=f[i−1][j−1]+f[i−1][j]×(j−1)
就是考虑第 i i i 个是染一个新颜色还是染之前出现过的颜色。
又设 g [ i ] [ j ] g[i][j] g[i][j] 表示 d p dp dp 到第 i i i 行,且第 i i i 行染 j j j 种颜色的方案数,考虑用 g [ i − 1 ] [ k ] g[i-1][k] g[i−1][k] 来更新 g [ i ] [ j ] g[i][j] g[i][j],则:
{ g [ i ] [ j ] ⟵ g [ i − 1 ] [ k ] × ( m j ) × f [ a i ] [ j ] × j ! , j ≠ k g [ i ] [ j ] ⟵ g [ i − 1 ] [ k ] × ( ( m j ) − 1 ) × f [ a i ] [ j ] × j ! , j = k \begin{cases} g[i][j]\longleftarrow g[i-1][k]\times \binom m j\times f[a_i][j]\times j!,&j\ne k\\ g[i][j]\longleftarrow g[i-1][k]\times (\binom m j-1)\times f[a_i][j]\times j!,&j= k \end{cases} {g[i][j]⟵g[i−1][k]×(jm)×f[ai][j]×j!,g[i][j]⟵g[i−1][k]×((jm)−1)×f[ai][j]×j!,j=kj=k
( m j ) \binom m j (jm) 是在 m m m 种颜色中选当前的 j j j 中颜色,乘上预处理的 f [ a i ] [ j ] f[a_i][j] f[ai][j] 的方案,而乘 j ! j! j! 是因为这是无序枚举。
由于模数不一定是质数,所以不一定会有逆元,不能直接预处理组合数。但是,由于要乘 j ! j! j!,会把除的消掉,不用管。
时间复杂度 O ( S ) O(S) O(S)。
#include
using namespace std;
namespace IO{
const int Rlen=1<<22|1;
char buf[Rlen],*p1,*p2;
inline char gc(){
return (p1==p2)&&(p2=(p1=buf)+fread(buf,1,Rlen,stdin),p1==p2)?EOF:*p1++;
}
template<typename T>
inline T Read(){
char c=gc();T x=0,f=1;
while(!isdigit(c)) f^=(c=='-'),c=gc();
while( isdigit(c)) x=(x+(x<<2)<<1)+(c^48),c=gc();
return f?x:-x;
}
inline int in() {return Read<int>();}
}
using IO::in;
const int N=1e6+5,M=5005;
int n,m,P;
int add(int x,int y) {return x+y>=P?x+y-P:x+y;}
int dec(int x,int y) {return x-y< 0?x-y+P:x-y;}
int mul(int x,int y) {return 1ll*x*y>=P?1ll*x*y%P:x*y;}
void Add(int &x,int y) {x=add(x,y);}
void Max(int &x,int y) {if(x<y)x=y;}
void Min(int &x,int y) {if(x>y)x=y;}
int mx,a[N],C[N],fac[M],f[M][M],g[2][M];
int main(){
n=in(),m=in(),P=in();
for(int i=1;i<=n;++i) a[i]=in(),Max(mx,a[i]);
f[0][0]=fac[0]=1;
for(int i=1;i<=mx;++i) fac[i]=mul(fac[i-1],i);
for(int i=1;i<=mx;++i)
for(int j=1;j<=mx;++j)
f[i][j]=add(f[i-1][j-1],mul(f[i-1][j],j-1));
C[1]=m;
for(int i=2;i<=m;++i) C[i]=mul(C[i-1],m-i+1);
int t=0,sum=1;
for(int i=1;i<=n;++i,t^=1){
memset(g[t],0,sizeof(g[t]));
for(int j=0;j<=a[i];++j)
Add(g[t][j],mul(dec(mul(sum,C[j]),mul(g[t^1][j],fac[j])),f[a[i]][j]));
sum=0;
for( int j=0;j<=a[i];++j) Add(sum,g[t][j]);
}
printf("%d\n",sum);
return 0;
}
感觉不算很难,但是比较妙。
对 n n n 个数排序可以看成两步:先对前 n − 1 n-1 n−1 个数排序,再把第 n n n 个数排到对应的位置。
根据题目要求的排序方式,只有当 1 ∼ n − 1 1\sim n-1 1∼n−1 都排好了才会扫到 n n n 来排第 n n n 个。
那么有两种情况:
所以从 f n − 1 f_{n-1} fn−1 到 f n f_n fn 总共有 2 0 + 2 1 + ⋯ + 2 n − 2 = 2 n − 1 − 1 2^0+2^1+\cdots+2^{n-2}=2^{n-1}-1 20+21+⋯+2n−2=2n−1−1 的贡献,再乘个逆元即可。
时间复杂度 O ( n ) O(n) O(n)。
#include
using namespace std;
const int N=1e6+5,P=1e9+7;
int n,Mul=2,f[N],inv[N];
int add(int x,int y) {return x+y>=P?x+y-P:x+y;}
int dec(int x,int y) {return x-y< 0?x-y+P:x-y;}
int mul(int x,int y) {return 1ll*x*y>=P?1ll*x*y%P:x*y;}
int main(){
scanf("%d",&n),inv[1]=1;
for(int i=2;i<=n;++i){
inv[i]=mul(P-P/i,inv[P-P/i*i]);
f[i]=add(f[i-1],mul(dec(Mul,1),inv[i])),Mul=mul(Mul,2);
}
printf("%d\n",f[n]);
return 0;
}