线段树优化建图

线段树优化建图经常用来优化单点向某个区间连边,区间向单点连边的问题,可以将边数从 \(qn\) 级别降至 \(q \log n\) 级别。

具体地,从一道题来引入:CF786B Legacy

先单独考虑从点 \(u\) 向区间 \([l, r]\) 连边的操作。

你会发现因为任意一个区间 \([l, r]\) 在线段树上都可以被表示为 \(\log n\) 个区间,我们可以尝试从 \(u\) 向这 \(\log n\) 个区间直接连边,然后像加法一样打上一个懒标记。

那么问题就在于如何下传这个懒标记,你会发现如果 \(u\) 可以走到 \([l, r]\) 这个区间,那么一定可以走到 \([l, r]\) 在线段树上的子区间。

因此,我们可以事先将线段树上的每个区间向其子区间连一条有向边,那么懒标记的问题也就不存在了。

再来考虑从区间 \([l, r]\) 向单点 \(u\) 连边的操作。

同样地,我们新开一颗线段树,每次将区间 \([l, r]\) 在线段树上对应的 \(\log n\) 个区间向 \(u\) 连一条有向边。

但由于现在变成了一个区间的儿子区间都能走出去,因此我们预先需要从儿子区间向父亲区间连一条有向边。

但是你会发现我们的单点 \(u\) 是还未定义的,还需要考虑这最后一点。

一个最简单的想法就是直接开一排的点 \(1 \sim n\),然后将这上述两个操作连到这 \(n\) 个点上。

于此同时,因为实际上在我们的构造中出现了 \(3\) 个点均代表原图上的同一个点,但构造中这 \(3\) 个可能可以到达不同点。

因此,我们还需要将这三个点联通。

但实际上是不需要这样的,不难发现单点也可以被认为是线段树上的一个区间,因此可以直接用两个线段树的底层代表单点。

同时,还是需要保证单点在图上的一致性,因此还需要将两个单点之间连上一条无向边。

#include <bits/stdc++.h>
using namespace std;
#define ls t[p].l
#define rs t[p].r
#define mid (l + r >> 1)
#define rep(i, l, r) for (int i = l; i <= r; ++i)
#define Next(i, u) for (int i = h[u]; i; i = e[i].next)
const int N = 400000 + 5;
const int M = 30 + 5;
const long long inf = 1e14;
struct edge { int v, next, w;} e[N * M];
struct tree { int l, r;} t[N];
struct node { 
    int p; long long w;
    bool operator < (const node &x) const {
        return w > x.w;
    }
};
bool book[N];
priority_queue <node> Q;
long long dis[N];
int n, q, l, r, u, v, w, s, opt, tot, cnt, rt[2], h[N], pl[2][N];
int read() {
    char c; int x = 0, f = 1;
    c = getchar();
    while (c > '9' || c < '0') { if(c == '-') f = -1; c = getchar();}
    while (c >= '0' && c <= '9') x = x * 10 + c - '0', c = getchar();
    return x * f;
}
void add(int u, int v, int w, int type) { 
    if(!type) e[++tot].v = v, e[tot].w = w, e[tot].next = h[u], h[u] = tot;
    else e[++tot].v = u, e[tot].w = w, e[tot].next = h[v], h[v] = tot;
}
void build(int &p, int l, int r, int type) {
    p = ++cnt; if(l == r) { pl[type][l] = p; return;}
    build(ls, l, mid, type), build(rs, mid + 1, r, type);
    add(p, ls, 0, type), add(p, rs, 0, type);
}
void update(int p, int l, int r, int x, int y, int k, int w, int type) {
    if(l >= x && r <= y) { add(k, p, w, type); return;}
    if(mid >= x) update(ls, l, mid, x, y, k, w, type);
    if(mid < y) update(rs, mid + 1, r, x, y, k, w, type);
}
signed main() {
    n = read(), q = read(), s = read();
    build(rt[0], 1, n, 0), build(rt[1], 1, n, 1);
    rep(i, 1, n) add(pl[0][i], pl[1][i], 0, 0), add(pl[0][i], pl[1][i], 0, 1);
    rep(i, 1, q) {
        opt = read();
        if(opt == 1) u = read(), v = read(), w = read(), update(rt[0], 1, n, v, v, pl[1][u], w, 0);
        if(opt == 2) u = read(), l = read(), r = read(), w = read(), update(rt[0], 1, n, l, r, pl[1][u], w, 0);
        if(opt == 3) u = read(), l = read(), r = read(), w = read(), update(rt[1], 1, n, l, r, pl[0][u], w, 1);
    }
    rep(i, 1, cnt) dis[i] = inf;
    Q.push((node){pl[1][s], 0}), dis[pl[1][s]] = 0;
    while (!Q.empty()) {
        node u = Q.top(); Q.pop();
        if(book[u.p]) continue; book[u.p] = true;
        Next(i, u.p) {
            int v = e[i].v; 
            if(dis[v] > dis[u.p] + e[i].w) dis[v] = dis[u.p] + e[i].w, Q.push((node){v, dis[v]});
        }
    }
    rep(i, 1, n) printf("%lld ", dis[pl[0][i]] == inf ? -1 : dis[pl[0][i]]);
    return 0;
}

值得一提的是,线段树的优秀性质使得它能解决很大一部分设计区间的问题。

比如:

  • 一个区间能在线段树上用 \(\log n\) 个区间表示,这类问题通常拥有区间的包含性。换句话说,父亲区间满足的信息子区间也满足。

  • 线段树的可并性,这给一类单点修改问题很大的契机。比如:楼房重建

  • 线段总长度 \(O(n \log n)\),这给一类区间存在性问题很暴力的解法。即通常某个信息存在于某个区间,可以暴力插入到线段树上的对应区间,查询就只需要访问 \(\log n\) 个区间或最后一起访问。

posted @ 2020-10-17 22:30  Achtoria  阅读(161)  评论(0编辑  收藏  举报