介绍

线段树优化建图用于解决图中边数过多,不能直接建的问题。

直接看例题:

例1 CF786B. Legacy

题意

给定 $n$ 个点的有向带权图,初始状态下图中没有边。给定 $q$ 个操作。每次操作有 $3$ 种:

$1~u~v~w$:从 $u$ 向 $v$ 连一条权值为 $w$ 的边。

$2~u~l~r~w$:从 $u$ 向 $[l,r]$ 的所有点连一条权值为 $w$ 的边。

$3~u~l~r~w$:从 $[l,r]$ 的所有点向 $u$ 连一条权值为 $w$ 的边。

最后输出从点 $s$ 出发到其他所有点的最短路。

我们利用线段树优化建图。建一棵线段树,假设我们要从 $8$ 连向 $[3,7]$,则我们可以这样建:

img

这样每次只需要建 $O(\log n)$ 条边。

其中,连出的粉色边拥有边权 $w$,而线段树上的边的边权均为 $0$,表达了从上面的节点可以无损耗的来到叶子节点(也就是原图中真正的节点)。

如果是从 $[3,7]$ 连向 $8$,则是反过来的。

但不能把这些边都建在同一棵树上,需要两棵线段树分别来表示这两种操作。

建立两棵线段树,一个的边是从上到下(叫 out-tree),一个的边是从下到上(叫 in-tree),而这些的线段树的叶子节点均代表原图中的节点。它们本质上是同一个节点,所以它们之间需要连权值为 $0$ 的双向边。

img

建好以后就可以连边了,无论是从区间连到点,还是从点连到区间,都是从in-tree出发,连到out-tree。其中,

  1. 区间 $\rightarrow$ 点:in-tree 的区间节点 $\rightarrow$ out-tree 的叶子节点。
  2. 点 $\rightarrow$ 区间:in-tree 的叶子节点 $\rightarrow$ out-tree 的区间节点。

最后怎么跑最短路呢?

实际上就是在这两棵线段树上跑,只不过除了原图中的点(叶子节点)以外,多了线段树上的一些点而已。我们就从任何一棵树的叶子节点 $s$ 出发,然后到任何一棵树的其他叶子节点的最短距离即是我们的最短路。本质上是给原图添加了一些辅助点。

• 写代码的时候用 uu+N 区分两棵树上的节点,并且注意原图中的点对应的是叶子节点,所以编号不再是 1-n 了,用 inleaf[maxn], outleaf[maxn] 来记录编号。

时间复杂度:$O(m \log m) = O(n\log^2n)$,空间复杂度:$O(m) = O(n \log n)$。

代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e6+500;

struct Edge {
    int u, v, w;
};
vector<Edge> adj[maxn];
struct Node {
    int v; ll d;
    bool operator<(const Node& other) const {
        return d > other.d;
    }
};
struct Dijkstra {
    bool vis[maxn];
    ll dis[maxn];
    priority_queue<Node> pq;
    void run(int x) {
        memset(dis, -1, sizeof(dis));
        memset(vis, 0, sizeof(vis));
        pq.push({x, 0});
        dis[x] = 0;
        while (pq.size()) {
            auto [u, d] = pq.top(); pq.pop();
            if (vis[u]) continue;
            vis[u] = 1;
            for (auto [_, v, w] : adj[u]) {
                if (dis[v] == -1 || dis[v] > dis[u] + w) {
                    dis[v] = dis[u] + w;
                    pq.push({v, dis[v]});
                }
            }
        }
    }
} di;
int n, q, s;
int N = 5e5;
void addEdge(int u, int v, int w) {
    adj[u].push_back({u,v,w});
}

// [1...N] 是 out-tree的范畴,[N+1...2N] 是in-tree的范畴
int outleaf[maxn], inleaf[maxn];
void build(int cur, int l, int r) {
    if (l == r) {
        outleaf[l] = cur;
        inleaf[l] = cur+N;
        addEdge(outleaf[l], inleaf[l], 0);
        addEdge(inleaf[l], outleaf[l], 0);
        return;
    }
    int mid = (l+r) >> 1;
    int lc = cur<<1, rc = lc|1;
    addEdge(cur, lc, 0);
    addEdge(cur, rc, 0);
    addEdge(lc+N, cur+N, 0);
    addEdge(rc+N, cur+N, 0);
    build(cur<<1, l, mid);
    build(cur<<1|1, mid+1, r);
}

