题意概括:
给你一个长度为 \(n\) 的序列,以及 \(m\) 个询问,每次询问给出询问区间 \(l\) 以及 \(r\),试求 \(l\sim r\) 之间所有数的 \(\gcd\) 是多少。
题解:
这题方法真多啊(
先总结一下几种已经出现了的方法。
1.线段树or树状数组
这俩是挺经典的求区间的工具,在这里就不多赘述,可以去看别的大佬的题解。
2.dp
对于每个区间可以用\(O(n^2)\)的复杂度转移出来,然后询问就可以O(1)了,这个方法挺快,但是如果把 \(n\) 也提起来,就废了。
3.分块
这个我喜欢,分块是一种优雅的暴力,待会讲我的方法的时候会用到,大概就是把序列分成很多个块,然后每个块预处理一下,最后统计的时候整块算,大概复杂度是 \(O(\dfrac{n^2}{S}+mS)\),S是块长。这里的 S 一般取 \(\sqrt{n}\)。但是在这么大的 \(m\) 面前,大概也是会被卡的。
4.ST表
待会也要用到,ST表是一种神奇的方法,不会的可以看 这里,他不仅能处理max/min,甚至可以处理一切有结合律的运算。
然后就是重头戏了:
5.lxl 的 ST 表
听起来好高级的亚子,毒瘤劝退
这种方法也就是我这题真正要讲的方法,就是分块套ST表,可以在保证空间为线性时,用期望 \(O(n+m)\) 的复杂度内求出 ST表 能求出的东西。虽然实际是可以卡到 \(O(n+m\sqrt{n})\) 的,但是在用的时候其实跑的挺快,下面步入正题。
我们发现,其实ST表的空间是很大的,比如在1e7这样的数据面前,ST表基本就没了。所以我们考虑优化ST的空间。我们讲序列分成一块一块的,每次只预处理出每个整块的ST,这样我们的空间就降到了 \(O(\sqrt{n}log\sqrt{n})\),时间也是一样的。之后对于每个块,我们都可以直接取出答案,复杂度为 \(O(1)\) 。
其实到这里就可以开始写了,但是我们考虑一个问题,就是两边不完整的块该怎么办。如果暴力求,那么时间复杂度和普通分块是没有两样的,于是我们可以预处理出 每一块 的 前缀gcd 和 后缀gcd,可以参考下面这张图来理解:
每个不同的色块表示这个点代表预处理的gcd后缀的区间,前缀也是一样的,这样一来,我们就可以预先用 \(O(n)\) 的复杂度预处理出这些前后缀,再 \(O(1)\) 查询就OK了。
这个方法可以通过数据随机且 \(n=2e7,m=2e7,TL=5.00s,ML=500MB\)的题目,也就是这题。
上代码,上面没看懂的可以看程序:
#include<bits/stdc++.h>
using namespace std;
const int maxn=1005;
const int maxm=1e6+5;
const int block=50;
int n,m;int p;
inline int gcd(int a,int b)//gcd不解释
{
while(b^=a^=b^=a%=b);
return a;
}
int st[block][20],belong[maxn],a[maxn];//ST是ST表,belong是每个点属于的块,a是原序列
int pre[maxn],bk[maxn];//每块的前缀gcd和后缀gcd
int lg[maxn];//预处理log2,用于ST表
inline void init()//ST表的预处理,唯一和普通ST表不一样的地方就是它是到n我是到belong[n],也就是sqrtn
{
for(register int i=1;i<=n;i++)st[belong[i]][0]=a[i];
for(register int i=1;(1<<i)<=p;i++)
{
for(register int j=1;j+(1<<i)-1<=p;j++)
{
st[j][i]=gcd(st[j][i-1],st[j+(1<<(i-1))][i-1]);
}
}
}
inline int query(int l,int r)//询问
{
int ans=0;
if(belong[l]==belong[r]){for(int i=l;i<=r;i++)ans=gcd(ans,a[i]);return ans;}//如果在同一个块中,直接暴力找,因为这一块不好预处理,所以这也就是这题为什么会被卡到sqrtn的原因。
if(belong[r]-belong[l]==1){return gcd(pre[r],bk[l]);}//如果块相差为1,也就是说这个询问的区间可以分为你右端点属于块的前缀,以及左端点属于的块的后缀,可以自己画图理解一下。
else
{
int x=lg[belong[r]-belong[l]-1];
ans=gcd(st[belong[l]-1][x],st[belong[r]-(1<<x)][x]);
ans=gcd(ans,gcd(pre[r],bk[l]));
return ans;
}//ST表和前后缀的gcd查询,应该是ST表的基本操作。这里唯一不同的就是把原询问的区间的gcd变成了原询问块的gcd。
}
int main()
{
//听说从主函数开始阅读程序是个好习惯(?)
int size=50;scanf("%d %d",&n,&m);
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
for(int i=1;i<=n;i++)belong[i]=(i-1)/size+1;
lg[0]=-1;p=belong[n];
for(register int i=1;i<=p;i++)lg[i]=lg[i>>1]+1;//处理log
for(int i=1;i<=n;i++)pre[i]=(i%size^1)?gcd(pre[i-1],a[i]):a[i];//处理前缀,这里看不懂的话可以试着把它写成if,else的形式。
for(int i=n;i>=1;i--)bk[i]=(i%size)?gcd(bk[i+1],a[i]):a[i];//后缀,和前缀差不多。
init();//ST表预处理
//前面是一大堆的预处理
for(int i=1;i<=m;i++)
{
int l,r;scanf("%d %d",&l,&r);
printf("%d\n",query(l,r));
}//询问,输出,结束。
}
似乎这种方法的延伸性很强,各位大佬们可以下去自己尝试尝试其他的询问区间的题目。