线段树优化建图
线段树优化建图经常用来优化单点向某个区间连边,区间向单点连边的问题,可以将边数从 \(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\) 个区间或最后一起访问。