// type = 1 单独处理了 (从 in->out 或者 反过来都没区别)
// type = 2: 从 u 连一条边到 [L,R],那么从 inleaf[u] -> out  (N+1...2N -> 1...N)
// type = 3: 从 [L,R] 连一条边到 u,那么从 in -> outleaf[u]  (N+1...2N -> 1...N)
// w 为权值
void add(int cur, int l, int r, int u, int L, int R, int w, int type) {
    if (L <= l && R >= r) {
        if (type == 2) addEdge(inleaf[u], cur, w);
        else addEdge(cur+N, outleaf[u], w);
        return;
    }
    int mid = (l+r) >> 1;
    if (L <= mid) add(cur<<1, l, mid, u, L, R, w, type);
    if (R > mid) add(cur<<1|1, mid+1, r, u, L, R, w, type);
}

int main() {
    fastio;
    cin >> n >> q >> s;
    build(1, 1, n);
    while (q--) {
        int type; cin >> type;
        if (type == 1) {
            int u, v, w; cin >> u >> v >> w;
            addEdge(inleaf[u], outleaf[v], w);
        } else {
            int u, L, R, w; cin >> u >> L >> R >> w;
            add(1, 1, n, u, L, R, w, type);
        }
    }
    di.run(inleaf[s]);
    for (int i = 1; i <= n; i++) {
        cout << di.dis[outleaf[i]] << " ";
    }
    cout << "\n";
}

例2 洛谷P6348 [PA2011] Journeys

题意

$n$ 个节点的无权无向图中,有 $m$ 条道路。

每条道路代表从 $[a,b]$ 都能走到 $[c,d]$。

给定出发点 $s$,求从 $s$ 出发到其他点的最短路。

其中,$n \leq 5 \times 10^5, m \leq 10^5$。

题解

几乎和上一道题一样,区别在于这次可以从区间连到区间了,而且是双向边。

把一个区间 $[a,b]$ 拆成线段树上的节点 $x_1,x_2,…,x_k$,然后把 $[c,d]$ 拆成 $y_1,y_2,…,y_l$。

然后对于每个 $i,j$,都连 $x_i \leftrightarrow y_j$。不过这样每次要连 $O(log^2 n)$ 条边,复杂度太高了。

可以建一个虚拟的点 $u$,然后将所有 $x_i \rightarrow u \rightarrow y_j$。由于是双向边,就再建一个 $v$,$y_j \rightarrow v \rightarrow x_i$。

这样复杂度又降回 $O(\log n)$ 了。

• 最后注意这是无权图,跑个 01-BFS 找最短路即可。

代码
#include <bits/stdc++.h>
using namespace std;
struct Edge {
    int u, v, w;
};
vector<Edge> adj[maxn];
int n, m, s;
int N = 2e6;
void addEdge(int u, int v, int w) {
    adj[u].push_back({u,v,w});
}

// [1...N] 是 out-tree的范畴,[N+1...2N] 是in-tree的范畴
int outleaf[maxn], inleaf[maxn];
void build(int cur, int l, int r) {
    if (l == r) {
        outleaf[l] = cur;
        inleaf[l] = cur+N;
        addEdge(outleaf[l], inleaf[l], 0);
        addEdge(inleaf[l], outleaf[l], 0);
        return;
    }
    int mid = (l+r) >> 1;
    int lc = cur<<1, rc = lc|1;
    addEdge(cur, lc, 0);
    addEdge(cur, rc, 0);
    addEdge(lc+N, cur+N, 0);
    addEdge(rc+N, cur+N, 0);
    build(cur<<1, l, mid);
    build(cur<<1|1, mid+1, r);
}

void find_nodes(int cur, int l, int r, int L, int R, vector<int>& nodes) {
    if (L <= l && R >= r) {
        nodes.push_back(cur);
        return;
    }
    int mid = (l+r) >> 1;
    if (L <= mid) find_nodes(cur<<1, l, mid, L, R, nodes);
    if (R > mid) find_nodes(cur<<1|1, mid+1, r, L, R, nodes);
}

