CF1416D Graph and Queries (并查集 重构树 线段树)

CF1416D Graph and Queries (并查集 重构树 线段树)

CF1416D Graph and Queries

给定一个 n n n 个点 m m m 条边的无向图,第 i i i 个点的点权初始值为 p i p_i pi , 所有 p i p_i pi 互不相同。

接下来进行 q q q 次操作,分为两类:

  • 1   v \tt 1\ v 1 v 查询与 v v v 连通的点中, p u p_u pu 最大的点 u u u 并输出 p u p_u pu,然后让 p u = 0 p_u=0 pu=0
  • 2   i \tt 2\ i 2 i 将第 i i i 条边删掉。

删边操作肯定不好处理,还是要考虑将操作离线下来,转删边操作为加边操作。但是有一个问题,就是倒序处理的话,没办法知道哪些点已经被访问过点权变成 0 .

于是我们希望在能够得知任意时刻结点连通情况的前提下,顺着询问顺序处理操作 1 。也就是说我们逆着时间线根据操作 2 预处理每个时刻结点的连通情况,然后顺着时间线处理操作 1.

这里运用了建立重构树的技巧。

先将所有被删除的边打上标记,然后合并没有被删除的边两端的结点,得到最后时刻的结点连通情况。

在这里插入图片描述

然后逆着时间线处理被删除的边,当加入这条边会影响图中某些结点的连通情况时,新建一个虚点,连向会影响到的两个连通块,点权为 0 .如果碰到操作 1 ,那就查询一下, v v v​ 所在连通块的树根,并记录下来。

在这里插入图片描述

完成以上操作后,我们会得到一棵树,然后我们对这棵树进行 DFS ,以 DFS 序作为定义域,那么就能方便用线段树维护子树中的最大权值。

回到正序的时间线,当我们碰到操作 1 的时候,就可以用线段树查询 v v v 所在的连通块的树根的子树中的最大权值,并且修改权值即可。

本题的妙处在于:

  1. 删边转为加边的套路。普通的并查集并不支持删边操作,所以这种套路常常使用。
  2. 建立虚点用于表达关键时刻的连通情况。一个虚点连接了两个将要分离的连通块,在分离之前是虚点维护了连通性,也作为了连通块的树根,便于查询。
  3. 虚点带有时间性质。当一个虚点作为另一个虚点的儿子的时候,它就失去了树根的性质,表达了其子树与别的连通块在早些时候连通的信息。
  4. 以 DFS 序作为线段树的定义域,树根的 DFS 序与子树中结点的 DFS 序一定是连续的,所以就可以用线段树维护子树中的信息。轻重链剖分也是相似的原理,通过处理出一些连续的信息,可以用线段树维护树上的信息。
#include<iostream>
#include<cstdio>
#include<algorithm>

using namespace std;
const int MAX_N = 200000;
const int MAX_M = 300000;
const int MAX_Q = 500000;

int N,M,Q;
int w[MAX_N + MAX_Q + 5],head[MAX_N + MAX_Q + 5],cnt,fa[MAX_N + MAX_Q + 5],siz[MAX_N + MAX_Q + 5];
int x[MAX_M + 5],y[MAX_M + 5],opt[MAX_Q + 5],z[MAX_Q + 5],quest[MAX_Q + 5],xx,yy;
int dfncnt , dfn[MAX_N + MAX_Q + 5],lt[MAX_N + MAX_Q + 5],rt[MAX_N + MAX_Q + 5],rev[MAX_N + MAX_Q + 5];
int t[(MAX_N + MAX_Q + 5) << 2],tr[(MAX_N + MAX_Q + 5) << 2],tot;
bool del[MAX_M + 5];
pair<int ,int > ans;

struct edge{
	int nxt;
	int to;
}e[MAX_M + MAX_Q + 5];

void Addedge(int from,int to)
{
	cnt ++;
	e[cnt].nxt = head[from];
	e[cnt].to = to;
	head[from] = cnt;
	return ;
}

int Find(int x)//并查集
{
	return (fa[x] == x ? x : (fa[x] = Find(fa[x])));//只要不影响虚点对子树的统治,可以路径压缩
}

