给出一个长度为
n
n
n的
01
01
01串
S
S
S表示初始状态。
给出
k
k
k个集合,集合内的元素是
1
1
1~
n
n
n,表示
S
S
S的下标。保证任意三个不同集合的交为空集。
每次操作可以选择一个集合,使得集合内所有下标的状态
S
S
S翻转。
询问要使得状态
S
S
S前
i
i
i位都为
1
1
1所需要的最少操作次数。
题目保证必有解。
(
1
≤
n
,
k
≤
3
×
1
0
5
)
.
(1\le n,k\le 3\times 10^5).
(1≤n,k≤3×105).
首先看到这个很奇怪的条件:任意三个不同集合的交为空。换句话说任何一个位置最多被两个操作所覆盖。然后稍微分类讨论一下:
这样就发现操作与操作之间是存在限制关系,然后就考虑拆点建并查集。对所有操作拆两个点:一个点 x x x代表选,代价为 1 1 1;另一个点 x + k x+k x+k代表不选,代价为 0 0 0。
因为有些操作是必选和不选的,我开始是想分类讨论在并查集里维护的,写不下去了。直接另外开一对点,选了的代价为无穷大,不选的代价为 0 0 0。必选就把 x x x连向 0 0 0, x + k x+k x+k连向无穷大;不选就把 x x x连向无穷大, x + k x+k x+k连向 0 0 0。因为 x x x与 x + k x+k x+k所在的并查集,代价较小的那个一定会对答案产生贡献。
题目要求对每个前缀都要求答案,只要把当前位两个操作的贡献都删去,等并查集啥的都维护好了,再加上贡献就行了。
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+7;
int n,k;
char s[N];
int fa[N],val[N];
int fd(int x){return x==fa[x]?x:fa[x]=fd(fa[x]);}
int op[N][2];
void uni(int u,int v){
int fu=fd(u),fv=fd(v);
if(fu==fv) return;
val[fv]+=val[fu];
fa[fu]=fv;
}
int main(){
scanf("%d%d",&n,&k);
scanf("%s",s+1);
for(int i=1,m;i<=k;i++){
scanf("%d",&m);
for(int j=1,x;j<=m;j++){
scanf("%d",&x);
if(op[x][0]) op[x][1]=i;
else op[x][0]=i;
}
}
k++;
for(int i=0;i<=2*k;i++) fa[i]=i,val[i]=(i<=k);
val[0]=N;val[k]=0;
int ans=0;
for(int i=1;i<=n;i++){
int u=op[i][0],v=op[i][1];
if(!u){printf("%d\n",ans);continue;}
if(s[i]=='0'&&fd(u)!=fd(v+k)){
ans-=min(val[fd(u)],val[fd(u+k)]);
ans-=min(val[fd(v)],val[fd(v+k)]);
uni(u,v+k);
uni(v,u+k);
ans+=min(val[fd(u)],val[fd(u+k)]);
}
if(s[i]=='1'&&fd(u)!=fd(v)){
ans-=min(val[fd(u)],val[fd(u+k)]);
ans-=min(val[fd(v)],val[fd(v+k)]);
uni(u,v);
uni(u+k,v+k);
ans+=min(val[fd(u)],val[fd(u+k)]);
}
printf("%d\n",ans);
}
}