基本概念
-
生成树:一个连通图的生成树是含有这个连通图全部顶点的一个极小连通子图。如果连通图 G 的顶点个数为 n ,那么G的生成树的边数 必然为 n-1。无向图的生成树添加一条边,必然形成环。
-
自由树:不带简单回路的无向图。
-
简单回路:除了第一个顶点和最后一个顶点之外,其余顶点不重复的回路。
-
回路/环:第一个顶点和最后一个顶点相同的路径。
-
网络 :带权的连通图。
-
有向树:如果一个有向图,只有一个顶点的入度为0,其余顶点的入度都是1,就被称为有向树。
-
最小生成树:有向图G的生成树有很多,其中有个生成树的所有边的权值总和是最小的,那就是最小生成树。
-
Kruskal 算法,图 G = <V, E>, V 中 n 个顶点视为 n 个连通分量。循环:从 E 的 m 条边中不断挑选 边 出来转移到 A 中。挑选原则:每次从 E 中剩余的边里挑选权值最小的边,如果这条边的两个顶点原本是不连通的(不考虑当前挑选的边),那么就放进A,否则无视它丢掉它,另外再选权值最小的边。直到 A 中的边涉及的顶点等于总顶点数量。
-
Prim 算法:以图 G 中任意一个点 a 为起点,将 a 视为一个连通分量,a 连接了图 G 中的1个或多个其他顶点,从这些连接 a(连通分量中任意点) 和其他点的边中,选一个权值最小的边加入到 a 这个连通分量。不断循环,a 连通分量中的顶点就会不断增多,直到等于G的顶点总数,此时 a 就是最小生成树。
-
如果以上算法没有生成最小生成树,也就是说 最后得到的连通分量的顶点数量小于图G的顶点总数,那么就说明图 G 是非连通图。
leetcode题目
声明:以下内容参考文章链接:https://michael.blog.csdn.net/article/details/107796632
题目简述
想象一下你是个城市基建规划者,地图上有 N 座城市,它们按以 1 到 N 的次序编号。
给你一些可连接的选项 conections,其中每个选项 conections[i] = [city1, city2, cost] 表示将城市 city1 和城市 city2 连接所要的成本。(连接是双向的,也就是说城市 city1 和城市 city2 相连也同样意味着城市 city2 和城市 city1 相连)。
返回使得每对城市间都存在将它们连接在一起的连通路径(可能长度为 1 的)最小成本。
该最小成本应该是所用全部连接代价的综合。如果根据已知条件无法完成该项任务,则请你返回 -1。
示例 1:
输入:N = 3, conections = [[1,2,5],[1,3,6],[2,3,1]]
输出:6
解释:
选出任意 2 条边都可以连接所有城市,我们从中选取成本最小的 2 条。
Kruskal 算法
- Kruskal 算法,图 G = <V, E>, V 中 n 个顶点视为 n 个连通分量。循环:从 E 的 m 条边中不断挑选 边 出来转移到 A 中。挑选原则:每次从 E 中剩余的边里挑选权值最小的边,如果这条边的两个顶点原本是不连通的(不考虑当前挑选的边),那么就放进A,否则无视它丢掉它,另外再选权值最小的边。直到 A 中的边涉及的顶点等于总顶点数量。
- 解法:(1)将边的权值排序,小的先遍历,用并查集检查两个顶点是否合并了,没有合并则将该边加入生成树。(2)也可以使用优先队列实现(相当于排序)
class dsu // 并查集,主要作用是检查两个顶点是不是在同一个连通分量
{
vector<int> f;
public:
dsu(int n)
{
f.resize(n);
for(int i = 0; i < n; ++i)
f[i] = i;
}
void merge(int a, int b)
{
int fa = find(a);
int fb = find(b);
f[fa] = fb;
}
int find(int a)
{
int origin = a;
while(a != f[a])
{
a = f[a];
}
return f[origin] = f[a];
}
};
class Solution {
public:
int minimumCost(int N, vector<vector<int>>& connections) {
dsu u(N+1);
sort(connections.begin(), connections.end(),[&](auto a, auto b){
return a[2] < b[2]; //距离短的边优先
});
int edge = 0, p1, p2, dis, total = 0;
for(int i = 0; i < connections.size(); ++i) {
p1 = connections[i][0];
p2 = connections[i][1];
dis = connections[i][2];
if(u.find(p1) != u.find(p2)) { // 对于两个顶点不在同一连通分量的边,才予以考虑。(上文有说)
u.merge(p1,p2);
edge++;
total += dis;
}
if(edge == N-1) // N个节点的图,其生成树的边数量必然是 N-1,所以当边数等于N-1,就可以中止循环了。
break;
}
// 如果边数量不是N-1,那么不存在最小生成树,返回-1,也就是说这是一个非连通图。
return edge==N-1 ? total : -1;
}
};
Prim 算法
- Prim 算法:以图 G 中任意一个点 a 为起点,将 a 视为一个连通分量,a 连接了图 G 中的1个或多个其他顶点,从这些连接 a(连通分量中任意点) 和其他点的边中,选一个权值最小的边加入到 a 这个连通分量。不断循环,a 连通分量中的顶点就会不断增多,直到等于G的顶点总数,此时 a 就是最小生成树。
- 如果以上算法没有生成最小生成树,也就是说 最后得到的连通分量的顶点数量小于图G的顶点总数,那么就说明图 G 是非连通图。
- (1). 把一个初始顶点的所有边加入优先队列;
(2). 取出最短的边,把这条边的另一个顶点相关的边加入队列;
(3). 再取出最小的边,重复下去,直到所有顶点加入过了。
struct cmp {
// pair 类型数据之间可以使用 <, >做比较,但优先比较 first, 如果 first 相同才会比较 second.
// 以下是对括号 () 的操作符重载,用结构体模仿函数的行为,这是仿函数的标准做法。
bool operator()(const pair<int,int>& a, const pair<int,int>& b) const {
return a.second > b.second; //小顶堆, 距离小的优先
}
};
class Solution {
public:
int minimumCost(int N, vector<vector<int>>& connections) {
vector<bool> vis(N+1, false);
vector<vector<pair<int,int>>> edges(N+1,vector<pair<int,int>>());
for(auto& c : connections) {
// edges[i] 是个vector,里面存了i点直接相连的点及其对应的边的权值。
edges[c[0]].push_back({c[1],c[2]});
edges[c[1]].push_back({c[0],c[2]});
}
priority_queue<pair<int,int>, vector<pair<int,int>>, cmp> q;
int to, distance, total = 0, edge = 0;
vis[1] = true; // 第1点是起始点。
for(auto& e : edges[1])
q.push(e); // 把第 1 点的所有边加入优先队列;
while(!q.empty()) {
to = q.top().first; // to 是 连通分量a中的所有点的相关边里,权值最小的边的另一个顶点
distance = q.top().second; // 权值最小的边的权值
q.pop(); // 去掉刚刚处理的边
if(!vis[to]) { // 如果刚刚处理的点(边)以前访问过,那么说明已经在这个连通分量 a 里面了,不需要再考虑
vis[to] = true;
total += distance;
edge++;
if(edge == N-1) // 放入连通分量 a 的 边的数量等于N-1,最小生成树就形成了。
return total;
// 把刚刚处理的边的另一顶点(to)也加入了 连通分量a, 那么也要把 to 的相关 边 也加入连通分量的相连边这个队列中。
for(auto& e : edges[to])
q.push(e);
}
}
return -1; // 如果连通分量 a 的 边的数量无法等于N-1,说明是非连通图,没有生成树。
}
};