一盏灯最多只会在 个集合里出现。
设包含第 盏灯的两个集合依次为 (若没有则值为 )。设 表示集合 选过, 表示没选过。
对于第 盏灯的情况进行讨论
- ,说明这盏灯不属于任何集合且 一定为 ,故答案不变。
- ,说明这盏灯只由一个集合控制。如果 ,那么一定要撤销 的选择。如果 ,那么一定要选 。其余两种情况, 状态必须不变。前两种情况,即 时会使得与 有关联的集合状态发生改变,故答案会发生改变;而后两种不变。
- ,说明这盏灯只由两个集合同时控制,所以这两个集合有关联.与 同理讨论可得:如果 ,那么其中一个集合的状态一定要发生改变,优先选择改变总操作次数少且与之关联的集合能够改变状态的集合,答案加上这部分开销。否则,两个集合状态都可以不变,故答案不变。
对于关联性问题,常常使用并查集来解决。
设 表示改变改变集合 状态的总代价, 表示集合 的状态是不是不能改变。
- 根节点集合 的状态要改变,则并查集里所有集合的状态都要取反,这样才能确保不影响 。花费代价 ,然后 变为其相反数,表示之后撤销的代价.取反可以在根节点打上一个 ,子树里的点的实际状态都是原来的状态异或上根节点的 。
- 由于有合并操作,故某点 的实际状态为: 到根节点路径上所有节点的 值的异或和。对于一个并查集,只有根节点的 值在之后可能会发生改变,故可以将 到根节点儿子这一段 值的异或和合并起来,即路径压缩过程中不引入根节点的 。
- 将并查集 合并到 上: 累加; 取或; 异或上 ,以确保 的子节点状态不变.
时间复杂度
#include <bits/stdc++.h>
using namespace std;
const int N = 3e5 + 5;
typedef int arr[N];
int n, m, Ans;
char s[N];
arr L, R, fa, tag, Cant, Cost, Size;
inline int Gf(int x) {
if (!fa[x])
return x;
if (!fa[fa[x]]) // 跳过根节点的tag
return fa[x];
int p = Gf(fa[x]);
tag[x] ^= tag[fa[x]];
return fa[x] = p;
}
inline void Merge(int a, int b) {
fa[a] = b,
Size[b] += Size[a],
Cost[b] += Cost[a],
Cant[b] |= Cant[a],
tag[a] ^= tag[b];
}
inline int op(int x) { return tag[x] ^ tag[fa[x]]; }
inline int Calc(int i) {
if (!L[i])
return Ans;
if (!R[i]) {
int x = Gf(L[i]);
Cant[x] = 1;
if (op(L[i]) == (s[i] - '0'))
Ans += Cost[x], tag[x] ^= 1, Cost[x] = -Cost[x];
return Ans;
}
int x = Gf(L[i]), y = Gf(R[i]);
if (Size[x] > Size[y])
swap(x, y);
if (x == y)
return Ans;
if ((op(L[i]) ^ op(R[i])) == s[i] - '0') {
int Cx = Cost[x], Cy = Cost[y];
if ((Cx < Cy && !Cant[x]) || Cant[y])
Ans += Cx, tag[x] ^= 1, Cost[x] = -Cx;
else
Ans += Cy, tag[y] ^= 1, Cost[y] = -Cy;
}
Merge(x, y);
return Ans;
}
int main() {
scanf("%d%d%s", &n, &m, s + 1);
for (int i = 1, c; i <= m; ++i) {
scanf("%d", &c);
for (int x; c--;)
scanf("%d", &x), L[x] ? R[x] = i : L[x] = i;
}
for (int i = 1; i <= m; ++i)
Size[i] = Cost[i] = 1;
for (int i = 1; i <= n; ++i)
printf("%d\n", Calc(i));
return 0;
}