LGP6276题解
众所周知,排列是一个置换,一个置换是一车环。
步数就是这些环长的 \(lcm\)。
如果你去思考直接 DP,会发现很困难,根本设不出来状态。于是考虑正难则反:每个质数幂 \(p^k\) 对答案的贡献。
可以发现,如果设 \(f[p^k]\) 是 \(p^k\) 在 \(f[p^k]\) 个排列的 \(lcm\) 中出现过,那么答案是 \(\sum_{p^k\leq n}p^{f[p^k]}\)。
考虑去计算这个 \(f\)。计算出现过似乎也很困难,正难则反计算没出现过的。(后文令 \(t=p^k\))
然后有一个显然的 DP:设 \(dp[n]\) 为 \(n\) 个元素构成的置换群中,没有一个环的长度是 \(t\) 的倍数。那么似乎只需要从非 \(t\) 的倍数转移过来就可以了。
但是我们并不会计算新加入若干个元素会产生多少个新排列。考虑原本有 \(n\) 个元素的情况下加入了一个大小为 \(m\) 的环。
我们强制钦定 \(1\) 必须在这个环中,那么我们还需要从 \([2,n+m]\) 中选出 \([m-1]\) 个数,然后有 \((m-1)!\) 种不同的排列方式。所以方案数乘上了 \(\binom{n+m-1}{m-1}(m-1)!=\frac{(n+m-1)!}{n!}\)。
然后我们好像会了 \(O(n^3)\) 计算这个玩意儿(
考虑优化,正难则反。我们不计算非 \(t\) 倍数位置转移过来的值,考虑维护前面所有位置转移过来的值减去 \(t\) 倍数转移过来的值。这个是很好维护的。
于是做到了 \(O(n^2)\)。
#include<cstdio>
typedef unsigned ui;
typedef __uint128_t LL;
typedef unsigned long long ull;
const ui M=7505;
struct Barrett{
ull m,b;
Barrett(const ui&m=1):m(m),b(((LL(1)<<80)+m-1)/m){}
friend inline ull operator%(const ull&a,const Barrett&mod){
return a-mod.m*(LL(mod.b)*a>>80);
}
}mod,MOD;
ui n,m,tm,TM,c[M],S[M],dp[M],minp[M];
inline ui pow(ui a,ui b){
ui ans(1);for(;b;b>>=1,a=1ull*a*a%mod)if(b&1)ans=1ull*ans*a%mod;return ans;
}
signed main(){
ui ans(1),fac(1);
scanf("%u%u",&n,&m);minp[1]=1;
mod=Barrett(tm=m);MOD=Barrett(TM=m-1);
for(ui i=1;i<=n;++i)c[i]=1,fac=1ull*fac*i%MOD;
for(ui i=2;i<=n;++i)if(!minp[i]){
for(ui j=i*i;j<=n;j+=i)minp[j]=1;
for(ui j=i;j<=n;j*=i)minp[j]=i;
}
for(ui i=2;i<=n;++i){
for(ui j=1;j+i-1<=n;++j)c[j]=1ull*c[j]*(i+j-2)%MOD;
if(minp[i]<=1)continue;
const ui&pk=i;ui sum(1);
dp[0]=1;S[0]=1;
for(ui i=1;i<=n;++i){
if(pk<=i)S[i]=1ull*S[i-pk]*c[i-pk+1]%MOD;else S[i]=0;
dp[i]=(TM+sum-S[i])%MOD;
S[i]=(1ull*S[i]*i+dp[i])%MOD;
sum=(1ull*sum*i+dp[i])%MOD;
}
ans=1ull*ans*pow(minp[i],TM+fac-dp[n])%mod;
}
printf("%u",ans);
}
但是这样真的够快吗?
考虑生成函数。
置换是一个典型的有标号计数问题。只需要计算出一个环的 EGF 再将其 \(\exp\) 即可。
而一个环的 EGF 显然是 \(\sum_{i=1}\frac{(i-1)!}{i!}x^i-\sum_{i=1}\frac{(it-1)!}{(it)!}x^{it}\)。
注意到 \(\frac{1}{1-x}\) 是前缀和,分子又只在 \(t\) 的倍数处有值,所以可以考虑改写一下这个东西:
剩下的就交给广义二项式定理吧。
设 \(m=\lfloor\frac{n}{t}\rfloor\):
但是这个是在指数上做的,所以模数并不是质数。
注意到这个 \(it\) 将 \(n!\) 分成了 \(m+1\) 段,可以将 \(n!\) 看做一个序列,我们只需要支持查询区间乘积即可。
使用猫树可以做到 \(O(n\log n)\) 的复杂度。
#include<cstdio>
typedef unsigned ui;
typedef __uint128_t LL;
typedef unsigned long long ull;
const ui M=7505;
ui n,m,minp[M];
struct Barrett{
ull m,b;
Barrett(const ui&m=1):m(m),b(((LL(1)<<80)+m-1)/m){}
friend inline ull operator%(const ull&a,const Barrett&mod){
return a-mod.m*(LL(mod.b)*a>>80);
}
}mod,MOD;
struct DS{
ui D,len,lg[8192],prod[15][8192];
inline void init(const ui&n){
D=0;len=1;
while((1<<D)<=n)++D,len<<=1;
for(ui i=1;i<=len;++i)prod[0][i]=i;
for(ui i=1;i<=len;++i)lg[i]=lg[i>>1]+1;
for(ui d=1;d<=D;++d){
for(ui k=1;k<=(len>>d);++k){
const ui&L=k-1<<d,&R=k<<d,&mid=L+R>>1;
prod[d][mid]=mid;prod[d][mid+1]=mid+1;
for(ui i=mid-1;i>=L+1;--i)prod[d][i]=1ull*prod[d][i+1]*i%MOD;
for(ui i=mid+1+1;i<=R;++i)prod[d][i]=1ull*prod[d][i-1]*i%MOD;
}
}
}
inline ui operator()(const ui&L,const ui&R){
if(L>R)return 1;if(L==R)return L;
ui d=lg[L-1^R-1];return 1ull*prod[d][L]*prod[d][R]%MOD;
}
}SGT;
inline ui pow(ui a,ui b){
ui ans(1);for(;b;b>>=1,a=1ull*a*a%mod)if(b&1)ans=1ull*ans*a%mod;return ans;
}
inline ui Solve(const ui&k){
const ui&m=n/k;ui ans;ans=SGT(m*k+1,n);
for(ui i=1;i<=m;++i)ans=1ull*ans*SGT((i-1)*k+1,i*k-1)%MOD,ans=1ull*ans*(i*k-1)%MOD;
return ans;
}
signed main(){
ui ans(1),fac(1);
scanf("%u%u",&n,&m);
mod=Barrett(m);MOD=Barrett(m-1);SGT.init(n);
for(ui i=1;i<=n;++i)fac=1ull*fac*i%MOD;
for(ui i=2;i<=n;++i){
if(!minp[i]){
for(ui j=i*i;j<=n;j+=i)minp[j]=1;
for(ui j=i;j<=n;j*=i)minp[j]=i;
}
if(minp[i]>1)ans=1ull*ans*pow(minp[i],m-1+fac-Solve(i))%mod;
}
printf("%u",ans);
}