[洛谷P4768] [NOI2018]归程 (kruskal重构树模板讲解)

2022-11-13,,,

洛谷题目链接:[NOI2018]归程

因为题面复制过来有点炸格式,所以要看题目就点一下链接吧\(qwq\)


题意: 在一张无向图上,每一条边都有一个长度和海拔高度,小\(Y\)的家在\(1\)节点,并且他有一部车,车只能在海拔高度大于降水量的道路上行驶,如果某一条边的海拔高度小于等于降水量,那么小\(Y\)就必须下车步行,现在有\(q\)次询问,每次询问从目标点到\(1\)要步行的最短距离.强制在线.

题解: 这题我采用的做法是kruskal重构树.

可能大家对kruskal重构树并不是很熟悉,但是想必都会kruskal.我们先分析一下题目有哪些性质:

    降雨量越少,可以走的边越多,从出发点开始可以到达的点越多.
    下车后要到达\(1\)号节点,且必须步行,则选择最短路走.

根据性质\(1\),我们可以知道,每次询问的起点其实是一个集合,且在降水量越少的时候集合内的元素越多.

既然有这样的性质,那么我们可以考虑一下这题的离线做法:我们可以先预处理一下\(1\)到所有点的最短路,再将询问的降水量从大到小排个序,这样可以保证每次起点集合是在不断变大或不变的.

然后建出一颗最大生成树(以降雨量为关键字).这样虽然无法保证从起点集合中一个点到\(1\)号点的距离最小,但是可以保证起点集合尽量大.

每次询问前将所有海拔高度大于此时降雨量的边的顶点所在的并查集合并,这样就可以维护出一个起点集合.同时再在并查集中维护到达\(1\)节点的最短路,这样在查询的时候就可以保证距离是最小的了.

这样的复杂度是\(O(mlogm+k*n)\)的(主要来自于对边的排序和并查集合并的常数.


但是因为题目对查询强制在线,所以我们就必须采用一个更高效的方式来维护这个起点集合.

这时候就引入了我们的kruskal重构树.

做法与离线做法大概相似,但是在维护集合合并关系的时候并不是直接采用数组记录,而是通过开一个节点构出一颗二叉树的方式来维护.

我们还是先预处理最短路,然后将边按照海拔高度排序,然后就开始构kruskal重构树了.

在合并一条边的起点\(st\)和终点\(ed\)的时候,朴素做法是将两个点所在的并查集合并,这里我们新开一个节点,并将\(st\)和\(ed\)的父亲指向这个新节点\(node\),并将这条边的海拔计入这个点,同时维护一个并查集,将\(st\),\(ed\)和\(node\)所在的并查集合并.

那么这样合并好的一颗最小生成树,表示在重构树上就是一颗二叉树.并且它具有这样的性质:

    节点记录的海拔高度是一个二叉堆.
    某个节点所在的子树中所有的节点都是在同一个集合中的.
    在原图上的边权信息以点权的方式存入了重构树中.

既然如此,我们就可以在重构树上跑倍增了(因为节点上的海拔高度是单调递增的),并且我们可以在构树的时候将这个节点的子树中的节点距离\(1\)的最小值记录下来,这样在倍增找到起点集合的最大范围后可以\(O(1)\)回答这个集合到\(1\)的最短距离.

分析一下空间复杂度.因为每次合并两个点,集合数量都会减少\(1\),所以最多会新建\(n\)个节点.所以有关新建节点的数组都要开两倍空间.

到这里其实就做完了,对于这题有些小细节,比如不能跑\(SPFA\),在处理倍增数组的时候要将新建立的点也加入倍增的预处理中.

#include<bits/stdc++.h>
using namespace std;
const int N = 300000+5;
const int M = 400000+5;
const int inf = 2147483647; int T, n, m, q, k, s, ecnt = 0, last[N], lans = 0;
int dist[N], vis[N];
int fa[N*2], cnt = 0, mn[N*2], gup[N*2][26], val[N*2]; struct edge{
int to, nex, rain, w;
}e[M*2]; struct EDGE{
int st, ed, w, rain;
}a[M]; bool cmpe(EDGE a, EDGE b){
return a.rain > b.rain;
} struct node{
int dis, id;
bool operator < (const node &a) const{
return dis > a.dis;
}
}; int gi(){
int ans = 0, f = 1; char i = getchar();
while(i<'0' || i>'9'){ if(i == '-') f = -1; i = getchar(); }
while(i>='0' && i<='9') ans = ans*10+i-'0', i = getchar();
return ans * f;
} void clear(){
memset(last, 0, sizeof(last)), ecnt = 0, cnt = n, lans = 0;
memset(dist, 127/3, sizeof(dist)), memset(vis, 0, sizeof(vis));
memset(gup, 0, sizeof(gup));
for(int i=1;i<=n;i++) fa[i] = i;
for(int i=1;i<=n;i++) val[i] = 0;
for(int i=n+1;i<=n+n;i++) mn[i] = inf;
} void add(int x, int y, int z, int w){
e[++ecnt].to = y, e[ecnt].w = w, e[ecnt].rain = z;
e[ecnt].nex = last[x], last[x] = ecnt;
} int find(int x){ return fa[x] == x ? x : fa[x] = find(fa[x]); } void dijkstra(){
priority_queue <node> q; q.push((node){ 0, 1 }), dist[1] = 0;
while(!q.empty()){
node x = q.top(); q.pop();
if(vis[x.id]) continue; vis[x.id] = 1;
for(int to, i=last[x.id];i;i=e[i].nex){
to = e[i].to;
if(dist[to] > x.dis+e[i].w){
dist[to] = x.dis+e[i].w;
q.push((node){ dist[to], to });
}
}
}
} void init(){
for(int j=1;j<=20;j++)
for(int i=1;i<=cnt;i++) gup[i][j] = gup[gup[i][j-1]][j-1];
} int solve(int x, int lim){
for(int i=20;i>=0;i--)
if(gup[x][i] && val[gup[x][i]] > lim) x = gup[x][i];
return mn[x];
} int main(){
ios::sync_with_stdio(false);
int x, y; T = gi();
while(T--){
n = gi(), m = gi(); clear();
for(int i=1;i<=m;i++){
a[i].st = gi(), a[i].ed = gi(), a[i].w = gi(), a[i].rain = gi();
add(a[i].st, a[i].ed, a[i].rain, a[i].w);
add(a[i].ed, a[i].st, a[i].rain, a[i].w);
}
sort(a+1, a+m+1, cmpe), dijkstra();
for(int i=1;i<=n;i++) mn[i] = dist[i];
for(int i=1;i<=m;i++){
int r1 = find(a[i].st), r2 = find(a[i].ed);
if(r1 != r2){
gup[r1][0] = gup[r2][0] = fa[r1] = fa[r2] = ++cnt;
fa[cnt] = cnt, mn[cnt] = min(mn[r1], mn[r2]), val[cnt] = a[i].rain;
}
}
init(), q = gi(), k = gi(), s = gi();
for(int i=1;i<=q;i++){
x = gi(), y = gi(), x = (x+k*lans-1) % n + 1, y = (y+k*lans) % (s+1);
cout << (lans = solve(x, y)) << endl;
}
}
return 0;
}

[洛谷P4768] [NOI2018]归程 (kruskal重构树模板讲解)的相关教程结束。

《[洛谷P4768] [NOI2018]归程 (kruskal重构树模板讲解).doc》

下载本文的Word格式文档,以方便收藏与打印。