Kruskal重构树学习

引入

简单来讲,就是在Kruskal算法进行的过程中,我们把最小生成树的边权改为点权;

可以看成是Kruskal算法的扩展使用;

假设原树有 n n n个结点,因为最小生成树是 n − 1 n-1 n1条边;

那么重构树有 2 n − 1 2n-1 2n1个结点;


举个例子,比如 x x x y y y连了一条边;
在这里插入图片描述
那么重构以后变成这样;

在这里插入图片描述


性质

  • 是一个小/大根堆(由建树时边权的排序方式决定)
  • 所有原来的点是叶子节点
  • L C A ( u , v ) LCA(u,v) LCA(u,v)的点权是 原图 u u u v v v路径上最大边权的最小值或者最小边权的最大值(由建树时边权的排序方式决定)

重构流程

在这里插入图片描述
其实就是在 K r u s k a l Kruskal Kruskal基础上加点代码;

void EX_Kru(){
    sort(input+1,input+1+m);
    for(int i=1;i<=2*n-1;++i) p[i] = i;
    for(int i=1;i<=m;++i){
        int u = input[i].p1,v = input[i].p2,val = input[i].val;
        int fu = _find(u),fv = _find(v);
        //如果不形成环
        if(fu != fv){
            //化边权为点权
            p[fu] = p[fv] = ++idx;
            pval[idx] = val;
            add(idx,fu),add(idx,fv);
        }
    }
}

例题

货车运输

题面

传送门
在这里插入图片描述
在这里插入图片描述

思路

因为想让运送的货物尽可能重

不难想到,货物重量的下限是取决于称重最少的边;

比如当前从 u u u v v v,我们需要求 u u u v v v中最短路径中权值最小的,而我们希望这条路径的边权是尽可能大的,也就是求最大边权的最小值

之所以是最短路径,是因为现在 u u u已经能到 v v v了,那么多加其他的边,只可能让最小边权变小,因此我们可以贪心的搞;

Code

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <queue>

using namespace std;

typedef long long ll;

const int N = 2e4 + 10,M = 5e4 + 10;

int p[N],n,m,q;

struct Edge{
    int next,to;
}e[M << 1];

struct Node{
    int p1,p2,val;
    bool operator<(const Node &o){
        return val > o.val;
    }
}input[M];
int head[N],tot;
int _find(int x){
    if(x == p[x]) return x;
    return p[x] = _find(p[x]);
}
int pval[N],idx;//点权、点的编号
void add(int u,int v){
    e[++tot].to = v;
    e[tot].next = head[u];
    head[u] = tot;
}
//Kru重构树
void EX_Kru(){
    sort(input+1,input+1+m);
    for(int i=1;i<=2*n-1;++i) p[i] = i;
    for(int i=1;i<=m;++i){
        int u = input[i].p1,v = input[i].p2,val = input[i].val;
        int fu = _find(u),fv = _find(v);
        //如果不形成环
        if(fu != fv){
            //化边权为点权
            p[fu] = p[fv] = ++idx;
            pval[idx] = val;
            add(idx,fu),add(idx,fv);
        }
    }
}
int dep[N],fa[N][30];
void init_LCA(int root){
    queue<int> que;
    dep[0] = 0,dep[root] = 1;
    que.push(root);
    while(!que.empty()){
        int u = que.front();
        que.pop();
        for(int i=head[u];i;i=e[i].next){
            int to = e[i].to;
            if(dep[to] > dep[u] + 1){
                dep[to] = dep[u] + 1;
                que.push(to);
                fa[to][0] = u;
                for(int k=1;k<=25;++k){
                    fa[to][k] = fa[fa[to][k-1]][k-1];
                }
            }
        }
    }
}
int LCA(int u,int v){
    if(dep[u] < dep[v]) swap(u,v);
    for(int k=25;k>=0;--k){
        if(dep[fa[u][k]] >= dep[v]){
            u = fa[u][k];
        }
    }
    if(u == v) return u;
    for(int k=25;k>=0;--k){
        if(fa[u][k] != fa[v][k]){
            u = fa[u][k];
            v = fa[v][k];
        }
    }
    return fa[u][0];
}

