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;
}