Educational Codeforces Round 62 (Rated for Div. 2) F. Extending Set of Points(可撤销并查集+线段树分治)

题目

给定二维平面的集合S,扩展集合E(S)首先被定义为S,然后开始往里面加点

任取S内集合的三个点,如果它们能构成矩形的三个顶点,则把矩形的第四个顶点也加到E(S)里

最终的E(S),是不能再往里面加新的不存在的点的最大点集

现在给定初始为空的集合S,以下q(q<=3e5)个操作,

第i次给出一个点Pi(xi,yi),

①如果Pi已经在S中,就将Pi从S中删去

②否则将Pi加入到集合S中

每次操作后,询问当前E(S)集合的大小

思路来源

https://codeforces.com/blog/entry/66147(官方tutorial)

https://www.cnblogs.com/CJLHY/p/10606629.html

https://blog.youkuaiyun.com/ccsu_cat/article/details/88834535

https://www.cnblogs.com/uuzlove/p/10585235.html

题解

官方题解的大意,

把x轴抽象成点,把y轴也抽象成点,把形如(x,y)的点抽象成边,去连接坐标轴被抽象成的点。

每个点对应一个集合,集合内cntx[]记录这个集合内有多少条x轴线,cnty[]记录有多少条y轴线,

想象网格纸,若干条交叉线,如果一个集合内有这些x轴的线和y轴的线,

那么一定可以补全交叉线上的所有点,因为这每一条线的出现都是因为这条线上的一个点的存在,

而这些线被合并到一个集合里去,是因为一个点可以将两条线合并到一起

 

由于除插入外还有删除操作,所以考虑可撤销并查集,离线地考虑询问的事件。

将插入和删除点,转化为这个点在[L,R]事件内是存在的,按事件的时间戳建一个线段树,

如果这个点没有删除,可以认为保留到了事件尾,处理成[L,q]即可

线段树内节点(p,l,r)的vector维护的是在[l,r]事件内都存在的点,

所以可以类似区间染色问题,把每个点所代表的[l,r]事件分解成O(logq)段,添加进线段树上对应节点的vector

维护一个并查集和当前值的答案now,根据dfs序对线段树进行遍历,

当进入一个线段树上的节点时,先将这个节点存的点在并查集上进行修改,

如果已经dfs到叶了,说明当前now值就是对应事件的答案,

离开一个节点的时候,应该撤销对并查集的修改,

可撤销并查集类似于数据库刚讲的rollback回滚问题,即要回滚哪些值,即提前在日志里备用上这些值,

那么我们维护一个栈,来逆序恢复,

由于栈内存的是两棵树的树根,所以撤销的时候直接让树根恢复为树根,不用存别的sz[]、cnt[]、par[]值了

特别地,这里的并查集不能使用路径压缩,因为涉及撤销操作,sz[]用于按秩(实际是大小)合并保证复杂度

四点半睡不着起来补题可还行

代码

#include<bits/stdc++.h>
#define pb push_back
#define fi first
#define se second
#define lson p<<1,l,mid
#define rson p<<1|1,mid+1,r
using namespace std;
typedef long long ll;
const int maxn=3e5;
const int N=maxn+10;
typedef pair<int,int> P;
map<P,int> last;//last[P]代表这个点上一次存在的位置
vector<P>dat[4*N];//储存点坐标的线段树向量 
//把点i处理成在[li,ri]询问内,点i存在
//从而处理成线段树染色问题 如果完整区间包含就不递归子树 称线段树分治
//dfs序访问线段树的树 进入时修改 离开时撤销 用可撤销并查集维护 
int n,par[2*N],cntx[2*N],cnty[2*N],sz[2*N];
P a;
ll now,ans[N];
int find(int x)
{
	return par[x]==x?x:find(par[x]);//不带路径压缩,因为要撤销 
}
void merge(int x,int y,stack<P> &q)
{
	x=find(x),y=find(y);
	if(x==y)return;
	//按秩合并 按disjoint集合大小合并 小的向大的合 这里不维护高度 
	if(sz[x]<sz[y])swap(x,y);
	now-=1ll*cntx[x]*cnty[x];//在用到int*int时,转1ll 
	now-=1ll*cntx[y]*cnty[y];
	q.push(P(x,y));//用栈保留更改 由于记录的是两棵树的树根 逆序撤销时只需令par[y]=y即可 
	sz[x]+=sz[y]; 
	cntx[x]+=cntx[y];
	cnty[x]+=cnty[y];
	par[y]=x;
	now+=1ll*cntx[x]*cnty[x];
}
void undo(stack<P> &q)//撤销线段树上这个结点的操作 
{
	while(!q.empty())
	{
		int x=q.top().fi;
		int y=q.top().se;
		q.pop();
		now-=1ll*cntx[x]*cnty[x];
		sz[x]-=sz[y];
		cntx[x]-=cntx[y];
		cnty[x]-=cnty[y];
		par[y]=y;//将y还原为没合并之前的树根y
		now+=1ll*cntx[x]*cnty[x];
		now+=1ll*cntx[y]*cnty[y]; 
	} 
} 
void update(int p,int l,int r,int ql,int qr,P v)
{
	if(ql<=l&&r<=qr)
	{
		dat[p].pb(v);
		return;
	}
	int mid=(l+r)/2;
	if(ql<=mid)update(lson,ql,qr,v);
	if(qr>mid)update(rson,ql,qr,v);
}
void dfs(int p,int l,int r)
{
	stack<P>q;
	for(auto v:dat[p])//处理当前节点所包含区间的事件 
	merge(v.fi,v.se,q); 
	if(l==r)ans[l]=now;
	else
	{
		int mid=(l+r)/2;
		dfs(lson);
		dfs(rson);
	}
	undo(q);
}
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;++i)
	{
		scanf("%d%d",&a.fi,&a.se);
		a.se+=maxn;
		if(last.count(a))
		{
			update(1,1,n,last[a],i-1,a);//a实际存在的事件时刻是[last[a],i-1]
			last.erase(a); 
		}
		else last[a]=i;
	}
	for(auto v:last)//没有被删掉的 认为存在到事件尾 
	update(1,1,n,v.se,n,v.fi);//第一维点对 第二维位置 
	for(int i=1;i<=maxn;++i)
	par[i]=i,sz[i]=1,cntx[i]=1;
	for(int i=maxn+1;i<=2*maxn;++i)
	par[i]=i,sz[i]=1,cnty[i]=1;
	dfs(1,1,n);//按dfs序,即前序,遍历线段树
	for(int i=1;i<=n;++i)
	printf("%I64d%c",ans[i],i==n?'\n':' ');
	return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小衣同学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值