void solve(){
    cin >> n >> m;
    idx = n;
    for(int i=1;i<=m;++i){
        cin >> input[i].p1 >> input[i].p2 >> input[i].val;
    }
    EX_Kru();
    //因为这道题可能是一个森林 因此我们需要初始化每颗树
    memset(dep,0x3f,sizeof dep);
    for(int i=n+1;i<=idx;++i){
        //说明是根结点
        if(p[i] == i){
            init_LCA(i);
        }
    }
    cin >> q;
    while(q--){
        int u,v;
        cin >> u >> v;
        //不连通肯定不在同一个集合中
        if(_find(u) != _find(v)) cout << -1 << '\n';
        else{
            int anc = LCA(u,v);
            cout << pval[anc] << '\n';
        }
    }
}

signed main(){
    std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    solve();
    return 0;
}

归程

传送门

题面

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

思路

由题意我们知道,从 v v v 1 1 1,我们分为一半坐车另一半走路;

假设在点 u u u下车,那么答案要求 v v v u u u可以开车, u u u 1 1 1走路距离最短;

要使得 v v v u u u可以开车,那么就要求这些路径的水位线高于当天的水位线

这些路径一定在原图的最大生成树上,这就启发我们可以使用Kruskal重构树

而从任意一点到点 1 1 1走路的最短距离,我们只需要以点 1 1 1为源点,跑一个迪杰斯特拉最短路即可;


现在我们将每条边以海拔为关键字降序排序;

比如说重构树上的某个子树的根root的水位线是大于当天的水位线的;

那么这个子树内部的其他路径也必然是满足大于当天水位线的(小根堆性质);

也就是这棵子树的叶子结点是开车可达的;

那么我们枚举从这些点到点 1 1 1走路的最小值,即是答案;

由于是树形,我们只需要用树形DP来处理出dist(u),这样就不需要枚举了;

dist(u)表示以某点为根,走路到点 1 1 1的最小长度;

而如何快速的找到重构树上的某一点的深度是最浅的,且大于水位线,只需要使用树上倍增即可;

Code

#include <iostream>
#include <cstdio>
#include <queue>
#include <cstring>
#include <algorithm>
#include <utility>

using namespace std;

typedef long long ll;

typedef pair<int,int> pii;

const int N = 400000 + 10,M = 800000 + 10;

int p[N],idx;
struct Node{
    int u,v,height;
    bool operator<(const Node &o){
        return height > o.height;
    }
}ip[M];
struct Edge{
    int next,to,val;
}e[M];
int head[N],dist[N],tot;
bool vis[N];
void add(int u,int v,int w){
    e[++tot].to = v;
    e[tot].val = w;
    e[tot].next = head[u];
    head[u] = tot;
}
void add(int u,int v){
    e[++tot].to = v;
    e[tot].next = head[u];
    head[u] = tot;
}
int n,m,pval[N],fa[N][30];
int _find(int x){
    if(x == p[x]) return x;
    return p[x] = _find(p[x]);
}

void dij(int start){
    priority_queue<pii,vector<pii>,greater<pii>> pq;
    memset(dist,0x3f,sizeof dist);
    memset(vis,0,sizeof vis);
    dist[start] = 0;
    pq.push({dist[start],start});
    while(!pq.empty()){
        int u = pq.top().second;
        pq.pop();
        if(vis[u]) continue;
        vis[u] = 1;
        for(int i=head[u];i;i=e[i].next){
            int to = e[i].to;
            if(dist[to] > dist[u] + e[i].val){
                dist[to] = dist[u] + e[i].val;
                pq.push({dist[to],to});
            }
        }
    }
}
void EX_Kru(){
    idx = n;
    for(int i=1;i<=2*n-1;++i) p[i] = i;
    sort(ip+1,ip+1+m);
    int cnt = 0;
    for(int i=1;i<=m;++i){
        int u = ip[i].u, v = ip[i].v, h = ip[i].height;
        int fu = _find(u), fv = _find(v);
        if(fu != fv){
            if(++cnt == n) break;
            //化边为点
            p[fu] = p[fv] = ++idx;
            pval[idx] = h;
            add(idx,fu),add(idx,fv);  
        }
    }
}
void dfs(int u){
    //先处理深度较浅的点
    for(int k=1;k<=25;++k)
        fa[u][k] = fa[fa[u][k-1]][k-1];
    for(int i=head[u];i;i=e[i].next){
        int to = e[i].to;
        fa[to][0] = u;
        dfs(to);
        dist[u] = min(dist[u],dist[to]);
    }
}
int query(int u,int p){
    for(int k=25;k>=0;--k)
        //水位线要高于p
        if(fa[u][k] && pval[fa[u][k]] > p)
            u = fa[u][k];
    return dist[u];
}
void solve(){
    memset(head,0,sizeof head);
    memset(fa,0,sizeof fa);
    tot = 0;
    cin >> n >> m;
    for(int i=1,u,v,w,h;i<=m;++i){
        cin >> u >> v >> w >> h;
        ip[i] = {u,v,h};
        add(u,v,w),add(v,u,w);
    }
    dij(1);//处理出dist数组
    tot = 0;
    memset(head,0,sizeof head);
    EX_Kru();
    for(int i=n+1;i<=2*n-1;++i){
        if(p[i] == i){ //处理每颗树
            dfs(i);
        }
    }
    int q,k,s;
    cin >> q >> k >> s;
    int lastans = 0;
    while(q--){
        int v0,p0;
        cin >> v0 >> p0;
        v0 = (v0+k*lastans-1)%n+1;
        p0 = (p0+k*lastans)%(s+1);
        lastans = query(v0,p0);
        cout << lastans << '\n';
    }
}

