Codeforces Round #406 (Div. 1) B. Legacy(最短路+线段树优化建图)

原题链接:B. Legacy


题目大意:


给出一张图,给出三个数字 n n n q q q s s s 分别代表有 n n n 个点, q q q 种操作,起点为 s s s

初始时图中只有 n n n 个孤立点,没有边,你要可以执行下面操作进行连边。

对于每一种操作:

  • 输入 1 1 1 u u u v v v w w w 代表着 u u u 号点到 v v v 号点有一条路径,权值为 w w w
  • 输入 2 2 2 u u u l l l r r r w w w 代表着 u u u 号点到标号在 [ l , r ] [l,r] [l,r] 内的所有点都有一条单向路径,权值为 w w w
  • 输入 3 3 3 v v v l l l r r r w w w 代表着标号在 [ l , r ] [l,r] [l,r] 内的所有点到 v v v 号点都有一条单向路径,权值为 w w w

给出这样一张图,要你求出从 s s s 号点出发,到其他所有点的最短距离是多少,如果不能到达则输出 − 1 -1 1

解题思路:


初见题目,就是一个普通最短路板子,但是建边很困难,先考虑暴力怎么做。

对所有的操作,暴力建边,空间复杂度是 O ( q n ) O(qn) O(qn) 的。

即使空间能接受,跑 d i j dij dij 时候也会寄,考虑如何优化建图。

对于很多条边,我们可以考虑建立一个虚拟源点,类似这样:

在这里插入图片描述

比如当 1 1 1 2 2 2 3 3 3 都要向区间在 [ 4 , 8 ] [4,8] [4,8] 的所有点连边时,这样建图与暴力地将每一个点与能到的点相连相比,会将边数从 15 15 15 条优化成 8 8 8 条,边数得到明显减少。

但是即使是这样,还是会有很多建图的问题,每个虚拟节点复用率很低,一个虚拟节点只会与一部分节点相连,虚拟节点数多了之后,效率还不如普通的连边方法。

引入一个知识:线段树优化建图


我们建立这样一颗线段树(我们称之为入树),所有父亲向子节点连出一条单向边,因为是进入节点所以叫入树(手画一画):

在这里插入图片描述
我们要怎么利用这棵树呢?

比如我们现在有一个节点 9 9 9 (按本题说不应该存在 9 9 9 ,只是举个例) ,要与区间在 [ 1 , 7 ] [1,7] [1,7] 的所有节点连一条单向边(下图表示橙色边),那么我们是不是就可以像刚才一样,和虚拟源点连边就就行了(如下图):

在这里插入图片描述
可以看到,我们如果从 9 9 9 这个节点出发,按照路径,我们是完全可以走到区间 [ l , r ] [l,r] [l,r] 内的所有点的。

同理的,我们也可以建立一颗线段树,所有子节点向父亲连出一条单向边(同理,因为是从节点出发我们称之为出树):

在这里插入图片描述
我们现在有一个区间 [ 3 , 7 ] [3,7] [3,7],所有节点都要向 10 10 10 这个节点连一条单向边(下图表示橙色边),同理,虚拟源点向着节点连边就就行了(如下图):

在这里插入图片描述

可以看到,我们如果从 [ 3 , 7 ] [3,7] [3,7] 中的任何节点出发,按照路径,我们都能到达 10 10 10 号点。

因为两棵树 [ 1 , 8 ] [1,8] [1,8] 号的所有叶子节点都是一个节点,我们把两棵树的所有叶子节点根据题意使用 双向边 相连。

在这里插入图片描述
这样子,我们的整个虚拟图,加上虚拟节点都建立好了。

通过线段树辅助建树有什么好处呢?

我们之前考虑的是节点复用率问题,我们之前建立的每一个虚拟节点管辖的点都是不定的。

但是在线段树中,每一个点虚拟节点都固定管辖着一个区间 [ l , r ] [l, r] [l,r] ,而且节点的标号也非常容易找到。

每一次建边的时间复杂度是 O ( log ⁡ n ) O(\log n) O(logn),总时间复杂度是 O ( n log ⁡ n + q log ⁡ n ) O(n \log n +q \log n) O(nlogn+qlogn) 的。空间上复杂度是 O ( n log ⁡ n + q log ⁡ n ) O(n \log n+q \log n) O(nlogn+qlogn) ,比我们新建虚拟节点来说优秀不少。

这样子,单点向区间,或者区间向单点连边时候,我们只需要跑一下线段树,找到节点管辖的范围,然后连边就好了。

可以发现我们这样建立新图的时,对于已经规定的路径,我们的所有节点都满足原图条件。对于没有规定的路径,我们的虚拟源点都不会影响到原图的正确性。

回到本题


我们将线段树应用到题目中,这题还多了一个边权的影响。

虚拟节点不能影响到原图,所以我们建立出树和入树时,要把所有树内的边权都设置为 0 0 0

