题目
给定二维平面的集合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;
}