原题链接: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;
}