CF1290C Prefix Enlightenment 题解
Solution
不难注意到“任意三个子集的交集为空”等价于每盏灯最多同时出现于 \(2\) 个集合中。
设第 \(i\) 盏灯出现在第 \(p_i,q_i\) 两个集合中,若没有则为 \(0\)。设 \(f_k\in \{0,1\}\) 表示是否选集合 \(A_k\)。
然后依据题意从左向右扫,分讨第 \(i\) 盏灯的情况。
- \(p_i=q_i=0\):一定有 \(s_i=1\),答案与这盏灯无关。
- \(p_i\neq 0,q_i=0\):仅 \(A_{p_i}\) 影响该灯,故 \(f_{p_i}=\operatorname{not} s_i\)。
- \(p_i\neq 0,q_i\neq 0\):该灯同时受两个集合影响,必有 \(f_{p_i}\operatorname{xor} f_{q_i}=\operatorname{not}s_i\)。
考虑图论建模,将约束转化成带权无向边。
- 对于 \(f_{p_i}=c\) 型的约束,新建一个虚点 \(0\),在 \(p_i,0\) 间建边权为 \(c\) 的无向边。
- 对于 \(f_{p_i}\operatorname{xor} f_{q_i}=c\) 型的约束,在 \(p_i,q_i\) 间建边权为 \(c\) 的边。
不难发现此时要维护的是一个 01 二分图。每个连通块的代价为左部点和右部点数的较小值。特别地,如果点 \(0\) 在该连通块中,则代价为与 \(0\) 不在同一部分的点数。
然后用带权并查集维护即可,具体细节详见代码。
AC Code
#include <bits/stdc++.h>
#define rep(i,a,b) for(int i(a);i<b;++i)
#define rept(i,a,b) for(int i(a);i<=b;++i)
using namespace std;
constexpr int N=3e5+5;
int fa[N],w[N],siz[N][2];
int n,k,c,ans; // ans:当前总代价
char s[N];
vector<int> g[N];
// w[i]:点i的父边权值
// siz[i][0]:和根节点i在同一部分的点数
// siz[i][0]:和根节点i在不同部分的点数
int find(int x){
if(fa[x]==x) return x;
int t=find(fa[x]);
w[x]^=w[fa[x]];
return fa[x]=t;
}
int calc(int u){ // u所在连通块的代价
if(find(0)==u) return siz[u][w[0]^1]; // 与 $0$ 不在同一部分的点数
return min(siz[u][0],siz[u][1]); // 左部点和右部点数的较小值
}
void merge(int x,int y,int k){
int u=find(x),v=find(y),z=k^w[x]^w[y];
if(u^v){ // 千万别忘了判
ans-=calc(u),ans-=calc(v);
w[u]=z;
siz[v][0]+=siz[u][z];
siz[v][1]+=siz[u][z^1];
fa[u]=v;
ans+=calc(v);
}
}
signed main(){
scanf("%d%d%s",&n,&k,s+1);
rept(i,1,k) fa[i]=i,siz[i][0]=1;
rept(i,1,k){
cin>>c;
while(c--){
int x;scanf("%d",&x);
g[x].emplace_back(i);
}
}
rept(i,1,n){
if(g[i].size()==2) merge(g[i][0],g[i][1],s[i]=='0');
else if(g[i].size()==1) merge(g[i][0],0,s[i]=='0');
printf("%d\n",ans);
}
return 0;
}

浙公网安备 33010602011771号