题目链接:点击查看
题目大意:有 n 个球员和 m 个球迷,一个球员可能是多个球迷的粉丝,需要选择最少的球员进行比赛,使得所有的球迷都愿意观看(对于每个球迷来说,都有至少一个其喜欢的球员入选比赛)
对于球迷与球员之间的关系,如果球迷 a 喜欢球员 b ,且球迷 c 喜欢球迷 b ,那么球迷 a 也会喜欢球迷 c 所喜欢的其他球员,对于球迷 c 同理
现在有 q 次粉丝关系的修改(增加或删除),对于每次修改后回答询问
题目分析:画画图不难看出,球迷与球员连边,设 cnt 为总的连通块的个数,设 cntn 为有多少个孤立的球员,cntm 有多少个孤立的球迷,举个例子:
上图的关系中,cnt = 3 , cntn = 1 , cntm = 0
对于每次询问,如果 cntm > 0 的话,意思就是有 cntm 个球迷没有喜欢的球员,答案显然为 -1 ,除此之外,答案都为 cnt - cntn
这样的话题目就转换为了维护一个并查集的关系就好了,但是并查集需要支持增加和删除操作,如果只需要维护一个 cnt 变量的话,直接用并查集的删除这个算法足够,但同时还需要维护 cntn 和 cntm ,用并查集的删除就不太好维护了
这里先说一下 cntn 和 cntm 该如何维护吧,因为每次连边一定是球迷连向球员的一条边,假设 x 为球迷,y 为球员:
- 添加 ( x , y ) 这条边:
- 如果 x 之前是孤立的球迷,则连通后 cntm 减一
- 如果 y 之前是孤立的球员,则连通后 cntn 减一
- 删除 ( x , y ) 这条边:
- 如果 x 之后是孤立的球迷,则连通后 cntm 加一
- 如果 y 之后是孤立的球员,则连通后 cntn 加一
既然不能用并查集的删除,那么可以换一个方向思考,删除一条边等于撤销这条边,所以我们可以使用并查集按秩合并中的撤销操作来表示删除(其实就是用栈维护一下合并的信息,撤销的时候再边出栈边撤销就好了)
鉴于这个题目允许离线操作,可以用线段树分治来实现,线段树的下标作为时间单位,这样每个叶子结点就代表了每个询问,递归的时候用并查集维护一下联通性就好了
时间复杂度应该是 qlogq * log( n + m ),因为并查集是按秩合并的,每次查询需要 log n 的时间复杂度
代码:
#include<iostream>
#include<cstdio>
#include<string>
#include<ctime>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<stack>
#include<climits>
#include<queue>
#include<map>
#include<set>
#include<sstream>
#include<cassert>
#include<bitset>
#include<unordered_map>
using namespace std;
typedef long long LL;
typedef unsigned long long ull;
const int inf=0x3f3f3f3f;
const int N=4e5+100;
map<int,int>pre[N];
int f[N],rk[N],ans[N],cnt,cntn,cntm;
struct revo
{
int cnt,cntn,cntm;
int fax,fay;
int rkx,rky;
};
struct Node
{
int l,r;
vector<pair<int,int>>node;//位于[l,r]这段时间内的边有哪些
vector<revo>st;//并查集的撤销用
}tree[N<<2];
int find(int x)
{
return f[x]==x?x:find(f[x]);
}
revo merge(int x,int y)//并查集的合并,返回合并之前的信息revo用于撤销
{
revo ans;
int xx=find(x),yy=find(y);
ans.cnt=cnt,ans.cntm=cntm,ans.cntn=cntn;
ans.fax=xx,ans.fay=yy;
ans.rkx=rk[xx],ans.rky=rk[yy];
if(xx!=yy)
{
if(rk[xx]==1)
cntm--;
if(rk[yy]==1)
cntn--;
cnt--;
if(rk[xx]>rk[yy])
swap(xx,yy);
f[xx]=yy;
rk[yy]=max(rk[yy],rk[xx]+1);
}
return ans;
}
void revocation(revo node)//并查集的撤销,将并查集恢复至node状态
{
cnt=node.cnt;
cntn=node.cntn;
cntm=node.cntm;
f[node.fax]=node.fax;
f[node.fay]=node.fay;
rk[node.fax]=node.rkx;
rk[node.fay]=node.rky;
}
void build(int k,int l,int r)
{
tree[k].l=l;
tree[k].r=r;
if(l==r)
return;
int mid=l+r>>1;
build(k<<1,l,mid);
build(k<<1|1,mid+1,r);
}
void update(int k,int l,int r,pair<int,int>val)
{
if(l>r)
return;
if(tree[k].l>r||tree[k].r<l)
return;
if(tree[k].l>=l&&tree[k].r<=r)
{
tree[k].node.push_back(val);
return;
}
update(k<<1,l,r,val);
update(k<<1|1,l,r,val);
}
void dfs(int k)//线段树分治
{
for(auto it:tree[k].node)//并查集的合并
tree[k].st.push_back(merge(it.first,it.second));
if(tree[k].l==tree[k].r)
{
if(cntm>0)
ans[tree[k].l]=-1;
else
ans[tree[k].l]=cnt-cntn;
}
else
{
dfs(k<<1);
dfs(k<<1|1);
}
while(tree[k].st.size())//并查集的撤销
{
revocation(tree[k].st.back());
tree[k].st.pop_back();
}
}
void init()
{
for(int i=1;i<N;i++)
{
f[i]=i;
rk[i]=1;
}
}
int main()
{
#ifndef ONLINE_JUDGE
// freopen("data.in.txt","r",stdin);
// freopen("data.out.txt","w",stdout);
#endif
// ios::sync_with_stdio(false);
init();
int n,m,q;//n:球员 m:球迷 edge:m->n
scanf("%d%d%d",&n,&m,&q);
cnt=n+m,cntn=n,cntm=m;
build(1,1,q);
for(int i=1;i<=n;i++)
{
int k;
scanf("%d",&k);
while(k--)
{
int x;
scanf("%d",&x);
pre[x][i]=1;
}
}
for(int i=1;i<=q;i++)
{
int a,b;
scanf("%d%d",&a,&b);
if(!pre[a].count(b))
pre[a][b]=i;
else
{
update(1,pre[a][b],i-1,make_pair(a+n,b));
pre[a].erase(b);
}
}
for(int i=1;i<=m;i++)
for(auto it:pre[i])
update(1,it.second,q,make_pair(i+n,it.first));
dfs(1);
for(int i=1;i<=q;i++)
printf("%d\n",ans[i]);
return 0;
}