You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

6.9 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

##Legacy

洛谷题目传送门

视频讲解

一、题目大意

二、解决思路

线段树优化建图的板子题

考虑暴力建图。显然不能通过此题。1≤n,q≤10^5,1≤w≤10^9,数量太大!

这时候就需要用线段树优化建图了。线段树优化建图就是 利用线段树减少连边数量,从而降低复杂度。

基本思想

先建一棵线段树。假如现在我们要从 8 号点向区间 [3,7] 的所有点连一条权值为 w 有向边。

那么怎么连边?把区间 [3,7] 拆成 [3,4][5,6][7,7] 然后分别连边。

就这样:(如下图所示。其中黑色普通边的边权为 0,粉色边的边权为 w。)

原来我们要连 5 条边,现在只需要连 3 条边,也就是 ⌈log_27⌉ 条边。

于是 O(n) 的边数就优化成了 O(logn)

那么 操作三 (op=3) 用和 操作二 (op=2) 类似的方法连边。从区间 [3,7] 的所有点向 8 号点连一条权值为 w 有向边:(其实就是边反了个方向)

以上是操作二与操作三分开来考虑的情形,那么操作二与操作三相结合该怎么办呢?

考虑建两棵线段树,第一棵只连自上而下的边,第二棵只连自下而上的边。方便起见,我们把第一棵树称作“出树”,第二棵树称作“入树”。

初始时自上而下或自下而上地在每个节点与它的父亲之间连边。由于两棵线段树的叶子节点实际上是同一个点,因此要在它们互相之间连边权为 0 的边。初始时是这样的:

接下来:

  • 对于操作一,就从入树的叶子节点向出树的叶子节点连边。
  • 对于操作二,就从入树的叶子节点向出树中的对应区间连边。
  • 对于操作三,就从入树中的对应区间向出树中的叶子节点连边。

举个栗子。比如现在我们要从 8 号点向区间 [3,7] 的所有点连一条权值为 w 有向边。那么就如图所示连边:(为了让图更清楚,图中把入树和出树叶子节点之间相连的边省略了。)

三、代码实现

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const LL INF = 0x3f3f3f3f3f3f3f3f;
const int N = 100100 << 3, M = N << 2;
typedef pair<LL, int> PII;

//最短路径
LL d[N]; //最短距离数组
bool st[N];

//邻接表
int e[M], h[N], idx, w[M], ne[M];
void add(int a, int b, int c) {
    e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}

struct Node {
    int l, r;
    int ls, rs; //左儿子编号,右儿子编号
} tr[N];

int rtin, rtout, cnt; //出树根编号,入树根编号,编号计数器(从n+1开始前面n个留给真实节点)

// l:左边界, r:右边界, 返回新创建的节点编号
int build1(int l, int r) {
    if (l == r) {
        tr[l] = {l, r}; // tr[l]:叶子节点比较牛B不用计数器生成节点号而是真实的[1~n]。这样才能真正实现最底层的重复利用,防止重复建设
        return l;
    }
    int u = ++cnt;  //非叶子节点利用cnt获取节点号
    tr[u] = {l, r}; //记录区间范围
    int mid = (l + r) >> 1;
    tr[u].ls = build1(l, mid);     //构建左子树
    tr[u].rs = build1(mid + 1, r); //构建右子树
    //父节点向左右儿子连权值为0的边
    add(u, tr[u].ls, 0), add(u, tr[u].rs, 0);
    //返回新创建的节点编号
    return u;
}

int build2(int l, int r) {
    if (l == r) {
        tr[l] = {l, r};
        return l;
    }
    int u = ++cnt;
    tr[u] = {l, r};
    int mid = (l + r) >> 1;
    tr[u].ls = build2(l, mid);
    tr[u].rs = build2(mid + 1, r);
    //左右儿子向父节点连权值为0的边
    add(tr[u].ls, u, 0), add(tr[u].rs, u, 0);
    return u;
}
/**
 * @brief 点向区间连边 或 区间向点连边
 *
 * @param u 线段树根节点u
 * @param x 需要连边的点
 * @param l 区间左边界
 * @param r 区间右边界
 * @param w 权值
 */
void add1(int u, int x, int l, int r, int w) {
    if (l <= tr[u].l && tr[u].r <= r) {
        add(x, u, w);
        return;
    }
    if (l <= tr[tr[u].ls].r) add1(tr[u].ls, x, l, r, w);
    if (r >= tr[tr[u].rs].l) add1(tr[u].rs, x, l, r, w);
}

void add2(int u, int x, int l, int r, int w) {
    if (l <= tr[u].l && tr[u].r <= r) {
        add(u, x, w);
        return;
    }
    if (l <= tr[tr[u].ls].r) add2(tr[u].ls, x, l, r, w);
    if (r >= tr[tr[u].rs].l) add2(tr[u].rs, x, l, r, w);
}

void dijkstra(int s) {
    memset(d, 0x3f, sizeof(d));
    d[s] = 0;
    priority_queue<PII, vector<PII>, greater<PII>> q;
    q.push({0, s});
    while (q.size()) {
        int u = q.top().second;
        q.pop();
        if (st[u]) continue;
        st[u] = true;
        for (int i = h[u]; ~i; i = ne[i]) {
            int j = e[i];
            if (d[j] > d[u] + w[i]) {
                d[j] = d[u] + w[i];
                q.push({d[j], j});
            }
        }
    }
}

int main() {
    //加快读入
    ios::sync_with_stdio(false), cin.tie(0);
    memset(h, -1, sizeof h);
    int n, q, s;
    cin >> n >> q >> s;

    cnt = n;              //可用的合法编号从n+1开始
    rtin = build1(1, n);  //构建入树
    rtout = build2(1, n); //构建出树

    int op, x, y, l, r, w;
    for (int i = 1; i <= q; i++) {
        cin >> op;
        if (op == 1) { //点x到点y有一条边权w的边
            cin >> x >> y >> w;
            add(x, y, w);
        } else if (op == 2) { //点x到区间[l,r]有一条边权w的边
            cin >> x >> l >> r >> w;
            add1(rtin, x, l, r, w);
        } else if (op == 3) { //区间[l,r]到点x有一条边权w的边
            cin >> x >> l >> r >> w;
            add2(rtout, x, l, r, w);
        }
    }
    //最短路径
    dijkstra(s);

    //输出最短路
    for (int i = 1; i <= n; i++)
        printf("%lld ", d[i] == INF ? -1 : d[i]);
    putchar('\n');

    return 0;
}