CONTENT: 并查集 II
DETAIL: 并查集的按秩合并、可持久化并查集



BB

被师兄催更了 QAQQQQQ
本次算法实验5,让蒟蒻get了额外的并查集的两个点!Amazing!

Introduction

本次的主要内容涉及到了并查集的按秩合并、可持续化并查集

黑白树 - HYSBZ 3319

Description
给定一棵树,边的颜色为黑或白,初始时全部为白色。维护两个操作:
1.查询u到根路径上的第一条黑色边的标号。
2.将u到v 路径上的所有边的颜色设为黑色。
Notice:这棵树的根节点为1
Sample Input

5 4
1 2
1 3
2 4
2 5
1 2
2 2 3
1 3
1 4

Sample Output

0
2
1

Solution —— 并查集
补题系列QAQ(6个月前,用树剖一直TLE,直到才知道怎么用并查集写)
边染黑查离根最近的黑边,相当于白边连通的点集被黑边割断,点集割断是比较难考虑的,不妨倒过来考虑,在一个有黑边的图中将边染白
首先正向染黑并记录最开始染黑的时间,全部操作完成后,将始终为白的边的两端并入dsu,维护深度最小的为dsu的根,这个时候该根的父边就是该dsu中每个点离根最近的黑边
接下来把操作倒过来做,每次如果是染黑操作就看看哪些边是这个时间染黑的,将其染白,并入dsu(维护深度最小的为dsu的根),如果是询问就输出该点在dsu中的根的连接父亲的边
看哪些边是这个时间的染黑的,只需要在最开始正向染黑完成后,把所有黑边拷贝出来,按照时间排个序就能快速查询

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e6 + 15;

struct edge {
    int v, nxt;
};
struct info {
    int op, u, v;
};
struct edgeColorInfo {
    int v, idx;
    bool operator < (const edgeColorInfo& b) const {
        return idx < b.idx;
    }
};
edge e[N << 1];
int head[N], tot;
int ft[N], dpt[N], preEdge[N], par[N];
info input[N];
int isColor[N];
int ans[N], ansTot, ecTot;
edgeColorInfo ec[N];

inline char get(void) {
    static char buf[1000000], *p1 = buf, *p2 = buf;
    if (p1 == p2) {
        p2 = (p1 = buf) + fread(buf, 1, 1000000, stdin);
        if (p1 == p2) return EOF;
    }
    return *p1++;
}

inline int read() {
    int x = 0;
    static char c;
    bool minus = false;
    for (; !(c >= '0' && c <= '9'); c = get()) if (c == '-') minus = true;
    for (; c >= '0' && c <= '9'; x = x * 10 + c - '0', c = get());
    if (minus) x = -x;
    return x;
}

char WritellBuffer[1024];
template <typename T>
inline void write(T a,char end) {
    ll cnt=0,fu=1;
    if(a<0) {
        putchar('-');
        fu=-1;
    }
    do {
        WritellBuffer[++cnt]=fu*(a%10)+'0';
        a/=10;
    } while(a);
    while(cnt) {
        putchar(WritellBuffer[cnt]);
        --cnt;
    }
    if(end) putchar(end);
}

inline void dsuInit() {
    for(int i = 0; i < N; i++) {
        ft[i] = i;
    }
}

inline void init() {
    memset(head, -1, sizeof(head));
    tot = ansTot = dpt[1] = par[1] = 0;
    preEdge[1] = -1;
}

inline void addEdge(int u, int v) {
    e[tot] = edge{v, head[u]};
    head[u] = tot++;
}

inline int find(int x) {
    return ft[x] == x ? x : ft[x] = find(ft[x]);
}

inline void merge(int x, int y) {
    int p = find(x), q = find(y);
    if(p != q) {
        if(dpt[p] > dpt[q]) {
            swap(p, q);
        }
        ft[q] = p;
    }
}

void dfs(int u) {
    for(int i = head[u]; ~i; i = e[i].nxt) {
        int v = e[i].v;
        if(v == par[u]) {
            continue;
        }
        dpt[v] = dpt[u] + 1;
        par[v] = u;
        preEdge[v] = i >> 1;
        dfs(v);
    }
}

