P8059 [POI2003] Monkeys

这道题思路比较巧妙也比较常见:正难则反。

考虑正推,仿佛要带撤销并查集,反正很难,

面对这种删边问题,一般都是通过倒推的方法变为加边问题,往往会好做。

所以考虑倒推,问题转换为:通过不断加边,则每个点的掉落时间为它第一次与 $1$ 号节点联通的时间,可用并查集维护。

但是,一个问题来了:在一块连通块 $B$ 与 $1$ 号节点所在的连通块相连时,怎么更新 $B$ 内的节点?

可用链表维护,记录一个 $nxt_i$ 维护链表操作,

而且每个并查集维护三个信息:

  1. $f_i$,记录 $i$ 节点的父亲,初始值:$f_i=i$;

  2. $head_i$ 记录 $i$ 节点的连通块的节点中,编号最小的点;

  3. $tail_i$ 记录 $i$ 节点的连通块的节点中,编号最大的点;

其中 $head_i$ 与 $tail_i$ 的 $i$ 是连通块的根,即“老大”。

若要与 $1$ 合并,还要先更新答案,

而且每次合并时都是由大合并到小的,要保证 $1$ 节点一直为根。

#include <cstdio>
#include <iostream>
using namespace std;

const int N = 4e5 + 10;

int a[N][3]; 
int ans[N], nxt[N];
int p[N], w[N], vis[N][3];
int fa[N], head[N], tail[N];

int find(int x){
    if (fa[x] == x)return x;
    return fa[x] = find(fa[x]);
}

void merge (int u, int v, int p) {
    int fu = find(u), fv = find(v);
    if (fu == fv) return ;
    if (fu > fv) swap (fu, fv);
    if (fu == 1 && p != -1) { // 更新答案。其中 p = -1 代表是没有删过的边将它加上,所以不更新答案。
        for (int use = head[fv]; use; use = nxt[use]) ans[use] = p;
    }
    fa[fv] = fu; nxt[tail[fu]] = head[fv]; tail[fu] = tail[fv];
} 

int main() {
    int n, m; scanf ("%d%d", &n, &m);
    for (int i = 1; i <= n; ++i) {
        scanf ("%d%d", &a[i][1], &a[i][2]); ans[i] = -1;
    }
    for (int i = 0; i < m; ++i) {
        scanf ("%d%d", &p[i], &w[i]);
        vis[p[i]][w[i]] = 1; // 记录此条边会被删掉。
    }
    for (int i = 1; i <= n; ++i) fa[i] = head[i] = tail[i] = i, nxt[i] = 0;
    for (int i = 1; i <= n; ++i) { // 若从来没有删过的边,一开始就要加上。
        if (!vis[i][1] && a[i][1] != -1) merge (i, a[i][1], -1);
        if (!vis[i][2] && a[i][2] != -1) merge (i, a[i][2], -1);
    }
    for (int i = m - 1; i >= 0; --i) {
        int u = p[i], v = a[p[i]][w[i]];
        merge (u, v, i);
    }
    for (int i = 1; i <= n; ++i) printf("%d\n",ans[i]);
    return 0;
} 
posted @ 2022-06-05 21:47  wangzhongyuan  阅读(7)  评论(0编辑  收藏  举报  来源