NOI模拟 刀客球
涉及知识点:数论,单调栈
题意
给你一串长度为 \(n\ (\leq10^5)\) 的序列,值域 \(\leq 10^7\),求该序列所有区间的 \(\text{lcm}\) 之积,对 \(10^9+7\) 取模。
思路
形如求 “所有区间” 的某种值的这种题目,肯定不能直接直接思考每个区间的贡献,因为但就枚举区间的基本复杂度 \(O(n^2)\) 是很难降下来的,一般这种情况有两种解决方式:
- \(O(n)\) 顺序遍历所有点,对于节点 \(i\) 可以在 \(O(logn)\) 或者 \(O(\sqrt{n})\) 的复杂度之内求出以 \(i\) 结尾的所有区间的贡献。
- \(O(n)\) 遍历所有节点,并且可以快速计算出节点 \(i\) 决定了哪些节点的贡献。本题采用的是这种做法。
首先考虑如何计算一个区间的 \(\text{lcm}\),直接利用 \(\text{lcm}(a,b,c)=\text{lcm}(\text{lcm}(a,b),c)\) 是不现实的,不同于 \(\gcd\) 会越操作越小,这样计算 \(\text{lcm}\) 会导致非常大的结果。但还有另外一种计算方式,即我们将每个数质因数分解,然后将每个质因数的次方数取区间最大值,最后相乘即可得到区间 \(\text{lcm}\),形式化地说,设 \(ai=p_1^{q_1}p_2^{q_2}\cdots p_k^{q_k}\),那么区间 \(a_l,\dots,a_r\) 的区间 \(\text{lcm}\) 即为 \(\Large {p_1^{\large\max\limits_{i=l\sim r}q_i}p_2^{\large\max\limits_{i=l\sim r}q_i}\cdots \ p_k^{\large\max\limits_{i=l\sim r}q_i}}\)。
于是我们想到可以分开计算每个质因数的贡献,对于第 \(i\) 个数 \(a_i\) 的第 \(j\) 个质因数 \(p_{i,j}\),记 \(a_i\) 分解后 \(p_{i,j}\) 上的指数为 \(q_{i,j}\),\(p_{i,j}\) 做出的贡献即为 \(a_i\) 往左往右拓展直到某个数质因数分解后 \(p_{i,j}\) 的指数大于 \(q_{i,j}\),形式化的讲,找到一个极大的区间 \([l,r]\) 使得 \(\forall t\in[l,r],at=p_1^{q_1}p_2^{q_2}\cdots p_k^{q_k}\),如果有 \(p_x=p_{i,j}\),满足 \(q_x\leq q_{i,j}\)(两边都取小于等于可能会导致重复计数,所以在实现时要左闭右开)。
实现
为了常数更优,我们将分解出的质因数分成两部分计算,以 \(\sqrt{\text{值域}}\) 为分界线,大于此分界线的质因数在每个数中最多出现 \(1\) 次,上文所述的”能拓展的区间“很显然就是左边第一个与右边第一个含有相同质因子的数,但是由于统计时”左开右闭“(代码里是左开右闭方便写,左闭右开也行),实际上计数时右端点是序列尾端点;而小于等于分界线的数通过单调栈实现 \(O(n)\) 找左右能拓展的最远点。
代码
#include<bits/stdc++.h>
using namespace std;
#ifdef ONLINE_JUDGE
#define getchar __getchar
inline char __getchar(){
static char ch[1<<20],*l,*r;
return (l==r&&(r=(l=ch)+fread(ch,1,1<<20,stdin),l==r))?EOF:*l++;
}
#endif
template<class T>inline void rd(T &x){
T res=0,f=1;
char ch=getchar();
while(ch<'0' || ch>'9'){if(ch=='-')f=-1; ch=getchar();}
while('0'<=ch && ch<='9'){res=res*10+ch-'0';ch=getchar();}
x=res*f;
}
template<class T>inline void wt(T x,char endch='\0'){
static char wtbuff[20];
static int wtptr;
if(x==0){
putchar('0');
}
else{
if(x<0){x=-x;putchar('-');}
wtptr=0;
while(x){wtbuff[wtptr++]=x%10+'0';x/=10;}
while(wtptr--) putchar(wtbuff[wtptr]);
}
if(endch!='\0') putchar(endch);
}
typedef long long LL;
const LL P=1e9+7;
const int MAXN=100005,MAXA=1e7;
int n,pre[MAXN],aft[MAXN],cnt[MAXN];
LL a[MAXN],ans=1;
inline LL qpow(LL x,LL y){
x%=P;
LL res=1;
while(y){
if(y&1) res=res*x%P;
x=x*x%P;
y>>=1;
}
return res;
}
int pri[MAXA],pri_cnt=0;
bool vis[MAXA+5];
inline void get_prime(){
for(int i=2;i<=MAXA;i++){
if(!vis[i]) pri[++pri_cnt]=i;
for(int j=1;j<=pri_cnt;j++){
if(1LL*pri[j]*i>MAXA) break;
vis[pri[j]*i]=1;
if(i%pri[j]==0) break;
}
}
}
int stk[MAXN],stp;
pair<int,int>tot[MAXN];
int totcnt=0;
int main(){
get_prime();
rd(n);
for(int i=1;i<=n;i++){
rd(a[i]);
}
for(int j=1;1LL*pri[j]*pri[j]<=MAXA;j++){
int it=pri[j];
for(int i=1;i<=n;i++){
cnt[i]=0;
while(a[i]%it==0){
a[i]/=it;
cnt[i]++;
}
}
stp=1;
stk[1]=0;
cnt[0]=0x3f3f3f3f;
for(int i=1;i<=n;i++){
while((stp) && (cnt[i]>cnt[stk[stp]])) stp--;
pre[i]=stk[stp];stk[++stp]=i;
}
stp=1;
stk[1]=n+1;
cnt[n+1]=0x3f3f3f3f;
for(int i=n;i>=1;i--){
while((stp) && (cnt[i]>=cnt[stk[stp]])) stp--;
aft[i]=stk[stp];stk[++stp]=i;
}
for(int i=1;i<=n;i++){
ans=ans*qpow(it,1LL*cnt[i]*(i-pre[i])*(aft[i]-i))%P;
}
}
for(int i=1;i<=n;i++){
if(a[i]>1) tot[++totcnt]=make_pair(a[i],i);
}
sort(tot+1,tot+1+totcnt);
for(int l=1,r,lst;l<=totcnt;){
r=l;
while(r+1<=totcnt && tot[l].first==tot[r+1].first) r++;
lst=0;
for(int i=l;i<=r;i++){
ans=ans*qpow(tot[i].first,1LL*(tot[i].second-lst)*(n-tot[i].second+1))%P;
lst=tot[i].second;
}
l=r+1;
}
wt(ans,'\n');
return 0;
}
处理小于分界线的数的循环:
枚举质因数 \(O(\sqrt R)\),分解质因数 \(O(\log n)\),单调栈 \(O(n)\),综合最大复杂度 \(O(n\sqrt R)\),\(R\) 为值域
处理大于分界线的数的循环:
双指针,\(O(n)\)