Luogu P4435

题目链接

题意:单点修改;给定区间,查询有多少子区间的 \(\gcd>1\)

考虑只查询一次。这种子序列计数问题容易想到分治。

设当前递归到 \([l,r]\),设要找的是有多少满足条件的 \([L,R]\in[l,r]\)。我们只需要计数跨区间的(因为 \(L,R\) 都在一个区间的可以递归求解),即 \(L\in [l,mid],R\in(mid,r]\) 的数量。

注意到 \(\text{gcd}\) 是单调不增的。可以对左区间算后缀 \(\gcd\) 和,右区间算前缀 \(\gcd\) 和。注意到这两个的单调性,可以双指针求,具体就是右指针在 \(mid+1\) 往右扫,左指针在 \(l\) 往右扫。可以 \(O(len)\) 求解,其中 \(len\) 是区间的长度。结合分治的复杂度是 \(O(n\log n)\)

考虑拓展到线段树上。因为线段树本身就是分治的结构,我们可以考虑对每个线段树的节点维护前缀 \(\gcd\) 和与后缀 \(\gcd\) 和以及该区间的答案,这样具有可合并性,这些左子的信息和右子的信息可以合并得到该节点的答案,符合线段树的信息可合并性。

但是直接莽会寄,因为直接维护前缀和,后缀和的时空复杂度都是爆的。注意到前缀和,后缀和的更新方式是 \(\gcd\),注意到 \(a\ne b\) 时有 \(\gcd(a,b)\leq \lfloor\dfrac{a}{2}\rfloor\)(不妨设 \(a\ge b\)),因此 \(\gcd\) 和的种类数是 \(\log A\) 的(\(A\) 是值域,对这个结论的解释是每次至少减半),所以我们可以维护前缀后缀和的种类以及出现的位置,方便计算答案。

具体就是用若干个 pair<int,int>,分别描述该位置上 \(\gcd\) 和的值和第一次出现这个值的位置。而对于一个节点,最多会有 \(\min(r-l+1,\log A)\) 个。

然后直接线段树维护这些前缀 \(\gcd\) 和,后缀 \(\gcd\) 和,区间答案即可。唯一的难点就是合并信息。这个的话就是先继承左子和右子的答案,然后使用双指针计算跨区间的 \([L,R]\)。然后更新前缀 \(\gcd\) 和,后缀 \(\gcd\) 和就好了。

总时间复杂度 \(O(n\log n\log A)\)

// Problem: P4435 [COCI2017-2018#2] Garaža
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P4435
// Memory Limit: 500 MB
// Time Limit: 4000 ms
// 
// Powered by CP Editor (https://cpeditor.org)

#include<bits/stdc++.h>
#define ll long long
#define pb push_back
#define fi first
#define se second
#define mx3(a,b,c) ((a>b?a:b)>c?(a>b?a:b):c)
#define mn3(a,b,c) ((a<b?a:b)<c?(a<b?a:b):c)
#define infll 1e16
#define inf 1e9
#define pii pair<int,int>
#define F(i,a,b) for(int i=a;i<=(b);i++)
#define dF(i,a,b) for(int i=a;i>=(b);i--)
#define wh(lzm) while(lzm--)
#define lowbit(x) (x&(-x))
#define HH printf("\n")
#define eb emplace_back
#define vi vector<int>
using namespace std;
int read(){
	int x=0,f=1;char c=getchar();
	while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}
	while(c>='0'&&c<='9') x=(x<<3)+(x<<1)+(c^48),c=getchar();
	return x*f;
}
const int mod=998244353,maxn=114514;
#define ls (o<<1)
#define rs (o<<1|1)
struct seg{
	vector<pii>pre,suf;
	int l,r;
	ll v;
}t[maxn<<2];
int n,k,lzm;
seg pushup(seg a,seg b){
	if(!a.pre.size()) return b;
	if(!b.pre.size()) return a;
	if(b.l<a.l) swap(a,b);
	seg rt;
	rt.l=a.l,rt.r=b.r;
	rt.v=a.v+b.v;
	int as=a.suf.size(),bp=b.pre.size();
	int ap=a.pre.size(),bs=b.suf.size();
	rt.pre=a.pre,rt.suf=b.suf;
	int now=a.pre[ap-1].fi;
	for(pii i:b.pre){
		int tt=__gcd(now,i.fi);
		if(tt<now) rt.pre.eb(tt,i.se);
		now=tt;
	}
	now=b.suf[bs-1].fi;
	for(pii i:a.suf){
		int tt=__gcd(now,i.fi);
		if(tt<now) rt.suf.eb(tt,i.se);
		now=tt;
	}
	int j=-1;
	dF(i,as-1,0){
		pii now=a.suf[i];
		int len;
		if(i==as-1) len=now.se-a.l+1;
		else len=now.se-a.suf[i+1].se;
		for(;j<bp-1&&__gcd(b.pre[j+1].fi,now.fi)>1;++j);	
		if(j==-1) continue;
		ll ad;
		if(j==bp-1) ad=1ll*len*(b.r-b.l+1);
		else ad=1ll*len*(b.pre[j+1].se-b.l);
		rt.v+=ad;
	}
	return rt;
}
void update(int o,int l,int r,int pos,int x){
	if(l==r){
		if(x>1) t[o].v=1;
		else t[o].v=0;
		t[o].suf.clear(),t[o].pre.clear();
		pii add=make_pair(x,pos);
		t[o].pre.pb(add);
		t[o].suf.pb(add);
		t[o].l=t[o].r=l;
		return;
	}
	int mid=(l+r)>>1;
	if(pos<=mid) update(ls,l,mid,pos,x);
	else update(rs,mid+1,r,pos,x);
	t[o]=pushup(t[ls],t[rs]);
}
seg query(int o,int l,int r,int ql,int qr){
	if(ql<=l&&qr>=r) return t[o];
	int mid=(l+r)>>1;
	if(qr<=mid) return query(ls,l,mid,ql,qr);
	if(ql>mid) return query(rs,mid+1,r,ql,qr);
	return pushup(query(ls,l,mid,ql,qr),query(rs,mid+1,r,ql,qr));
}
int a[maxn];
void build(int o,int l,int r){
	t[o].l=l,t[o].r=r;
	if(l==r){
		int x=a[l];
		if(x>1) t[o].v=1;
		else t[o].v=0;
		t[o].suf.clear(),t[o].pre.clear();
		pii add=make_pair(x,l);
		t[o].pre.pb(add);
		t[o].suf.pb(add);
		return;
	}
	int mid=(l+r)>>1;
	build(ls,l,mid);
	build(rs,mid+1,r);
	t[o]=pushup(t[ls],t[rs]);
}
signed main(){
	n=read(),lzm=read();
	F(i,1,n) a[i]=read();
	build(1,1,n);
	wh(lzm){
		int op=read(),l=read(),r=read();
		if(op==1) update(1,1,n,l,r);
		else printf("%lld\n",query(1,1,n,l,r).v);
	}
}

附一句,这题双倍经验 CF1004F

posted on 2023-10-13 18:58  nullptr_qwq  阅读(46)  评论(0)    收藏  举报