2019牛客第八场多校 E_Explorer 可撤销并查集(栈)+线段树

@

题意:

链接

题目类似:CF366D,Gym101652T
本题给你\(n(100000)\)个点\(m(10000)\)条边,每无向边允许通过编号在\([Li,Ri](1\le Li\le Ri\le 1e9)\)内的人,问从\(1\)\(n\)能通过多少个人。


分析:

赛中艹了30多发暴力无济于事。。。
因为以前做过一道数据范围1000的原题,当时做法好像是离散化后枚举区间暴力跑\(dfs\)查询或者带着区间跑暴力\(BFS\)最后检查一遍。。。

流下了菜鸡的泪水,主要是思维受限了,这很致命。
听说这题可以LCT写,目前只会可撤销并查集+线段树的写法。。
在这里插入图片描述

  • 上面讲的也很清楚了,把所有的权值离散化一下,然后建一个以权值为关键字的线段树
  • 这里提一下,这个线段树和上一场牛客的E题一模一样,是左闭右开线段树,就是每个叶子节点代表的是一段权值区间。这个线段树的解释:here
  • 然后线段树上每个节点存的是覆盖当前点的权值区间的所有边。
  • 区间更新完之后,把整颗树遍历一遍得出答案。
  • 对于从根到叶子节点这段的权值区间中包含的边,用一个并查集维护连通性。
  • 回溯时要撤销上面几条边的影响。记录一下合并上一条边的两个根,然后还原他们的\(fa[],rnk[]\)就行。
  • 用一个栈记录加入的边即可,后进先出的结构正适合本题的撤销操作。还有,可撤销并查集不能路径压缩,必须按秩合并。

Code

const int MXN = 3e3 + 7;
const int MXE = 2e6 + 7;
int n, m;
vector<int> sum[MXN<<2], vs;
int fa[MXN], rnk[MXN], top;
struct lh {
    int fi, se, u, v;
}stak[MXN];
struct lp {
    int u, v, l ,r;
}cw[MXN];
void update(int L, int R, int v, int l ,int r, int rt) {
    if(L <= l && r <= R) {
        sum[rt].eb(v);
        return;
    }
    int mid = (l + r) >> 1;
    if(L > mid) update(L, R, v, mid + 1, r, rt<<1|1);
    else if(R <= mid) update(L, R, v, l, mid, rt<<1);
    else {
        update(L, mid, v, l, mid, rt<<1), update(mid + 1, R, v, mid + 1, r, rt<<1|1);
    }
}
int Fi(int x) {
    return fa[x] == x? x: Fi(fa[x]);
}
int pa, pb, ans;
void build(int l, int r, int rt) {
    if(l == r) {
//        debug(l, r, Fi(3), Fi(5))
        for(auto x: sum[rt]) {
            pa = Fi(cw[x].u), pb = Fi(cw[x].v);
            if(pa == pb) continue;
//            debug(l, r, cw[x].u, cw[x].v)
            if(rnk[pa] > rnk[pb]) swap(pa, pb);
            fa[pa] = pb;
            rnk[pb] += rnk[pa];
            stak[++ top] = {rt, x, pa, pb};
        }
        if(Fi(1) == Fi(n)) ans += vs[l+1] - vs[l];
        while(top && stak[top].fi == rt) {//撤销
            int x = stak[top].se;
            if(rnk[stak[top].u] > rnk[stak[top].v]) {
                fa[stak[top].v] = stak[top].v;
                rnk[stak[top].u] -= rnk[stak[top].v];
            }else {
                fa[stak[top].u] = stak[top].u;
                rnk[stak[top].v] -= rnk[stak[top].u];
            }
            -- top;
        }
//        debug(l, r, Fi(3), Fi(5))
        return;
    }
    int mid = (l + r) >> 1;
//    debug(l, r, mid, rt)
    for(auto x: sum[rt]) {
        pa = Fi(cw[x].u), pb = Fi(cw[x].v);
        if(pa == pb) continue;
//        debug(pa, pb)
        if(rnk[pa] > rnk[pb]) swap(pa, pb);
        fa[pa] = pb;
        rnk[pb] += rnk[pa];
        stak[++ top] = {rt, x, pa, pb};
    }
    build(l, mid, rt << 1), build(mid + 1, r, rt << 1 | 1);
    while(top && stak[top].fi == rt) {//撤销
        int x = stak[top].se;
        if(rnk[stak[top].u] > rnk[stak[top].v]) {
            fa[stak[top].v] = stak[top].v;
            rnk[stak[top].u] -= rnk[stak[top].v];
        }else {
            fa[stak[top].u] = stak[top].u;
            rnk[stak[top].v] -= rnk[stak[top].u];
        }
        -- top;
    }
}
int main() {
#ifndef ONLINE_JUDGE
    freopen("/home/cwolf9/CLionProjects/ccc/in.txt", "r", stdin);
//    freopen("/home/cwolf9/CLionProjects/ccc/out.txt", "w", stdout);
#endif
    n = read(), m = read();
    for(int i = 1; i <= n; ++i) fa[i] = i, rnk[i] = 1;
    vs.eb(0);
    for(int i = 1; i <= m; ++i) {
        cw[i].u =read(), cw[i].v = read();cw[i].l = read(), cw[i].r = read();
        vs.eb(cw[i].l), vs.eb(cw[i].r + 1);
    }
    my_unique(vs);
//    for(auto x: vs) printf("%d ", x); printf("\n");
    for(int i = 1; i <= m; ++i) {
        update(lower_bound(all(vs), cw[i].l) - vs.begin(), upper_bound(all(vs), cw[i].r) - vs.begin() - 1, i, 1, vs.size() - 1, 1);
//        debug(lower_bound(all(vs), cw[i].l) - vs.begin(), upper_bound(all(vs), cw[i].r) - vs.begin() - 1, cw[i].u, cw[i].v);
    }
    build(1, vs.size() - 1, 1);
    printf("%d\n", ans);
    return 0;
}

两道待补练习题:CF 813F 可撤销并查集+分治BZOJ 3237 CDQ分治+带撤销并查集
复习带权并查集和可持久化并查集。

posted @ 2019-08-11 10:26  Cwolf9  阅读(303)  评论(0编辑  收藏  举报