Kruskal 重构树

算法

直接bb好像不是很好讲,那就从这道题入手吧。

重构树+树上倍增+dfs序+主席树

题目描述

在 Bytemountains 有n座山峰,每座山峰有他的高度 hi。有些山峰之间有双向道路相连,共 m 条路径,每条路径有一个困难值,这个值越大表示越难走。

现在有 q 组询问,每组询问询问从点 v 开始只经过困难值小于等于 x 的路径所能到达的山峰中第 k 高的山峰,如果无解输出 -1。

输入格式

第一行三个数 n,m,q。 第二行 n 个数,第 i 个数为 hi

接下来 m 行,每行三个整数 a,b,c表示从 a→b 有一条困难值为 c 的双向路径。 接下来 q 行,每行三个数 v,x,k表示一组询问。

输出格式

对于每组询问,输出一个整数表示能到达的山峰中第 k 高的山峰的高度。

输入

10 11 4
1 2 3 4 5 6 7 8 9 10
1 4 4
2 5 3
9 8 2
7 8 10
7 1 4
6 7 1
6 4 8
2 1 5
10 8 10
3 4 7
3 4 6
1 5 2
1 5 6
1 5 8
8 9 2

输出

6
1
-1
8

说明/提示

数据规模与约定
对于 100% 的数据,n≤10^5 0≤m,q≤5×10,
h_i, c,x≤10^9

思路

现在有Q组询问,每组询问询问从点v开始只经过困难值小于等于x的路径所能到达的山峰中第k高的山峰,如果无解输出−1
首先,这是一张图(你在说大实话么)

对于一个点来说,经过困难值小于等于x的路径所能到达的点是一定的。

但是这和生成树有啥关系呢?

显然,若一个点能通过一条路径到达,那么我们走最小生成树上的边也一定能到达该节点。

这样我们把最小生成树建出来,就可以少考虑很多边了。

然而并没有什么卵用.

现在我们需要做的,是找一种方法,能够维护出一个点能到达的点。

于是Kruskal重构树就诞生了。

它的思想是这样的:

在运行Kruskal算法的过程中,对于两个可以合并的节点(x,y),断开其中的连边,并新建一个节点T,把T向(x,y)连边作为他们的父亲,同时把(x,y)之间的边权当做T的点权

比如说

重构之后是这样的:

然后这个点就代表这个点集。

这个题目的样例重构得到的树:

这样我们得到了一个新的树,考虑它有什么性质。
其中最重要的一条就是:一个节点能走到的节点一定在它的子树中
那么我们找到v的点权<=x最远祖先lca。因为祖先节点的点权单增,这个可以倍增搞,那么lca的子树都是v可以得到的节点。
那么就是一个子树的第k大。dfs序建立主席树。主席树只在叶子节点更新。

然后这道题就做完了。O((n+Q)*logn)

当然,除了这一条之外,Kruskal重构树还有很多有意思的性质

1.是一个二叉树
2.如果是按最小生成树建立的话是一个大根堆(important!)
3.任意两个点路径上边权的最大值为它们的LCA的点权
4.一个子树里的节点之间的路径最长边<=根点权
5.树上除叶子结点以外的点都对应着原来生成树中的边,叶子结点就是原来生成树上的节点。
5.由于新点的创建顺序与原来生成树上边权的大小有关,可以发现,从每个点到根节点上除叶子结点外按顺序访问到的点的点权是单调的。
6.出于kruskal算法贪心的性质,两个点u和v的lca的点权就对应着它们最小生成树上的瓶颈。

#include<bits/stdc++.h>
#define LL long long
using namespace std;
const int maxn=1000005;
const int maxm=1000005;
#define mid (l+r>>1)
const int N=2e5+7;
const LL INF=1e18;

