使用并查集解决遍历所有点求最短路径问题——路径压缩

- 并查集解决最短路径

个人认为,最短路径问题大致可分为以下两种

  1. 畅通工程
    Problem Description
    省政府“畅通工程”的目标是使全省任何两个村庄间都可以实现公路交通(但不一定有直接的公路相连,只要能间接通过公路可达即可)。经过调查评估,得到的统计表中列出了有可能建设公路的若干条道路的成本。现请你编写程序,计算出全省畅通需要的最低成本。

    Input
    测试输入包含第1行给出评估的道路条数 N、村庄数目M ( < 100 );随后的 N
    行对应村庄间道路的成本,每行给出一对正整数,分别是两个村庄的编号,以及此两村庄间道路的成本(也是正整数)。为简单起见,村庄从1到M编号。

    Output
    对每个测试用例,在1行里输出全省畅通需要的最低成本。若统计数据不足以保证畅通,则输出“-1”。

    看到畅通工程,我们容易想到最小生成树(一个有 n 个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有 n 个结点,并且有保持图连通的最少的边。)问题,既保证任意两个村庄之间有道路连通就行,权最小,既成本最低。

    问: 既然要使最终成本最低,根据贪心算法,那我们每次都选取成本最低的一条路,并将已选取的路径合并,最终连通所有村庄,那成本是不是最低呢?这里,我们引入并查集
    并查集
    并查集,在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。
    我们先来看以下几个集合
    集合1{A,B,C},集合2{E,F},集合3{A,E}
    我们使用树结构来表示上述集合
    在这里插入图片描述
    那么要判断两个集合任两个字母a,b是否在一个集合中,只要判断他们是否在一个树中,既所在树的根节点一致(root(a)==root(b)),那么我们使用一个数组arr来表示每个字母的父节点,根节点的父亲为-1,则集合2可表示为

E F
-1 E

问:那如何合并两个集合呢?
我们可以将其中一个集合变成另一个集合的子树,既将其根节点指向另一树的根节点,那么合并之后的树为
在这里插入图片描述
查找算法优化——路径优化
如上,集合直接合并也有一个大的隐患。根据前文所述,对两个集合合并主要的操作就是查找节点的根节点,我们的查找策略是不断的去查找节点的父亲节点,直至父节点为-1,那么这个过程所消耗的时间就与树的高度息息相关。当节点数量大到一点数量的时候,查找将是一个及其耗时的操作,所以,我们必须在将两颗树合并的时候进行某些操作——在查找过程中,将当前节点与根节点之间的所有节点直接指向根节点,这个过程称为路径压缩,经过路径压缩之后,树结构变为
在这里插入图片描述
经过路径压缩之后,树结构得到优化,树高保持为1而且不影响查找操作。到这里,相信读者对并查集已经有了大致的了解,但如何编写相应的代码呢?
代码实现
首先,一个数组来描述所有节点的父节点(节点总数为N)

int parent[N]

其次,查找任意节点根节点(不断查找节点的父节点),我们通过递归来实现

//n——需要查找的节点
int findRoot(int n){
    if(parent[n]==-1){    //父节点为-1,此节点为根节点,返回
        return n;
    }
    findRoot(parent[n]);
}

经过路径压缩

int findRoot(int n){
    if(parent[n]==-1){
        return n;
    }
    int par=findRoot(parent[n]);
    parent[n]=par;    //路径压缩,将当前节点的父亲指向返回的根节点
    return par;
}

好了,到这里,我们可以试着用以上所学知识来解决畅通工程问题了。

#include<iostream>
#include<algorithm>
using namespace std;

/*
畅通工程
*/

struct load{  //修路结构体
    int a; //a,b路两端的两个村庄
    int b;
    int price; //价格
    bool operator<(const load&A) const{  //重载<运算符,根据price从小到大排序
        return price<A.price;
    }
}loads[5000];  //村庄数<100,估算路最多为4950

int parent[100];  //记录节点父亲的数组

int findRoot(int n){
    if(parent[n]==-1){
        return n;
    }else{
        int par=findRoot(parent[n]);
        parent[n]=par;
        return par;
    }
}

int main(){
    int n,m;  //n-道路条数,m-村庄个数
    cin>>n>>m;
    for(int i=0;i<m;i++){ //开始时各个节点独立,所以初始化节点父亲为-1
        parent[i]=-1;
    }
    for(int i=0;i<n;i++){  //输入道路信息
        cin>>loads[i].a;
        cin>>loads[i].b;
        cin>>loads[i].price;
    }
    sort(loads,loads+n); //排序
    int prices=0;   //统计价格
    for(int i=0;i<n;i++){
        int x=findRoot(loads[i].a);
        int y=findRoot(loads[i].b);
        if(x!=y){  //不属于同一集合,合并
            parent[y]=x;
            prices+=loads[i].price;
        }
    }
    cout<<prices<<endl;
}
  1. 旅游类
    问:那旅游类最短消费问题和畅通工程又有什么不同呢?
    结合实际,我们可以理解:畅通工程,只要满足任意两个节点之间连通即可,但就旅游来说,我们在游历完所有地点的前提下,一个地点我们只能游历一遍,不可能再经过一次。比如我们给出路径图A-B: 2,B-C:4,B-D:4,C-D:7
    如果是畅通工程,我们构造的树结构为
    在这里插入图片描述
    既我们构造的树结构除根节点与叶子节点,每个节点只能有一个父亲节点和一个儿子节点

猜你喜欢

转载自blog.csdn.net/weixin_43353645/article/details/89437010