洛谷 P1890 【gcd区间】

题意概括:

给你一个长度为 \(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));
    }//询问,输出,结束。
}

似乎这种方法的延伸性很强,各位大佬们可以下去自己尝试尝试其他的询问区间的题目。

猜你喜欢

转载自www.cnblogs.com/bovine-kebi/p/13369151.html