struct SegTree {
    LL sum[N*40], tot=0;
    int L[N*40], R[N*40];
    void init () {
        for(int i=0; i<=tot; i++){
            L[i]=R[i]=sum[i]=0;
        }
        tot=0;
    }
    int BT(int l, int r){
        int rt=++tot;
        sum[rt]=0;
        if(l<r){
            L[rt]=BT(l, mid);
            R[rt]=BT(mid+1, r);
        }
        return rt;
    }
    int add(int root, int l ,int r, int x, int val){//a[x]+=val
        int rt=++tot;
        L[rt]=L[root], R[rt]=R[root], sum[rt]=sum[root]+val;
        if(l<r){
            if(x<=mid) L[rt]=add(L[root], l, mid, x, val);
            else R[rt]=add(R[root], mid+1, r, x, val);
        }
        return rt;
    }
    int query(int x, int y, int l, int r, int k){//区间[x, y]的第k小
        if(l>=r) return l;//得到答案
        int s=sum[R[y]]-sum[R[x]];
        if(s>=k) return query(R[x], R[y], mid+1, r, k);
        else return query(L[x], L[y], l, mid, k-s);
    }

}Tree;

struct Edge{
    int from, to, nxt;
}E[maxm];
int head[maxn], cut=0;
int w[maxn];//重构树每个点的点权
int fa[maxn][21];//重构树每个节点父亲
int pos[maxn][2];//树的dfs序
int root[maxn];//根节点
void Addedge(int x, int y){
    E[++cut]={x, y, head[x]};
    head[x]=cut;
}

int a[maxn];
struct kruskal_Tree{
    Edge e[maxm];
    int cut=0, T=0;

    int f[maxn];
    int fd(int x){
        if(!f[x]) return x;
        return f[x]=fd(f[x]);
    }
    void add(int x, int y, int w){
        e[++cut]={x, y, w};
    }

    void DFS(int u){
        for(int i=1; i<=20; i++){
            fa[u][i]=fa[fa[u][i-1]][i-1];
        }
        pos[u][0]=T;
        //叶子节点
        if(!head[u]){
            pos[u][0]=++T;
            root[T]=Tree.add(root[T-1], 1, N, a[u], 1);
            return ;
        }
        //非叶子节点
        for(int i=head[u]; i; i=E[i].nxt){
            DFS(E[i].to);
        }
        pos[u][1]=T;
    }

    void get_Tree(int n){
        sort(e+1, e+cut+1, [](Edge &a, Edge &b){return a.nxt<b.nxt;});
        for(int i=1; i<=cut; i++){
            int x=fd(e[i].from), y=fd(e[i].to);
            if(x==y) continue;
            w[++n]=e[i].nxt;
            f[x]=f[y]=n;
            fa[x][0]=fa[y][0]=n;
            Addedge(n, x); Addedge(n, y);//建立重构树
        }
        //dfs顺序建立主席树
        root[0]=Tree.BT(1, N);
        //ps:这个图一定连通,不然要多次DFS
        DFS(n);
    }
}kt;

struct LSH{//离散化
    int b[N];
    int lsh(int a[], int n){//得到离散化后不同元素的个数
        for(int i=1; i<=n; i++) b[i]=a[i];
        sort(b+1, b+n+1);
        int cnt=unique(b+1, b+n+1)-b-1;
        for(int i=1; i<=n; i++){
            int x=a[i];
            a[i]=lower_bound(b+1, b+cnt+1, a[i])-b;
        }
        return cnt;
    }
    int id(int x){//得到原数
        return b[x];
    }
}Lsh;


int main() {

    int n, m, q; scanf("%d%d%d", &n, &m, &q);
    for(int i=1; i<=n; i++) scanf("%d", &a[i]);
    Lsh.lsh(a, n);
    for(int i=1; i<=m; i++){
        int x, y, w; scanf("%d%d%d", &x, &y, &w);
        kt.add(x, y, w);
    }
    kt.get_Tree(n);
    while(q--){
        int x, d, k; scanf("%d%d%d", &x, &d, &k);
        for(int i=20; i>=0; i--){
            if(fa[x][i]&&w[fa[x][i]]<=d) x=fa[x][i];
        }
        if(pos[x][1]-pos[x][0]<k){//如果x叶子节点小于k,那么没有第k大 
            printf("-1\n");
            continue;
        }
        printf("%d\n", Lsh.id(Tree.query(root[pos[x][0]], root[pos[x][1]], 1, N, k)));
    }

    return 0;
}

