[牛客算法竞赛入门课第九节习题] 连续区间的最大公约数 线段树区间合并

本文探讨了如何利用线段树高效解决区间内的最大公约数查询问题,并介绍了如何维护额外信息以计算满足特定条件的子区间个数。关键步骤包括合并notGcd、区间长度和gcd值,以及维护左右扩展gcd的vector数组。
摘要由CSDN通过智能技术生成
题目链接:连续区间的最大公约数
题意

给你n个数 a 1 , a 2 , . . . . , a n {a_1,a_2,....,a_n} a1,a2,....,an,有q次询问,每次询问一个区间[l,r]的 g c d ( a l , a l + 1 , a l + 2 , . . . . , a r ) {gcd(a_l,a_{l+1},a_{l+2},....,a_r)} gcd(al,al+1,al+2,....,ar)以及该区间有多少个子区间的gcd值等于该区间的gcd值。(gcd:求最大公约数)

题解

很明显n≤1e5,q≤1e5暴力定超时,对于区间查询、修改这类问题绝大多数情况都用线段树解决。
本题只涉及区间查询,没有修改。所以对于查询区间gcd就很快乐,左右子树gcd值的gcd就能得到父亲节点的gcd值。但是还要求等于该区间的gcd的子区间个数,那就很不友好了。

  1. 为什么不友好呢?
    因为父亲节点中满足条件的子区间个数无法直接通过左右子树的答案中获得。还有可能左子树一部分、右子树一部分这些中间情况,所以为了解决问题,我们需要在节点中维护更多的信息来维护出中间情况

  2. 如何维护呢?
    由于直接求满足条件的不好求,我们可以先求出不满足条件的子区间数目,最后用(总区间数-不满足)的就是答案了。

    我们首先维护三个值,不是区间gcd的子区间数目、区间长度、区间gcd值。

    对于那些中间情况,我们可以维护两个vector l , r { l,r} l,r,L数组就是从左端点开始向右扩展gcd变化情况,例如L[k]就是 g c d ( a 1 , a 2 , . . . , a K ) {gcd(a_1,a_2,...,a_K)} gcd(a1,a2,...,aK);同理R数组就是从右端点开始向左扩展gcd变化情况,例如R[k]就是 g c d ( a n , a n − 1 , . . . , a n − k + 1 ) {gcd(a_n,a_{n-1},...,a_{n-k+1})} gcd(an,an1,...,ank+1)。由于连续 L k , L k + 1 , . . . {L_k,L_{k+1},...} Lk,Lk+1,...可能会出现相同的gcd值,所以我们每个vector维护两个值,一个是gcd、另一个是具有相同长度的值。

    struct Pairr
    {
    	ll gcd,len;
    	void init(ll gcd,ll len)
    	{
    		this->gcd=gcd;
    		this->len=len;
    	}
    };
    struct Tree
    {
    	ll notGcd,len,gcd;
    	vector<Pairr> l,r;
    }tree[maxn<<2];
    

现在我们知道了维护哪些信息,接下来就是如何通过左右子节点的答案得出父节点
左右子树的gcd和len很好向上pushup,问题是如何将左右两个区间的notGcd和两个vector数组向上合并。

在此我们先说一个结论:区间长度越长,gcd值只能变小或者不变。
这个其实很好理解,毕竟gcd的定义就是求最大公约数,公约数的大小就一定是≤该数本身。

第一步合并:notGcd,len,gcd

现在我们来合并区间更新notGcd(不是区间gcd的子区间数目),合并后有三部分:左区间值+右区间值+中间情况的值

前两个很好处理,以左区间为例:

  1. 如果左区间的gcd值==父亲节点的gcd值,那么合并后notGcd左部分=左区间的notGcd值。
  2. 否则,由上面那个结论得出,区间长的gcd值≤区间短的gcd值,由于现在左区间的gcd值≠父亲节点的gcd值,那么左区间的gcd值一定大于父亲节点的gcd值,所以合并后notGcd左部分=左区间的所有子区间个数

右区间同理

现在看中间部分,我们设置两个标记分别从左、右区间最右端开始。
题解描述
将右标记向左移,如果此时左标记到右标记区间的gcd≠父节点的gcd,此时中间部分值+=左标记里维护的长度*右标记到右区间最左端的距离。因为无论右标记如何往左移动,后面的一定不等于父节点。(此时区间大的都不等于区间小的更不会等于。)
然后将左标记左移,此时注意右标记不用重新更新到右区间的最右端,(原理和上面结论一样,自行思考)。然后同样移动右标记直到找到≠父节点gcd,中间部分值+=左标记里维护的长度*右标记到右区间最左端的距离
以此类推,我们不难发现用最少的时间复杂度求出了中间情况的答案。

== 第二步合并:vector<Pairr> l,r ==

我们以合并vector<Pairr> l为例
很显然赋初值 fa.l=lchild.l
现在我们只需把右区间的gcd加在fa.l里面即可
所以从左往右遍历右区间,如果gcd值是前面的倍数,则直接更改前一个长度;否则,添加新的值。
合并vector<Pairr> r亦如此,读者可自行思考。

