[Codeforces1290C]Prefix Enlightenment

一盏灯最多只会在 2 2 2个集合里出现。

设包含第 i i i盏灯的两个集合依次为 L i , R i L_i,R_i Li,Ri(若没有则值为 0 0 0)。设 o p S = 1 op_S=1 opS=1表示集合 S S S选过, o p S = 0 op_S=0 opS=0表示没选过。

对于第 i i i盏灯的情况进行讨论

  1. L i = 0 , R i = 0 L_i=0,R_i=0 Li=0,Ri=0,说明这盏灯不属于任何集合且 s i s_i si一定为 1 1 1,故答案不变。
  2. L i ≠ 0 , R i = 0 L_i\neq0,R_i=0 Li=0,Ri=0,说明这盏灯只由一个集合控制。如果 o p L i = 1 ∧ s i = 1 op_{L_i}=1\land s_i=1 opLi=1si=1,那么一定要撤销 L i L_i Li的选择。如果 o p L i = 0 ∧ s i = 0 op_{L_i}=0\land s_i=0 opLi=0si=0,那么一定要选 L i L_i Li。其余两种情况, L i L_i Li状态必须不变。前两种情况,即 o p L i = s i op_{L_i}=s_i opLi=si时会使得与 L i L_i Li有关联的集合状态发生改变,故答案会发生改变;而后两种不变。
  3. L i ≠ 0 , R i ≠ 0 L_i\neq0,R_i\neq0 Li=0,Ri=0,说明这盏灯只由两个集合同时控制,所以这两个集合有关联.与 2. 2. 2.同理讨论可得:如果 o p L i ⊕ o p R i = s i op_{L_i}\oplus op_{R_i}=s_i opLiopRi=si,那么其中一个集合的状态一定要发生改变,优先选择改变总操作次数少且与之关联的集合能够改变状态的集合,答案加上这部分开销。否则,两个集合状态都可以不变,故答案不变。

对于关联性问题,常常使用并查集来解决。

C o s t u Cost_u Costu表示改变改变集合 u u u状态的总代价, C a n t u Cant_u Cantu表示集合 u u u的状态是不是不能改变。

  1. 根节点集合 u u u的状态要改变,则并查集里所有集合的状态都要取反,这样才能确保不影响 s 1... i s_{1...i} s1...i。花费代价 C o s t u Cost_u Costu,然后 C o s t u Cost_u Costu变为其相反数,表示之后撤销的代价.取反可以在根节点打上一个 t a g tag tag,子树里的点的实际状态都是原来的状态异或上根节点的 t a g tag tag
  2. 由于有合并操作,故某点 x x x的实际状态为: x x x到根节点路径上所有节点的 t a g tag tag值的异或和。对于一个并查集,只有根节点的 t a g tag tag值在之后可能会发生改变,故可以将 x x x根节点儿子这一段 t a g tag tag值的异或和合并起来,即路径压缩过程中不引入根节点的 t a g tag tag
  3. 将并查集 x x x合并到 y y y上:   C o s t \ Cost  Cost累加;   C a n t \ Cant  Cant取或;   t a g x \ tag_x  tagx异或上 t a g y tag_y tagy,以确保 x x x的子节点状态不变.

时间复杂度 O ( n α ( m ) ) O(n\alpha(m)) O(nα(m))

#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;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值