重构树+树上倍增+最短路

题目描述

本题的故事发生在魔力之都,在这里我们将为你介绍一些必要的设定。 魔力之都可以抽象成一个 n 个节点、m 条边的无向连通图(节点的编号从 1 至 n)。我们依次用 l,a 描述一条边的长度、海拔。

作为季风气候的代表城市,魔力之都时常有雨水相伴,因此道路积水总是不可避免的。由于整个城市的排水系统连通,因此有积水的边一定是海拔相对最低的一些边。我们用水位线来描述降雨的程度,它的意义是:所有海拔不超过水位线的边都是有积水的。

Yazid 是一名来自魔力之都的 OIer,刚参加完 ION2018 的他将踏上归程,回到他温暖的家。Yazid 的家恰好在魔力之都的 1 号节点。对于接下来 Q 天,每一天 Yazid 都会告诉你他的出发点 v ,以及当天的水位线 p。

每一天,Yazid 在出发点都拥有一辆车。这辆车由于一些故障不能经过有积水的边。Yazid 可以在任意节点下车,这样接下来他就可以步行经过有积水的边。但车会被留在他下车的节点并不会再被使用。 需要特殊说明的是,第二天车会被重置,这意味着:

车会在新的出发点被准备好。
Yazid 不能利用之前在某处停放的车。
Yazid 非常讨厌在雨天步行,因此他希望在完成回家这一目标的同时,最小化他步行经过的边的总长度。请你帮助 Yazid 进行计算。

本题的部分测试点将强制在线,具体细节请见【输入格式】和【子任务】。

输入格式
单个测试点中包含多组数据。输入的第一行为一个非负整数T,表示数据的组数。

接下来依次描述每组数据,对于每组数据:

第一行 2 个非负整数 n,m分别表示节点数、边数。

接下来 m 行,每行 4 个正整数u, v, l, a描述一条连接节点 u, v 的、长度为 l、海拔为 a 的边。 在这里,我们保证1≤u,v≤n。

接下来一行 3 个非负数 Q, K, S 其中 Q 表示总天数,K∈0,1 是一个会在下面被用到的系数,S 表示的是可能的最高水位线。

接下来 Q 行依次描述每天的状况。每行 2 个整数 v_0; p_0

描述一天:
这一天的出发节点为v=(v0+K×lastans1)modn+1
这一天的水位线为p=(p0+K×lastans)mod(S+1)

其中 lastans 表示上一天的答案(最小步行总路程)。特别地,我们规定第 1 天时 lastans = 0。 在这里,我们保证1v0n,0p0S

对于输入中的每一行,如果该行包含多个数,则用单个空格将它们隔开。

输出格式
依次输出各组数据的答案。对于每组数据:

输出 Q 行每行一个整数,依次表示每天的最小步行总路程。

输入

1
4 3
1 2 50 1
2 3 100 2
3 4 50 1
5 0 2
3 0
2 1
4 1
3 1
3 2

输出

0
50
200
50
150

输入

1
5 5
1 2 1 2
2 3 1 2
4 3 1 2
5 3 1 2
1 5 2 1
4 1 3
5 1
5 2
2 0
4 0

输出

0
2
3
1
说明/提示
n≤200000, m≤400000,q<=400000

思路

我们同样建立最大生成树重构树。我们找到v的权值>p最远祖先。那么v到它的所有子树节点是不用步行。如果我们知道这些子节点到1的最短路径就可以了。先求求个最短路。然后我们不能去一个一个的遍历子节点吧,我们应该dp一下:mi[i]:i节点的子树到1的最短距离,这题就写完了。

#include <bits/stdc++.h>
#define LL long long
using namespace std;

const int maxn = 5e5+10;
const int maxm = 2e6+10;

