USACO Exercise
orangejuice · · 题解
「USACO 2020 US Open Platinum」Exercise
做法与模数是否是质数无关
问题可能比较复杂,需要多步分析
1.对于一个已知的排列
显然这样的置换会构成若干个环,设每个环长度为
2.对于已知的
考虑已经排列的个数为
为了避免重复,应当固定这个环的初始位置为1号点,其余位置按照原先顺序插入
则方案数可以分为两部分考虑:
2-1.环内排列,固定的环首不可排列,即
2-2.剩下的
即
所以就是
归纳一下,发现更形象的描述就是
也就是每次除掉转移时的大小,将
3.计算
考虑对于每个质因数计算其出现的幂次,注意这个幂次是对于
原先是求恰好包含
求质因数
Solution 1
那么反向求解,令
暴力转移,枚举
优化1:
考虑分解系数
这样单次求解复杂度为
优化2:
不枚举
如何将系数
每次
当
也就是模拟了上面提到的把
这样就去掉了阶乘逆元的求解
Tips:发现需要预处理
不同的
因此复杂度为
可以看到代码还是很简单的
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define Mod2(x) ((x<0)&&(x+=P2))
#define rep(i,a,b) for(int i=a,i##end=b;i<=i##end;++i)
#define drep(i,a,b) for(int i=a,i##end=b;i>=i##end;--i)
const int N=7510;
int n,P,P2;
int mk[N];
int s[N],T[N],dp[N];
ll qpow(ll x,ll k=P-2) {
ll res=1;
for(;k;k>>=1,x=x*x%P) if(k&1) res=res*x%P;
return res;
}
int main(){
scanf("%d%d",&n,&P),P2=P-1;
int ans=1;
rep(i,2,n) if(!mk[i]) {
for(int j=i+i;j<=n;j+=i) mk[j]=1;
for(int x=i;x<=n;x*=i) mk[x]=i;
}
rep(i,1,n) T[i]=1;
int r=1;
rep(i,1,n) r=1ll*r*i%P2;
rep(x,2,n) {
rep(j,1,n-x+1) T[j]=1ll*T[j]*(j+x-2)%P2;
// 滚动求解区间乘积
if(mk[x]<=1) continue;
dp[0]=1,s[0]=1;
int sum=1;
rep(i,1,n) {
s[i]=0;
if(i>=x) s[i]=1ll*s[i-x]*T[i-x+1]%P2;
dp[i]=sum-s[i]; Mod2(dp[i]);
s[i]=(1ll*s[i]*i+dp[i])%P2;
sum=(1ll*sum*i+dp[i])%P2;
}
ans=1ll*ans*qpow(mk[x],P2+r-dp[n])%P;
}
printf("%d\n",ans);
}
Solution 2
为了便于表达,设满足条件为至少出现一个
实际用min-max容斥确实比较好理解,设对于集合
简要证明的话:
把
对于
对于剩下的值
要计算最大值为1的方案数,那么就要计算最小值为1的子集方案数
考虑强制一个子集中每一个环大小均为
则实际对答案的贡献还要考虑这样的子集出现的次数
考虑选择子集的位置,以及剩下的
如果真的用
因此可以直接记录大小
(可以看到依然需要访问上面提到的
这样的
实际上这样的复杂度已经足够了,是
以下是
#include<bits/stdc++.h>
using namespace std;
#define rep(i,a,b) for(int i=a;i<=b;++i)
#define drep(i,a,b) for(int i=a;i>=b;--i)
typedef long long ll;
const int N=7510;
int n,P,P2,T[N][N],mk[N],dp[N];
ll qpow(ll x,ll k) {
ll res=1;
for(;k;k>>=1,x=x*x%P) if(k&1) res=res*x%P;
return res;
}
int Calc(int x){
int m=n/x,s=P2-1;
rep(i,1,m) {
s=1ll*s*T[(i-1)*x+1][i*x-1]%P2;
dp[i]=P2-s;
s=(1ll*s*i*x+dp[i])%P2;
}
int ans=0;
rep(i,1,m) ans=(ans+1ll*dp[i]*T[i*x+1][n])%P2;
return ans;
}
int main(){
scanf("%d%d",&n,&P),P2=P-1;
rep(i,2,n) if(!mk[i]) {
for(int j=i;j<=n;j+=i) mk[j]=1;
for(int j=i;j<=n;j*=i) mk[j]=i;
}
rep(i,1,n+1){
T[i][i-1]=1;
rep(j,i,n) T[i][j]=1ll*T[i][j-1]*j%P2;
}
int ans=1;
rep(i,2,n) if(mk[i]>1) ans=ans*qpow(mk[i],Calc(i))%P;
printf("%d\n",ans);
}
最终优化
可以看到,这个做法和Sol1的转移有十分的相同之处,因此考虑用同样的方法优化掉
但是预处理系数的部分依然是
1.线段树大法
预处理复杂度为
空间复杂度为
Oh这个代码是ZKW线段树
#include<bits/stdc++.h>
using namespace std;
#define rep(i,a,b) for(int i=a;i<=b;++i)
typedef long long ll;
const int N=7510;
int n,P,P2;
struct Tree {
int s[N<<2],bit;
void Build() {
for(bit=1;bit<=n;bit<<=1);
rep(i,1,n) s[i+bit]=i;
for(int i=bit;i>=1;--i) s[i]=1ll*s[i<<1]*s[i<<1|1]%P2;
}
int Que(int l,int r){
if(l>r) return 1;
if(l==r) return l;
int res=1;
for(l+=bit-1,r+=bit+1;l^r^1;l>>=1,r>>=1){
if(~l&1) res=1ll*res*s[l^1]%P2;
if(r&1) res=1ll*res*s[r^1]%P2;
}
return res;
}
} T;
int mk[N];
ll qpow(ll x,ll k) {
ll res=1;
for(;k;k>>=1,x=x*x%P) if(k&1) res=res*x%P;
return res;
}
int dp[N];
int Calc(int x){
int m=n/x,s=P2-1;
rep(i,1,m) {
s=1ll*s*T.Que((i-1)*x+1,i*x-1)%P2;
dp[i]=P2-s;
s=(1ll*s*i*x+dp[i])%P2;
}
int ans=0;
rep(i,1,m) ans=(ans+1ll*dp[i]*T.Que(i*x+1,n))%P2;
return ans;
}
int main(){
scanf("%d%d",&n,&P),P2=P-1;
T.Build();
rep(i,2,n) if(!mk[i]) {
for(int j=i;j<=n;j+=i) mk[j]=1;
for(int j=i;j<=n;j*=i) mk[j]=i;
}
int ans=1;
rep(i,2,n) if(mk[i]>1) ans=ans*qpow(mk[i],Calc(i))%P;
printf("%d\n",ans);
}
2.模数分解+CRT(Chinese Remainder Theory)
分解后,可以用模逆元处理,然后就直接做,最后CRT合并一下,其实我并不会实现。。。
3.猫树(嘿嘿)
这是一个
因此复杂度为
#include<bits/stdc++.h>
using namespace std;
#define rep(i,a,b) for(int i=a;i<=b;++i)
#define drep(i,a,b) for(int i=a;i>=b;--i)
typedef long long ll;
const int N=7510;
int n,P,P2;
struct SuckCat{
int s[14][8200],Log[8200],bit;
void Build() {
for(bit=1;bit<=n;bit<<=1);
rep(i,2,bit) Log[i]=Log[i>>1]+1;
for(int l=1,d=0;l*2<=bit;l<<=1,d++){
for(int i=l;i<=bit;i+=l*2){
s[d][i-1]=i-1,s[d][i]=i;
drep(j,i-2,i-l) s[d][j]=1ll*s[d][j+1]*j%P2;
rep(j,i+1,i+l-1) s[d][j]=1ll*s[d][j-1]*j%P2;
}
}
}
int Que(int l,int r){
if(l>r) return 1;
if(l==r) return l;
int d=Log[l^r];
return 1ll*s[d][l]*s[d][r]%P2;
}
} T;
int mk[N];
ll qpow(ll x,ll k) {
ll res=1;
for(;k;k>>=1,x=x*x%P) if(k&1) res=res*x%P;
return res;
}
int dp[N];
int Calc(int x){
int m=n/x,s=P2-1;
rep(i,1,m) {
s=1ll*s*T.Que((i-1)*x+1,i*x-1)%P2;
dp[i]=P2-s;
s=(1ll*s*i*x+dp[i])%P2;
}
int ans=0;
rep(i,1,m) ans=(ans+1ll*dp[i]*T.Que(i*x+1,n))%P2;
return ans;
}
int main(){
scanf("%d%d",&n,&P),P2=P-1;
T.Build();
rep(i,2,n) if(!mk[i]) {
for(int j=i;j<=n;j+=i) mk[j]=1;
for(int j=i;j<=n;j*=i) mk[j]=i;
}
int ans=1;
rep(i,2,n) if(mk[i]>1) ans=ans*qpow(mk[i],Calc(i))%P;
printf("%d\n",ans);
}