一道很好的题了,具体题解可以看b站的讲解。。
拆点的思想有一种2sat的感觉
/* 给定一组开关的集合,每个开关最多被两个集合包含,对集合操作一次则所有集合内的开关状态变化 现在要将前i个开关状态切换到开,问最少要操作几次集合,求出i从1到n的每个答案 一些性质:每个集合要么被操作一次,要么不被操作(两次操作等于不操作) 那么我们将每个集合拆点,a表示操作,b表示不操作 再看每个开关,设该开关被集合i,j所包含 初始状态0:要么i操作,j不操作(在ia和jb之间连一条无向边) 要么i不操作,j操作(在ib和ja之间连一条无向边) 初始状态1:要么i操作,j操作(在ia和ja之间连一条无向边) 要么i不操作,j不操作(在ib和jb之间连一条无向边) 具体实现:从1->n枚举所有点i 1.取消之前选择带来的贡献 2.增加强制关系 i 只有一个集合:不用连边,并且只能有一个选择(特判一下即可) i 有两个集合:连边后再选择较优的 3.在新的强制关系下再进行选择,更新答案 */ #include<bits/stdc++.h> using namespace std; #define N 600005 int n,k,l[N],r[N]; char s[N]; int F[N],size[N]; int find(int x){ return F[x]==x?x:F[x]=find(F[x]); } void bing(int x,int y){ int fx=find(x),fy=find(y); //一个是0的话就要把父亲设为0 if(!fx){ F[fy]=0; return; } if(!fy){ F[fx]=0; return; } if(fx!=fy){ size[fx]+=size[fy]; F[fy]=fx; } } int calc(int i){//对第i个集合,是选还是不选 int u=i,v; if(u>k)v=u-k; else v=u+k; int fu=find(u),fv=find(v); if(!fu || !fv)return size[fu+fv];//如果有一个不能选,那么直接选另一个 return min(size[fu],size[fv]); } int main(){ cin>>n>>k; scanf("%s",s+1); for(int i=1;i<=k*2;i++)F[i]=i; for(int i=1;i<=k;i++)size[i]=1; for(int i=1;i<=k;i++){ int m;cin>>m; while(m--){ int x;cin>>x; if(l[x])r[x]=i; else l[x]=i; } } int ans=0; for(int i=1;i<=n;i++){ if(l[i] && r[i]){//有两个集合可选 int u=l[i],v=r[i]; if(s[i]=='1'){ if(find(u)!=find(v)){ //如果这种强制关系还没被建立,那么现在必须建立起来,并且再去选择策略 //反之如果已经建立好了,表示这个点不管选哪种策略都不重要了,其贡献也不用统计 ans-=calc(u);ans-=calc(v);//先撤销u,v集合选择情况的贡献 bing(u,v);bing(u+k,v+k);//连边,即选u的同时必须选v,反之亦然连边 ans+=calc(u);//再把这个影响加回去 } } else{ if(find(u)!=find(v+k)){ ans-=calc(u);ans-=calc(v); bing(u,v+k);bing(u+k,v); ans+=calc(u); } } } else if(l[i] && !r[i]){//只有一个集合,必选 int u=l[i]; ans-=calc(u); if(s[i]=='1')F[find(u)]=0; else F[find(u+k)]=0; ans+=calc(u); } cout<<ans<<' '; } }