struct Edge {
    int from, to, nxt;
} E[maxm];
int head[maxn], cut=0;
int w[maxn];//重构树每个点的点权
int fa[maxn][21];//重构树每个节点父亲
int mi[maxn];//每个点的子树距离节点1的最短路
void Addedge(int x, int y) {
    E[++cut]= {x, y, head[x]};
    head[x]=cut;
}

struct kruskal_Tree {
    Edge e[maxm];
    int cut=0, T=0;
    int f[maxn];
    void init(){
        cut=T=0;
        memset(f, 0, sizeof(f));
    }

    int fd(int x) {
        if(!f[x])
            return x;
        return f[x]=fd(f[x]);
    }
    void add(int x, int y, int w) {
        e[++cut]= {x, y, w};
    }

    void DFS(int u) {
        for(int i=1; i<=20; i++) {
            fa[u][i]=fa[fa[u][i-1]][i-1];
        }
        for(int i=head[u]; i; i=E[i].nxt) {
            DFS(E[i].to);
            mi[u]=min(mi[u], mi[E[i].to]);
        }
    }

    void get_Tree(int n) {
        sort(e+1, e+cut+1, [](Edge &a, Edge &b) {
            return a.nxt>b.nxt;
        });
        for(int i=1; i<=cut; i++) {
            int x=fd(e[i].from), y=fd(e[i].to);
            if(x==y)
                continue;
            w[++n]=e[i].nxt;
            f[x]=f[y]=n;
            fa[x][0]=fa[y][0]=n;
            Addedge(n, x);
            Addedge(n, y);//建立重构树
        }
        //保证连通
        DFS(n);
    }
} kt;

struct Dij {
    struct Edge {
        int to, w, nxt;
    } E[maxm];
    int head[maxn], cut=0;
    int vis[maxn], d[maxn];
    priority_queue<pair<int, int> > q;
    void init(){
        memset(vis, 0, sizeof(vis));
        memset(head, 0, sizeof(head));
        cut=0;
        memset(d, 0x3f, sizeof(d));
    }
    void Addedge(int x, int y, int w) {
        E[++cut]= {y, w, head[x]};
        head[x]=cut;
    }
    void get(int s, int n){
        q.push({0, s}); d[s]=0;
        while(!q.empty()){
            int now=q.top().second; q.pop();
            if(vis[now]) continue;
            vis[now]=1;
            for(int i=head[now]; i; i=E[i].nxt){
                int to=E[i].to;
                if(!vis[to]&&d[to]>d[now]+E[i].w){
                    d[to]=d[now]+E[i].w;

                    q.push({-d[to], to});
                }
            }
        }
        //初始化
        for(int i=1; i<=n; i++){
            mi[i]=d[i];
        }
    }
}zdl;

void init() {
    zdl.init();
    kt.init();
    memset(mi, 0x3f, sizeof(mi));
    memset(head, 0, sizeof(head));
    memset(w, 0, sizeof(w));
    memset(fa, 0, sizeof(fa));
    cut=0;
}

int main() {

    int t;
    scanf("%d", &t);
    while(t--) {
        init();
        int n, m;
        scanf("%d%d", &n, &m);
        for(int i=1; i<=m; i++) {
            int u, v, l, a;
            scanf("%d%d%d%d", &u, &v, &l, &a);
            kt.add(u, v, a);
            zdl.Addedge(u, v, l); zdl.Addedge(v, u, l);
        }
        zdl.get(1, n);
        kt.get_Tree(n);
        int Q, K, S; scanf("%d%d%d", &Q, &K, &S);
        int last=0;
        while(Q--){
            int v, p; scanf("%d%d", &v, &p);
            v=(v+K*last-1)%n+1;
            p=(p+K*last)%(S+1);
            for(int i=20; i>=0; i--){
                if(fa[v][i]&&w[fa[v][i]]>p) v=fa[v][i];
            }
            printf("%d\n", last=mi[v]);
        }
    }

    return 0;

}

posted @   liweihang  阅读(185)  评论(0编辑  收藏  举报
编辑推荐:
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
Live2D
欢迎阅读『Kruskal 重构树』
点击右上角即可分享
微信分享提示