最小生成树算法
Prim 算法
求最小生成树的问题与求最短路问题的思路和代码实现都非常相似, 最小生成树中,即在一个图中,存在n个节点时, 找出其中的n - 1条边,使这n个点连通,且这n - 1条边的和最小,这样一个方案就是求一个图的最小生成树。
Prim算法的思路:
每次将距离最小生成树集合距离的点加入最小生成树的集合中,然后用其去更新未加入的其他点到最小生成树集合中的距离,每一次加入一个点即完成一个局部的最优解,当以此加入了所有的节点,此时即为最小生成树.
看代码的具体实现
练习链接: Prim算法求最小生成树
// 朴素版本的Prim算法, 其算法复杂度为 O(n2), 故n一般不会太大,m一般大,故这里用邻接矩阵存储图
#include<iostream>
#include<cstring>
using namespace std;
constexpr int N = 510, INF = 0x3f3f3f3f;
int n, m, g[N][N], d[N];
bool st[N]; // 用于标记哪些结点已经加入最小生成树的集合中
auto prim() -> int
{
d[1] = 0; // 我们首先将1号结点加入集合中,并将其到集合的距离初始化为0,这里随便取一个点就行
int res = 0;
for(int i = 0; i <= n; ++i) // 遍历每一个点,每一遍遍历将其中一个点加入集合
{
int t = -1;
for(int j = 1; j <= n; ++j) // 找到集合外距离集合最近的点,这里根Dijkstra算法一样
if(!st[j] && (t == -1 || d[t] > d[j])) t = j;
st[t] = true; // 找到后将其加入最小生成树集合,故标记该结点
if(d[t] == INF) return INF;
// 若当前距离集合最近的结点其距离目前集合距离为INF, 即存在结点之间没有连接孤立的点情况,
// 这时不存在最小生成树,故直接返回INF表示不存在最小生成树
res += d[t]; // 表示最小生成树集合中加入一条边
for(int k = 1; k <= n; ++k)
d[k] = min(d[k], g[t][k]); // 用新加入集合的点,更新一下其他集合外的点到目前新集合的距离
}
return res;
}
auto main() -> int
{
memset(g, 0x3f, sizeof g);
memset(d, 0x3f, sizeof d);
cin >> n >> m;
while(m--)
{
int a, b, c; cin >> a >> b >> c;
g[a][b] = g[b][a] = min(g[a][b], c);
}
int res = prim();
if(res == INF) cout << "impossible" << endl;
else cout << res << endl;
}
我们可以看出Prim算法其实长得很像Dijkstra算法,除去特判条件,二者的不同就只有最后的for更新距离的时候的区别,Dijkstra算法更新的是结点到起点的距离,故其更新时,除了g[t][k]这一项之外还要再加上d[t]这一项, 而Prim算法最后更新的是其他结点到集合的距离,由于只是新加入了一个点,那更新的依据就只用比较其他点到这个新加入的点的距离与之前到达集合中的最近距离比较即可。故这是两者区别最大的地方,除此之外prim的朴素版本基本和朴素的Dijkstra算法一致
Prim算法的堆优化版本
通过上面分析了其与Dijkstra算法的相似性后,所以其堆优化版本的思路也是一样的,主要优化找到最短距离这个操作,故这里采用小顶堆,快速从堆顶找到目前距离集合最近的一个点,然后,取出之后只更新其相连的点即可。代码如下
#include<iostream>
#include<queue>
#include<algorithm>
#include<cstring>
using namespace std;
using T = pair<int,int>;
constexpr int N = 4e+5 + 10, INF = 0x3f3f3f3f;
int n, m, e[N], g[N], ne[N], w[N], idx, d[N]; // 采用邻接表方式存储
bool st[N];
auto add(int a, int b, int c)
{
e[idx] = b, ne[idx] = g[a], w[idx] = c, g[a] = idx++;
}
auto prim() -> int
{
d[1] = 0;
priority_queue<T, vector<T>, greater<T> > q; // 小顶堆
// 首先将其中一个点入堆,这里使用了pair,first表示的是距离集合的距离,second表示的是结点
q.emplace(0, 1);
int res = 0, cnt = 0;
while(q.size())
{
auto item = q.top(); q.pop();
int node = item.second;
if(st[node]) continue; // 说明该结点已经在集合中
st[node] = true; // 将结点加入集合
res += d[node], cnt++; // 加上边的长度 和 集合中点的个数+1.
for(int i = g[node]; i != -1; i = ne[i]) // 更新目前结点连接的下一结点
{
int t = e[i];
if(d[t] > w[i]) // 注意是与w[i]比较,而不是d[node] + w[i]
{
d[t] = w[i];
q.emplace(d[t], t);
}
}
}
if(cnt < n) return INF; // 若此时集合中的结点个数没有全部包含n个,说明最小生成树不存在
return res;
}
auto main() -> int
{
cin >> n >> m;
memset(g, -1, sizeof g);
memset(d, 0x3f, sizeof d);
while(m--)
{
int a, b, c; cin >> a >> b >> c;
add(a, b, c), add(b, a, c);
}
int res = prim();
if(res == INF) cout << "impossible" << endl;
else cout << res << endl;
return 0;
}
Kruskal算法
kruskal算法和堆优化后的prim算法都是用于稀疏图的最小生成树的计算,但常用的是Kruskal算法,其代码更加简洁,而且思路更好理解, 其算法的思路为将图中的m条边进行排序,然后从小到大依次遍历每一条边,若该边的两个端点不处于同一集合,则将它们加入到同一集合中,这里就是并查集的操作了,当遍历完所有边后若再判断此时的结点数是否为n, 若小于n, 则不存在最小生成树,或者可以用最小生成树的边判断,因为结点为n的最小生成树,其边的条数为n - 1, 若此时边的条数小于 n - 1, 也说明不存在最小生成树。 若存在,则此时即为最小生成树,因为边的长度是从小到大加入,故最先将结点加入集合完毕,此时的边的和即为最小的。
代码实现
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
constexpr int N = 2e+5 + 10, INF = 0x3f3f3f3f;
int n, m, d[N], p[N];
auto find(int x) -> int // 并查集的核心 find函数
{
if(p[x] != x) p[x] = find(p[x]);
return p[x];
}
struct E // 这里算法的实现用一个结构体来存储每一条边的信息
{
int a, b, w;
// 因为要用到排序,而自定义的数据结构使用sort排序需要重载小于号,或者也可以写一个comp函数
bool operator < (E &obj) {return this->w < obj.w;}
}e[N];
auto main() -> int
{
memset(d, 0x3f, sizeof d);
cin >> n >> m;
for(int i = 1; i <= m; ++i) // 输入m条边的信息
{
int a, b, c; cin >> a >> b >> c;
e[i] = {a, b, c};
}
sort(e + 1, e + m + 1); // 调用sort进行排序
for(int i =1; i <= n; ++i) p[i] = i; // 这里使用了并查集,初始化的套路
int res = 0, cnt = 0;
for(int i = 1; i <= m; ++i) // 从小到大遍历每一条边
{
auto item = e[i];
int a = find(item.a), b = find(item.b); // 首先判断两条边的端点是否处于同一集合
if(a != b) p[a] = b, res += item.w, cnt++;
}
if(cnt < n - 1) cout << "impossible" << endl; // 最后的判断
else cout << res << endl;
}