int id = 0;
void add(int L1, int R1, int L2, int R2) {
    vector<int> nodes1, nodes2;
    find_nodes(1, 1, n, L1, R1, nodes1);
    find_nodes(1, 1, n, L2, R2, nodes2);

    int nd1 = 2*N + id + 1, nd2 = 2*N + id + 2;
    id += 2;
    for (int x : nodes1) {
        addEdge(x+N, nd1, 1);
        addEdge(nd2, x, 1);
    }
    for (int y : nodes2) {
        addEdge(nd1, y, 1);
        addEdge(y+N, nd2, 1);
    }
}


deque<int> q;
int dis[maxn];
bool vis[maxn];
void bfs01(int x) {
    memset(dis, -1, sizeof(dis));
    q.push_back(x);
    dis[x] = 0;
    while (q.size()) {
        int u = q.front(); q.pop_front();
        if (vis[u]) continue;
        vis[u] = 1;
        for (auto [_, v, w] : adj[u]) {
            if (dis[v] == -1 || dis[v] > dis[u] + w) {
                dis[v] = dis[u] + w;
                if (w == 0) q.push_front(v);
                else q.push_back(v);
            }
        }
    }
}

int main() {
    fastio;
    cin >> n >> m >> s;
    build(1, 1, n);
    while (m--) {
        int a, b, c, d; cin >> a >> b >> c >> d;
        add(a, b, c, d);
    }
    bfs01(inleaf[s]);
    for (int i = 1; i <= n; i++) {
        cout << dis[outleaf[i]] / 2 << "\n";
    }
}

例3 洛谷P5025 [SNOI2017] 炸弹

题意

在一条直线上有 $n$ 个炸弹,每个炸弹的坐标是 $x_i$,爆炸半径是 $r_i$。

当一个炸弹 $i$ 爆炸时,如果另一个炸弹 $j$ 满足 $|x_j-x_i| \leq r_i$,那么炸弹 $j$ 也会被引爆。

对于每一个 $i$,计算如果一开始引爆 $i$,将会有多少个炸弹被引爆,设这个值为 $k_i$,计算 $\sum\limits_{i=1}^n i*k_i$。

其中,$n \leq 5 \times 10^5, |x_i| \leq 10^{18}, r_i \in [0, 2\times 10^{18}]$。

题解

显然,一个炸弹 $i$ 被引爆后,一定影响的是以它为中心的一个区间的范围。那么可以用二分求出这个区间,然后线段树优化建图来连边。

建完边以后,相当于给定一个图,问从每一个节点出发,能到达多少个节点?

那么可以先SCC缩点,得到一个DAG,然后在DAG上跑dp。

但DAG上的dp其实有一个问题,见下图:

img

节点 $2$ 和 $3$ 可到达的数量均为 $2$,但这样的话节点 $4$ 可到达的数量就会变成 $5$,因为节点 $1$ 被统计了两次。

并没有什么好办法来处理这个问题,但我们注意到,初始情况下引爆一个炸弹,最后的影响范围一定是一个区间。

于是 dp 值维护的是能够影响到的区间左端点和右端点,而不是影响到的节点数量,这样就没有这个问题了。

代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 4e6+50000;

vector<int> adj[maxn];
struct Node {
    int v; ll d;
    bool operator<(const Node& other) const {
        return d > other.d;
    }
};
int n;
int N = 2e6;
void addEdge(int u, int v) {
    adj[u].push_back(v);
}
// [1...N] 是 out-tree的范畴,[N+1...2N] 是in-tree的范畴
int outleaf[maxn], inleaf[maxn];
void build(int cur, int l, int r) {
    if (l == r) {
        outleaf[l] = cur;
        inleaf[l] = cur+N;
        addEdge(outleaf[l], inleaf[l]);
        addEdge(inleaf[l], outleaf[l]);
        return;
    }
    int mid = (l+r) >> 1;
    int lc = cur<<1, rc = lc|1;
    addEdge(cur, lc);
    addEdge(cur, rc);
    addEdge(lc+N, cur+N);
    addEdge(rc+N, cur+N);
    build(cur<<1, l, mid);
    build(cur<<1|1, mid+1, r);
}
void add(int cur, int l, int r, int u, int L, int R) {
    if (L <= l && R >= r) {
        addEdge(inleaf[u], cur);
        return;
    }
    int mid = (l+r) >> 1;
    if (L <= mid) add(cur<<1, l, mid, u, L, R);
    if (R > mid) add(cur<<1|1, mid+1, r, u, L, R);
}

