题解 SP4155 OTOCI - OTOCI

题目传送门

双倍经验哒

算法分析:欧拉序+并查集

太弱了不会 LCT 没办法……

提供一种不需要 LCT 的解法。

题目里虽说在线操作,但我们可以把操作先存下来。注意到按照顺序进行操作,最后得到的树是固定的,因此我们可以利用并查集,仅执行 bridge 操作,把树先建好。这样整个问题就转化为,在一棵固定的树上处理连通性和树链的权值和问题。对于树链的权值和,用欧拉序加线段树就可以解决了。

处理树链的权值和,具体地,可以参考 P3178 [HAOI2015]树上操作,这里简单说一下。利用时间戳,记录每个节点第一次和第二次被搜到的“时间”,在欧拉序中,这两次的时间之间就是这个节点子树的信息。需要注意的是,第一次搜到时在欧拉序中记正值,第二次时记负值。这样才可以在查询某条链的时候排除该子树中其他节点权值带来的影响。

另外,欧拉序查询的链不能是“折叠”的,或者说不能先向上,再向下。因此应当分两次处理。假定求 \(x\to y\) 的权值和,应该先求 \(x\to lca(x,y)\),再求 \(y\to lca(x,y)\) ,最后减去被重复计算的 \(lca(x,y)\) 的权值。同样的,在修改权值的时候,同样需要将两次搜到的时间对应的位置各自修改。

本题细节较多,需要格外小心,代码中会注明(树剖可以代替欧拉序,但本人还是不会……)。总体时间复杂度约为 \(\mathcal{O}(m\log_2n)\)。

code:

#include<bits/stdc++.h>
#define reg register
#define F(i,a,b) for(reg int i=(a);i<=(b);++i)
using namespace std;
inline int read();
const int N=3e4+5,M=3e5+5;
int n,a[N],m;
char tmp[10];
bool vis[N];
struct Union {//并查集 
	int fath[N];
	int find(int x) {
		return fath[x]==x?x:fath[x]=find(fath[x]);
	}
	inline void clear() {//初始化,不必多说 
		F(i,1,n)fath[i]=i;
	}
	inline bool meg(int x,int y) {//如果成功连接,返回 true,即原本不连通 
		int fx=find(x),fy=find(y);
		if(fx!=fy) {
			fath[fx]=fy;
			return true;
		}
		return false;
	}
} Uni;
struct Com {//command:储存操作 
	char c;
	int x,y;
} Com[M];
namespace Seg {//线段树+欧拉序 
	int ver[N<<1],net[N<<1],head[N],tot;
	inline void add(int x,int y) {
		ver[++tot]=y,net[tot]=head[x],head[x]=tot;
	}
	int dep[N],fa[N][25],lg[N];
	int dfn[N<<1],_t,st[N],ed[N];
	void dfs(int fath,int x) {
		dfn[st[x]=++_t]=x;
		fa[x][0]=fath,dep[x]=dep[fath]+1;
		F(i,1,lg[dep[x]])fa[x][i]=fa[fa[x][i-1]][i-1];
		for(reg int i=head[x]; i; i=net[i]) {
			int &y=ver[i];
			if(y==fath or vis[y])continue;
			vis[y]=1;
			dfs(x,y);
		}
		dfn[ed[x]=++_t]=-x;//存负值 
	}

	inline int Lca(int x,int y) {//倍增求LCA 
		if(dep[x]<dep[y])swap(x,y);
		while(dep[x]>dep[y])x=fa[x][lg[dep[x]-dep[y]]-1];
		if(x==y)return x;
		for(reg int k=lg[dep[x]]-1; k>=0; --k) {
			if(fa[x][k]!=fa[y][k])
				x=fa[x][k],y=fa[y][k];
		}
		return fa[x][0];
	}
#define ls (x<<1)
#define rs (x<<1|1)
	struct tree {
		int l,r,sum;
	} f[N<<4];
	void build(int x,int l,int r) {
		f[x]= {l,r,0};
		if(l==r) {
			int k=abs(dfn[l]);
			if(dfn[l]>0)f[x].sum=a[k];
			else if(dfn[l]<0)f[x].sum=-a[k];//权值记录负值 
			return;
		}
		int mid=l+r>>1;
		build(ls,l,mid);
		build(rs,mid+1,r);
		f[x].sum=f[ls].sum+f[rs].sum;
	}
	void modify(int x,int sit,int add) {
		if(f[x].l==f[x].r) {
			f[x].sum=add;
			return;
		}
		int mid=f[x].l+f[x].r>>1;
		if(sit<=mid)modify(ls,sit,add);
		else modify(rs,sit,add);
		f[x].sum=f[ls].sum+f[rs].sum;
	}
	int query(int x,int l,int r) {
		if(l<=f[x].l and f[x].r<=r)return f[x].sum;
		int mid=f[x].l+f[x].r>>1,ans=0;
		if(l<=mid)ans+=query(ls,l,r);
		if(r>mid)ans+=query(rs,l,r);
		return ans;
	}
	inline void init() {
		F(i,1,n)lg[i]=lg[i-1]+(1<<lg[i-1]==i);
		F(i,1,n)if(!vis[i])vis[i]=1,dfs(0,i);
		build(1,1,_t);
	}
}
int main() {
	n=read();
	F(i,1,n)a[i]=read();
	
	Uni.clear();//初始化并查集 
	
	m=read();
	F(i,1,m) {//储存操作,先建树 
		scanf("%s",tmp);
		Com[i].c=tmp[0];
		Com[i].x=read(),Com[i].y=read();
		if(tmp[0]=='b') {
			if(Uni.meg(Com[i].x,Com[i].y)) {//不连通 
				
				Seg::add(Com[i].x,Com[i].y),Seg::add(Com[i].y,Com[i].x);
				
			}
		}
	}
	
	Uni.clear();//重新初始化并查集 
	
	Seg::init();//建线段树 
	
	F(i,1,m) {
		int x=Com[i].x,y=Com[i].y;
		if(Com[i].c=='b')puts(Uni.meg(x,y)?"yes":"no");
		else if(Com[i].c=='p') {//修改 ,需修改两个点 
			Seg::modify(1,Seg::st[x],y);
			
			Seg::modify(1,Seg::ed[x],-y);
			
			a[x]=y;
		} else if(Com[i].c=='e') {
			if(Uni.find(x)==Uni.find(y)) {//已经联通 
				if(Seg::st[x]>Seg::st[y])swap(x,y);
				
				
				int lca=Seg::Lca(x,y);
				int ans=Seg::query(1,Seg::st[lca],Seg::st[x]);
				
				ans+=Seg::query(1,Seg::st[lca],Seg::st[y]);
				//分两次算 
				ans-=a[lca];//减去重复的部分 
				
				printf("%d\n",ans);
			} else puts("impossible");
		}
	}
	return 0;
}
inline int read() {
	reg int x=0;
	reg char c=getchar();
	while(!isdigit(c))c=getchar();
	while(isdigit(c))x=(x<<3)+(x<<1)+(c^48),c=getchar();
	return x;
}

跑得还是很快的,并查集和线段树部分操作都比较简单,就是一个模板。

AC

欢迎交流讨论,请点个赞哦~

上一篇:贪心算法入门——区间问题


下一篇:判断IP地址是否正确