void Join(int x,int y)//并查集部分
{
	int rx = Find(x);int ry = Find(y);
	if(rx == ry) return ;
//	if(siz[rx] > siz[ry]) swap(rx,ry);
	fa[rx] = ry;Addedge(ry,rx);
	return ;
}

void DFS(int u)//根据 DFS 序编号
{
	dfncnt ++;dfn[u] = dfncnt;lt[u] = dfncnt;rev[dfncnt] = u;
	for(int i = head[u],v = 0;i;i = e[i].nxt)
	{
		v = e[i].to;
		DFS(v);
	}
	rt[u] = dfncnt;
	return ;
}

void Build(int k,int l,int r)
{
	if(l == r)
	{
		t[k] = w[rev[l]];//用 DFS 序的编号构造维护最大权值的线段树
		tr[k] = rev[l];
		return ;
	}
	int mid = (l + r) >> 1;int lson = k << 1;int rson = lson + 1;
	Build(lson,l,mid);
	Build(rson,mid + 1,r);
	if(t[lson] > t[rson])
	{
		t[k] = t[lson];
		tr[k] = tr[lson];
	}
	else
	{
		t[k] = t[rson];
		tr[k] = tr[rson];
	}
	return ;
}

pair<int ,int > Query(int k,int l,int r,int x,int y)
{
	if(l > y || r < x) return make_pair(0,0);
	if(x <= l && r <= y) return make_pair(t[k],tr[k]);
	int mid = (l + r) >> 1;int lson = k << 1;int rson = lson + 1;
	pair<int ,int > res,tmp;
	tmp = Query(lson,l,mid,x,y);
	res = Query(rson,mid + 1,r,x,y);
	return (tmp.first > res.first ? tmp : res );
}

void Modify(int k,int l,int r,int x)
{
	if(l > x || r < x) return ;
	if(l == r && l == x)
	{
		t[k] = 0;
		return ;
	}
	int mid = (l + r) >> 1;int lson = k << 1;int rson = lson + 1;
	Modify(lson,l,mid,x);
	Modify(rson,mid + 1,r,x);
	if(t[lson] > t[rson])
	{
		t[k] = t[lson];
		tr[k] = tr[lson];
	}
	else
	{
		t[k] = t[rson];
		tr[k] = tr[rson];
	}
	return ;
}

int main()
{
	scanf("%d%d%d",&N,&M,&Q);tot = N;
	for(int i = 1; i <= N;i ++) scanf("%d",&w[i]);
	for(int i = 1;i <= N;i ++) fa[i] = i,siz[i] = 1;
	for(int i = 1;i <= M;i ++) scanf("%d%d",&x[i],&y[i]);
	for(int i = 1;i <= Q;i ++)
	{
		scanf("%d%d",&opt[i],&z[i]);
		if(opt[i] == 2) del[z[i]] = 1;
	}
	for(int i = 1;i <= M;i ++)
		if(!del[i]) Join(x[i],y[i]);//将所有没删的边连上
	for(int i = Q;i >= 1;i --)
	{
		if(opt[i] == 2)
		{
			xx = Find(x[z[i]]);yy = Find(y[z[i]]);//
			if(xx != yy)//影响连通性
			{
				tot ++;fa[tot] = tot;//建立一个虚点
				fa[xx] = tot;fa[yy] = tot;
				Addedge(tot,xx);Addedge(tot,yy);
			}
		}
		else
		{
			quest[i] = Find(z[i]);//获取当前连通块的根,知道询问时结点在哪个连通块内
		}
	}
	for(int i = 1;i <= tot;i ++)
		if(dfn[i] == 0)
		{
			xx = Find(i);
			DFS(xx);
		}
	Build(1,1,dfncnt);
	for(int i = 1;i <= Q;i ++)
		if(opt[i] == 1)
		{
			ans = Query(1,1,dfncnt,lt[quest[i]],rt[quest[i]]);//回答询问
			printf("%d\n",ans.first);
			if(ans.second > 0) Modify(1,1,dfncnt,dfn[ans.second]);//修改操作
		}
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值