题目
给出一个长度为n(n<=3e5)的01串S,
给出k(k<=3e5)个集合,每个集合里是串里的一些位置的下标,保证对于任意三个不同的集合i,j,k,
你可以指定一个集合,将集合内的01全部取反,视为依次操作
问,对于每个i,需要至少多少次操作,可以将前缀i个数都置1(此时不用关心后面的数是0还是1)
题目保证有解
思路来源
https://blog.csdn.net/wyy603/article/details/104156509
题解
考虑,三个集合相交为空,说明每个元素最多出现在两个集合里
没出现,显然不用管,
维护种类并查集,1到k是不选第i个集合,代价为0,k+1到2k是选第i个集合,代价为1
①出现在一个集合里时,如果这一位本身为1,就不选,否则必选
②出现在两个集合里时,不妨设x和y,如果为1,同时选x和y,或同时不选;如果为0,同时选x和y+k,或同时选x+k和y
对于一个固定大小的集合,选其或者其对立都是可行的方案,二者取小代价即可
对于②类型操作,考虑先减去上一次两个独立集合的贡献,然后将其合并,再加上一个独立集合的贡献
对于①类型的操作,必选这个比较难讨论,因为要涉及到后续的集合合并
这里的处理方式是,维护一个0节点,选0节点的代价为INF,
将x和x+k两个集合中一定不用的那个集合(不妨设为x)和0节点合并,
选x的代价为INF,就自然不会选INF了,达到了强制选x+k的目的
代码
#include<bits/stdc++.h>
using namespace std;
const int N=3e5+10,M=2*N,INF=0x3f3f3f3f;
int n,k,c,x,y,par[M],cost[M],ans;
vector<int>a[N];//a[i][j]表示i受a[i][j]个集合控制
char s[N];
int find(int x){
return par[x]==x?x:par[x]=find(par[x]);
}
void merge(int x,int y){
x=find(x),y=find(y);
if(x==y)return;
par[x]=y;
cost[y]+=cost[x];
}
int f(int x){
return min(cost[find(x)],cost[find(x+k)]);
}
int main(){
scanf("%d%d",&n,&k);
scanf("%s",s+1);
for(int i=1;i<=k;++i){
scanf("%d",&c);
for(int j=1;j<=c;++j){
scanf("%d",&x);
a[x].push_back(i);
}
}
//拆点并查集 1-n为不用 n+1 -2n为用
for(int i=1;i<=2*k;++i){
par[i]=i;
cost[i]= i>k;
}
par[0]=0;
cost[0]=INF;
for(int i=1;i<=n;++i){
int sz=(int)a[i].size();
if(sz==1){
int x=a[i][0];
int ban=(s[i]=='1')*k+x;
ans-=f(x);
merge(ban,0);
ans+=f(x);
}
else if(sz==2){
int x=a[i][0],y=a[i][1];
if(s[i]=='1'){
if(find(x)!=find(y)){
ans-=f(x)+f(y);
merge(x,y);
merge(x+k,y+k);
ans+=f(x);
}
}
else{
if(find(x)!=find(y+k)){
ans-=f(x)+f(y);
merge(x,y+k);
merge(x+k,y);
ans+=f(x);
}
}
}
printf("%d\n",ans);
}
return 0;
}