void color(int u, int v, int idx) {
    u = find(u), v = find(v);
    while(u != v) {
        if(dpt[u] > dpt[v]) {
            swap(u, v);
        }
        isColor[v] = idx;
        merge(v, par[v]);
        v = find(v);
    }
}

int main() {
    int n = read(), m = read();
    init();
    dsuInit();
    memset(isColor, 0, sizeof(isColor));
    for(int i = 1; i <= n - 1; i++) {
        int u = read(), v = read();
        addEdge(u, v);
        addEdge(v, u);
    }
    dfs(1);

    for(int i = 1; i <= m; i++) {
        input[i].op = read(), input[i].u = read();
        if(input[i].op == 2) {
            input[i].v = read();
            color(input[i].u, input[i].v, i);
        }
    }

    dsuInit();
    ecTot = 0;
    for(int i = 2; i <= n; i++) {
        if(isColor[i] == 0) {
            merge(i, par[i]);
        } else {
            ec[++ecTot] = edgeColorInfo{i, isColor[i]};
        }
    }
    sort(ec + 1, ec + 1 + ecTot);

    for(int i = m; i >= 1; i--) {
        if(input[i].op == 1) {
            ans[ansTot++] = preEdge[find(input[i].u)] + 1;
        } else {
            while(ecTot > 0 && ec[ecTot].idx == i) {
                merge(par[ec[ecTot].v], ec[ecTot].v);
                ecTot--;
            }
        }
    }

    for(int i = ansTot - 1; i >= 0; i--) {
        write(ans[i], '\n');
    }

}

Extends
目前看来,貌似从儿子找根比从根找儿子更容易

Bond - UVA 11354

Description
给定一个无向带权图,多组询问,问(u,v)上的最大边权,同时要求最大边权最小
Sample Input

2
3
1 2
1 3
9
9 4
4 3
1 3
7 4
1 6
5 7
2 4
6 8

Sample Output

Case #1: 3
Case #2: 21

Solution —— MST + 并查集的按秩合并
本题之前写过solution于是跳过一部分
如果u所在的dsu与v所在的dsu使用按秩合并合并,那么这一条连边在dsu中必然在(u,v)的路径上,但是并不知道具体是哪一条,如果能够在这一条边上维护所需的最大、最小等一整条路径上的突出信息(?),就可以通过暴力爬树到lca的方法,遍历一整条路径获取该值
在本题中,使用kruska求mst,因为kruskal求mst的性质,(u,v)路径上的最大值所在边一定是将(u,v)连通的边,那么在dsu中暴力爬到lca(u,v),取一整条路径的最大值,即使不知道这条边是哪一条,也可以获取到最大值这一信息

#include <bits/stdc++.h>
#define lson l, m
#define rson m + 1, r
using namespace std;
const int N = (int)1e5  + 5;
typedef long long ll;

struct edge {
    int u, v, w;
    bool operator < (const edge& b) {
        return w < b.w;
    }
};
edge e[N];
int ft[N], sz[N], val[N];

inline void init(int n) {
    for(int i = 0; i <= n; i++) {
        ft[i] = i;
        sz[i] = 1;
        val[i] = 0;
    }
}

inline int find(int x) {
    return ft[x] == x ? x : find(ft[x]);
}

inline void merge(int x, int y, int w) {
    int p = find(x), q = find(y);
    if(p != q) {
        if(sz[p] < sz[q]) {
            swap(p, q);
        }
        ft[q] = p;
        sz[p] += sz[q];
        val[q] = w;
    }
}

inline void kruskal(int m) {
    sort(e, e + m);
    for(int i = 0; i < m; i++) {
        int u = e[i].u, v = e[i].v;
        merge(u, v, e[i].w);
    }
}

inline int solve(int u, int v) {
    int ans = 0;
    while(u != v) {
        if(sz[u] < sz[v]) {
            swap(u, v);
        }
        ans = max(ans, val[v]);
        v = ft[v];
    }
    return ans;
}