// from[u] 代表 u 所在的SCC编号,scc代表scc编号,sz[scc] 代表对应scc的大小
struct Tarjan {
    int dfn[maxn], low[maxn], id, from[maxn], scc = 0, sz[maxn];
    bool in[maxn];  // instack or not
    int st[maxn], tail = -1;
    void dfs(int u) {
        in[u] = 1;
        st[++tail] = u;
        dfn[u] = low[u] = ++id;
        for (int to : adj[u]) {
            if (dfn[to] && in[to]) low[u] = min(low[u], dfn[to]);  // 要记得在栈内
            if (!dfn[to]) {
                dfs(to);
                low[u] = min(low[u], low[to]);
            }
        }
        if (dfn[u] == low[u]) {
            from[u] = ++scc;
            sz[scc] = 1;
            while (tail >= 0 && st[tail] != u) {
                int cur = st[tail];
                from[cur] = from[u];
                sz[scc]++;
                tail--;
                in[cur] = 0;  // 记得这里,将在栈中的标记去掉
            }
            tail--;
            in[u] = 0;  // 记得这里,将在栈中的标记去掉
        }
    }
    // 跑tarjan
    void solve() {
        for (int i = 1; i <= N*2; i++) {
            if (!dfn[i]) dfs(i);
        }
    }
} tj;

struct Graph {
    int n;
    vector<int> adj[maxn];
    int w[maxn], deg[maxn];
    pii dp[maxn];
    void addEdge(int u, int v) {
        adj[u].push_back(v);
    }
    void remove_dup() {
        for (int i = 1; i <= n; i++) {
            sort(adj[i].begin(), adj[i].end());
            adj[i].resize(unique(adj[i].begin(), adj[i].end()) - adj[i].begin());
            for (int v : adj[i]) deg[v]++;  // 注意重边,所以不能在addEdge的时候统计deg,得先去重
        }
    }

    vector<int> seq;  // 拓扑排序的逆序列
    void topo() {
        vector<int> q;
        for (int i = 1; i <= n; i++) {
            if (!deg[i]) q.push_back(i);
        }
        while (q.size()) {
            int u = q.back();
            q.pop_back();
            for (int v : adj[u]) {
                deg[v]--;
                if (deg[v] == 0) {
                    q.push_back(v);
                }
            }
            seq.push_back(u);
        }
        reverse(seq.begin(), seq.end());
    }

    void run_dp() {
        for (int u : seq) {
            for (int v : adj[u]) {
                dp[u].first = min(dp[u].first, dp[v].first);
                dp[u].second = max(dp[u].second, dp[v].second);
            }
        }
    }
} graph;

struct Bomb {
    ll x, r;
    int id;
    bool operator<(const Bomb& other) const {
        return x < other.x;
    }
} a[maxn];
int main() {
    fastio;
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i].x >> a[i].r, a[i].id = i;
    sort(a+1, a+n+1);

    build(1, 1, n);
    for (int i = 1; i <= n; i++) {
        int l = lower_bound(a+1, a+i+1, Bomb {a[i].x - a[i].r, 0, 0}) - a;
        int r = prev(upper_bound(a+i+1, a+n+1, Bomb {a[i].x + a[i].r, 0, 0})) - a;
        add(1, 1, n, i, l, r);
    }

    tj.solve();
    for (int i = 1; i <= N*2; i++) {
        for (int j : adj[i]) {
            int fu = tj.from[i], fv = tj.from[j];
            if (fu == fv) continue;
            graph.addEdge(fu, fv);
        }
    }
    graph.n = tj.scc;
    for (int i = 1; i <= graph.n; i++) graph.dp[i] = {1e9, -1};

    for (int i = 1; i <= n; i++) {
        int u = tj.from[inleaf[i]];
        graph.dp[u].first = min(graph.dp[u].first, i);
        graph.dp[u].second = max(graph.dp[u].second, i);
    }
    graph.remove_dup();
    graph.topo();
    graph.run_dp();

    Z ans = 0;
    for (int i = 1; i <= n; i++) {
        Z res = 0;
        int from = tj.from[inleaf[i]];
        ll num = graph.dp[from].second - graph.dp[from].first + 1;
        res = num * a[i].id;
        ans += res;
    }
    cout << ans << endl;
}