合并完后剩下的就没什么难度了,正常查询即可。
所以综上所述,本题的难点在于区间合并操作,熟练地运用线段树也是向其进阶的一部分。加油!

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<bitset>
#include<cassert>
#include<cctype>
#include<cmath>
#include<cstdlib>
#include<ctime>
#include<deque>
#include<iomanip>
#include<list>
#include<map>
#include<queue>
#include<set>
#include<stack>
#include<vector>
#include<unordered_set>
#include<unordered_map>
using namespace std;
//extern "C"{void *__dso_handle=0;}
typedef long long ll;
typedef long double ld;
#define fi first
#define se second
#define pb push_back
#define mp make_pair
#define pii pair<int,int>
#define lowbit(x) x&-x

const double PI=acos(-1.0);
const double eps=1e-6;
const ll mod=1e9+7;
const int inf=0x3f3f3f3f;
const int maxn=1e5+10;
const int maxm=100+10;
#define ios ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);

ll a[maxn];
ll GCD(ll a,ll b) { return b==0? a:GCD(b,a%b); }

struct Pairr
{
	ll gcd,len;
	void init(ll gcd,ll len)
	{
		this->gcd=gcd;
		this->len=len;
	}
};
struct Tree
{
	ll notGcd,len,gcd;
	vector<Pairr> l,r;
}tree[maxn<<2];

void pushup(Tree& fa,Tree& lchild, Tree& rchild)
{
	fa.gcd=GCD(lchild.gcd, rchild.gcd);
	fa.len=lchild.len+rchild.len;
	fa.notGcd=(fa.gcd==lchild.gcd ? lchild.notGcd :(1+lchild.len)*lchild.len/2);
	fa.notGcd+=(fa.gcd==rchild.gcd ? rchild.notGcd :(1+rchild.len)*rchild.len/2);
	//中间情况
	ll tot=rchild.len,last=rchild.l.size()-1;
	for(int i=0;i<lchild.r.size();i++)
	{
		while (last>=0 && GCD(rchild.l[last].gcd, lchild.r[i].gcd)==fa.gcd) {
			tot-=rchild.l[last--].len;
		}
		fa.notGcd+=tot*lchild.r[i].len;
	}
	//更新father的l
	fa.l=lchild.l,fa.r=rchild.r;
	for(int i=0;i<rchild.l.size();i++)
	{
		if(rchild.l[i].gcd%fa.l.back().gcd==0)
			fa.l.back().len+=rchild.l[i].len;
		else
		{
			Pairr tmp; tmp.init(GCD(fa.l.back().gcd, rchild.l[i].gcd),rchild.l[i].len);
			fa.l.push_back(tmp);
		}
	}
	//更新father的r
	for(int i=0;i<lchild.r.size();i++)
	{
		if(lchild.r[i].gcd%fa.r.back().gcd==0)
			fa.r.back().len+=lchild.r[i].len;
		else
		{
			Pairr tmp; tmp.init(GCD(fa.r.back().gcd, lchild.r[i].gcd), lchild.r[i].len);
			fa.r.push_back(tmp);
		}
	}
}

void build(int p,int l,int r)
{
	if(l==r)
	{
		tree[p].notGcd=0;
		tree[p].len=1;
		tree[p].gcd=a[l];
		tree[p].l.clear();
		tree[p].r.clear();
		Pairr tmp; tmp.init(a[l],1);
		tree[p].l.push_back(tmp);
		tree[p].r.push_back(tmp);
		return ;
	}
	int mid=(l+r)>>1;
	build(p*2, l, mid);
	build(p*2+1, mid+1, r);
	pushup(tree[p],tree[p*2],tree[p*2+1]);
}

Tree query(int p,int l,int r,int ql,int qr)
{
	if(ql<=l && qr>=r) return tree[p];
	int mid=(l+r)>>1;
	Tree ans,lc,rc;
	lc.len=0,rc.len=0;
	if(ql<=mid) lc=query(p*2,l,mid,ql,qr);
	if(qr>mid) rc=query(p*2+1, mid+1, r, ql, qr);
	if(!lc.len) return rc;
	if(!rc.len) return lc;
	pushup(ans, lc, rc);
	return ans;
}

int main()
{
	int t;
	scanf("%d",&t);
	for(int cas=1;cas<=t;cas++)
	{
		int n; scanf("%d",&n);
		for(int i=1;i<=n;i++) scanf("%lld",&a[i]);
		build(1, 1, n);
		int q;
		scanf("%d",&q);
		printf("Case #%d:\n",cas);
		while(q--)
		{
			int l,r; scanf("%d%d",&l,&r);
			Tree ans=query(1, 1, n, l, r);
			printf("%lld %lld\n",ans.gcd,(1+ans.len)*ans.len/2-ans.notGcd);
		}
	}
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值