signed main(){
    std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    int t;
    cin >> t;
    while(t--)
        solve();
    return 0;
}

Life is a Game

题面

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

题意

从某点出发,

思路

不难想到,因为我们要获取尽可能多的声望;

因此从当前点到另一个点,肯定要走所需声望少的边;

那么实际上,我们如果要走完所有的点,那么走的必然是最小生成树

因此我们可以考虑使用Kruskal重构树

我们按所需声望从小到大排序,搞出一个大根堆;

Code

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <queue>
#include <cstring>
#include <utility>
#include <set>

using namespace std;

typedef long long ll;

const int N = 2e5 + 10;

int n,m,q,pval[N];
struct Edge{
    int next,to;
}e[N];
int head[N],tot;
void add(int u,int v){
    e[++tot].to = v;
    e[tot].next = head[u];
    head[u] = tot;
}
struct Node{
    int u,v,w;
    bool operator<(const Node &o){
        return w < o.w; //大根堆
    }
}ip[N];
int p[N],fa[N][30],g[N][30],tr_sum[N];
int _find(int x){
    if(x == p[x]) return x;
    return p[x] = _find(p[x]);
}
void EX_Kru(){
    int idx = n;
    int cnt = 0;
    for(int i=1;i<=2*n-1;++i) p[i] = i;
    sort(ip+1,ip+1+m);
    for(int i=1;i<=m;++i){
        int u = ip[i].u,v = ip[i].v,w = ip[i].w;
        int fu = _find(u),fv = _find(v);
        if(fu != fv){
            //最小生成树,n-1条边
            if(++cnt == n) break;
            p[fu] = p[fv] = ++idx;
            pval[idx] = w;
            add(idx,fu),add(idx,fv);
        }
    }
}
//当前结点
void dfs(int u){
    for(int k=1;k<=25;++k){
        fa[u][k] = fa[fa[u][k-1]][k-1];
    }
    for(int i=head[u];i;i=e[i].next){
        //这里是单向边
        int to = e[i].to;
        fa[to][0] = u;
        dfs(to);
        tr_sum[u] += tr_sum[to];
        g[to][0] = pval[u] - tr_sum[to];
    } 
}
typedef pair<int,int> pii;
void solve(){
    cin >> n >> m >> q;
    for(int i=1;i<=n;++i) cin >> pval[i],tr_sum[i] = pval[i];
    for(int i=1;i<=m;++i){
        cin >> ip[i].u >> ip[i].v >> ip[i].w;
    }
    EX_Kru();
    for(int i=n+1;i<=2*n-1;++i){
        if(p[i] == i){
            fa[i][0] = 0;
            dfs(i);
        }
    }
    for(int k=1;k<=25;++k)
        for(int i=1;i<=2*n-1;++i)
            //防止父亲满足但是儿子不满足
            g[i][k] = max(g[i][k-1],g[fa[i][k-1]][k-1]);
    int x,power;
    while(q--){
        cin >> x >> power;
        for(int k=25;k>=0;--k){
            if(fa[x][k] == 0) continue;
            if(g[x][k] <= power){
                x = fa[x][k];
            }
        }
        cout << power + tr_sum[x] << '\n';
    }
}

signed main(){
    std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    solve();
    return 0;
}

参考

博客1
博客2
博客3

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值