int main(){
    int n, m;
    bool isFirst = true;
    while(~scanf("%d%d", &n, &m)) {
        if(!isFirst) {
            puts("");
        }
        isFirst = false;

        init(n);
        for(int i = 0; i < m; i++) {
            scanf("%d%d%d", &e[i].u, &e[i].v, &e[i].w);
        }
        kruskal(m);

        int q;
        scanf("%d", &q);
        while(q--) {
            int u, v;
            scanf("%d%d", &u, &v);
            printf("%d\n", solve(u, v));
        }
    }
}

Extends
路径压缩虽然效率高,但是会破坏并查集的树形结构,而按秩合并不会

可持久化并查集 by zky - HYSBZ 3673

Description
n个集合 m个操作
操作:
1 a b 合并a,b所在集合
2 k 回到第k次操作之后的状态(查询算作操作)
3 a b 询问a,b是否属于同一集合,是则输出1否则输出0

0<n,m<=2*10^4
Sample Input

5 6
1 1 2
3 1 2
2 0
3 1 2
2 1
3 1 2

Sample Output

1
0
1

Solution —— 可持久化并查集
可持久化并查集一般用主席树实现,毕竟就是可持久化数组嘛,每次合并改一个根的值,正好满足主席树的要求
一般采用按秩合并的方式,听说可以用路径压缩(不会,告辞 QAQ

#include <bits/stdc++.h>
#define lson l, m
#define rson m + 1, r
using namespace std;
const int N = (int)2e4 + 5;
typedef long long ll;

int root[N * 200], ls[N * 200], rs[N * 200], val[N * 200], sz[N * 200];
int tot;
int a[N];

inline void init() {
    tot = 1;
}

inline void build(int& rtx, int l, int r) {
    rtx = tot++;
    if(l == r) {
        sz[rtx] = 1;
        val[rtx] = a[l];
        return;
    }
    int m = (l + r) >> 1;
    build(ls[rtx], lson);
    build(rs[rtx], rson);
}

inline int query(int rtx, int pos, int l, int r) {
    if(l == r) {
        return rtx;
    }
    int m = (l + r) >> 1;
    if(pos <= m) {
        return query(ls[rtx], pos, lson);
    } else {
        return query(rs[rtx], pos, rson);
    }
}

inline void update(int& rtx, int rty, int pos, int x, int l, int r) {
    rtx = tot++;
    if(l == r) {
        val[rtx] = x;
        return;
    }
    ls[rtx] = ls[rty];
    rs[rtx] = rs[rty];
    int m = (l + r) >> 1;
    if(pos <= m) {
        update(ls[rtx], ls[rty], pos, x, lson);
    } else {
        update(rs[rtx], rs[rty], pos, x, rson);
    }
}

inline int find(int x, int rtx, int n) {
    int nodey = query(rtx, x, 1, n);
    return val[nodey] == x ? nodey : find(val[nodey], rtx, n);
}

int main(){
    int n, m;
    while(~scanf("%d%d", &n, &m)) {
        init();
        for(int i = 1; i <= n; i++) {
            a[i] = i;
        }
        build(root[0], 1, n);

        for(int i = 1; i <= m; i++) {
            int op, a, b;
            scanf("%d%d", &op, &a);
            if(op != 2) {
                scanf("%d", &b);
            }

            if(op == 2) {
                root[i] = root[a];
                continue;
            }

            root[i] = root[i - 1];
            if(op == 1) {
                int nodeP = find(a, root[i], n);
                int nodeQ = find(b, root[i], n);
                if(val[nodeP] == val[nodeQ]) {
                    continue;
                }
                if(sz[nodeP] < sz[nodeQ]) {
                    swap(nodeP, nodeQ);
                }
                sz[nodeP] += sz[nodeQ];
                update(root[i], root[i - 1], val[nodeQ], val[nodeP], 1, n);
            } else {
                int nodeP = find(a, root[i], n);
                int nodeQ = find(b, root[i], n);
                //printf("||%d %d\n", val[nodeP], val[nodeQ]);
                printf("%d\n", val[nodeP] == val[nodeQ] ? 1 : 0);
            }
        }
    }
}