我们拿第二个样例举例,先建立出树和入树:
在这里插入图片描述
根据题意,我们连边时候:

  • 节点向节点 连边,我们直接 将两个叶子节点相连 就好了(下图由深蓝色边标出)。
  • 节点向区间 连边,我们是将 叶子节点连向入树中的节点 (下图由紫色边标出)。
  • 区间向节点 连边,我们是将 出树中的节点连向叶子节点 (下图由橙色边标出)。
  • 如果还有 区间向区间 连边,那就要引入一个虚拟节点,出树中的节点连向虚拟节点虚拟节点再连向入树中的节点 ,执行两次连边操作就好了。

至于连到哪一棵树的叶子,其实都无所谓,它们本质上就是一个节点,我个人习惯是出树叶子连入树叶子,不过有时候还要根据题意连边。

我们执行连边操作可以得到:

在这里插入图片描述

建完图后,我们从起点 1 1 1 出发,跑一个 d i j dij dij 最短路就好了。

在用 S T L STL STL 的优先队列跑 d i j k s t r a dijkstra dijkstra 时,总时间复杂度大概是 O ( n log ⁡ n + q log ⁡ 2 n ) O(n \log n + q \log^{2}n) O(nlogn+qlog2n)

有直接建两棵树的方法,但我的习惯是加上一个偏移量 d x dx dx 来表示两棵树的不同节点。

AC代码:


#include <bits/stdc++.h>
#define YES return void(cout << "Yes\n")
#define NO return void(cout << "No\n")
using namespace std;

using u64 = unsigned long long;
using PII = pair<int, int>;
using i64 = long long;

const int N = 1e5 + 10, dx = 4e5;

//两棵树 范围要*10
vector<PII> g[N * 10];
i64 dist[N * 10];

//叶子节点对应的线段树标号
int leaf[N];

#define lson k << 1, l, mid
#define rson k << 1 | 1, mid + 1, r

//建立出入树操作,本题事实上没必要真正建立出线段树
//我们只需要链接树内节点,知道虚拟节点的标号就行了
void build(int k, int l, int r) {
    if (l == r) {
        //记录叶子节点的标号
        leaf[l] = k;
        return;
    }
    int mid = l + r >> 1;
    int ls = k << 1, rs = k << 1 | 1;
    build(lson), build(rson);
    //出树 儿子连父亲
    g[ls + dx].emplace_back(k + dx, 0);
    g[rs + dx].emplace_back(k + dx, 0);
    //入树 父亲连儿子
    g[k].emplace_back(ls, 0);
    g[k].emplace_back(rs, 0);
}

//op 2/3 的连边操作
int op = 0;
void connect(int k, int l, int r, int x, int y, int v, int w) {
    if (l >= x && r <= y) {
        //出树节点连向入树节点
        if (op == 2) g[v + dx].emplace_back(k, w);
        else g[k + dx].emplace_back(v, w);
        return;
    }
    int mid = l + r >> 1;
    if (x <= mid) connect(lson, x, y, v, w);
    if (y > mid) connect(rson, x, y, v, w);
}

void solve() {
    int n, m, s;
    cin >> n >> m >> s;

    build(1, 1, n);

    for (int i = 1; i <= n; ++i) {
        //将两颗树的叶子相连
        g[leaf[i]].emplace_back(leaf[i] + dx, 0);
        g[leaf[i] + dx].emplace_back(leaf[i], 0);
    }

    int u, v, l, r, w;
    for (int i = 1; i <= m; ++i) {
        cin >> op;
        if (op == 1) {
            cin >> u >> v >> w;
            //op 1 叶子直接相连
            g[leaf[u]].emplace_back(leaf[v], w);
        }
        else {
            cin >> u >> l >> r >> w;
            // 叶子连向入树/出树连向叶子 op 2/3
            // 我们递归找到管辖相关区间的线段树节点,直接相连即可
            connect(1, 1, n, l, r, leaf[u], w);
        }
    }

    // dijkstra 板子
    const i64 INF = INT64_MAX;
    fill(dist, dist + N * 10, INF);
    priority_queue<pair<i64, int>, vector<pair<i64, int>>, greater<pair<i64, int>>> heap;
    heap.emplace(0, leaf[s]);
    dist[leaf[s]] = 0;

    while (heap.size()) {
        auto [dis, u] = heap.top(); heap.pop();
        if (dist[u] < dis) continue;
        for (auto& [v, w] : g[u]) {
            if (dist[v] > dist[u] + w) {
                dist[v] = dist[u] + w;
                heap.emplace(dist[v], v);
            }
        }
    }

    //判断输出
    for (int i = 1; i <= n; ++i) {
        i64 dis = dist[leaf[i]];
        cout << (dis == INF ? -1 : dis) << ' ';
    }
}

signed main() {

    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);

    int t = 1; //cin >> t;
    while (t--) solve();

    return 0;
}


感谢观看!


  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柠檬味的橙汁

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值