1. 问题
在一给定的无向图 G < V , E > G<V, E> G<V,E> 中, E < u , v > E<u, v> E<u,v> 代表连接顶点 u u u与顶点 v v v 的边,而 W < u , v > W<u, v> W<u,v> 代表此边的权重。若存在 T T T 为 E E E 的子集且为无环图,使得的 W ( T ) W(T) W(T) 最小,即需要找到一颗最小权重生成树(简称最小生成树)。
2. 解析
P r i m Prim Prim 算法:开始随机选择一个起点,将其收录进集合 S e t Set Set。 S t e p Step Step:遍历 S e t Set Set 中的点 u u u,找到一条最短的边权 W < u , v > W<u,v> W<u,v>,并且满足这条边 E < u , v > E<u,v> E<u,v> 中的 v v v 不属于 S e t Set Set 中,然后将 v v v 收录进 S e t Set Set 中。重复 S t e p Step Step 操作直至收录完所有的点。
K r u s k a l Kruskal Kruskal 算法: S t e p Step Step:从边集 E < u , v > E<u,v> E<u,v> 选择一条未被选择过的且边权最小的 W < u , v > W<u,v> W<u,v>,并且点 u u u 和点 v v v 不包含于同一个集合中,然后将点 u u u 和点 v v v 所处的两个集合合并(并查集)。重复Step操作直至找到符合条件的n-1条边。
3. 设计
Prim算法:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 5e3+10;
struct node {
int u,v,w;
};
int n,m,k;
int mp[N][N];
int dis[N];
pair<int,int>fa[N];
int sum;
vector<node>res;
/**
* 建图
*/
void add(int u,int v,int w) {
mp[u][v] = mp[v][u] = min(mp[u][v],w);
}
/**
* 松弛操作是为了在稠密图的情况下降低时间复杂度
* 如果为了找出边权最小的边而直接遍历
* 当前集合中的点连接所有的边,总时间复杂度会是O(n*(n+m))
* 所以我们要在寻找最小边权的同时处理dis数组
* 总时间复杂度就会变成O(n*(n+n));
* dis[i]数组代表当前集合中的某点到i点的最短距离
*/
void slack(int u) {
for (int v=1;v<=n;v++) {
int w = mp[u][v];
if (dis[v] == -1) continue;
if (w < dis[v]) {
dis[v] = w;
fa[v] = {u,w};
}
}
}
/**
* 遍历集合中的点
* 找出集合中点的边权最小的边连接的那个点加入集合
* 如果这条边连接的点已经是集合中的点则跳过
*/
void Prim() {
dis[1] = 0;
for (int i=1;i<=n;i++) {
int idx,mi = 1e9;
for (int j=1;j<=n;j++) {
if (dis[j] == -1) continue;
if (dis[j] < mi) {
idx = j;
mi = dis[j];
}
}
if (mi==1e9) {
puts("MST is not existed");
return;
}
sum += mi;
dis[idx] = -1;
slack(idx);
}
printf("MST is existed and total weight = %d\n",sum);
puts("follwing the edges:");
for (int i=1;i<=n;i++) {
if (fa[i].first==i) continue;
printf("V%d->V%d Weight = %d\n",fa[i].first,i,fa[i].second);
}
}
void run() {
scanf("%d %d",&n,&m);
int u,v,w;
/**
* 预处理数据
*/
for (int i=1;i<=n;i++) {
dis[i] = 1e9;
fa[i].first = i;
for (int j=i;j<=n;j++) {
mp[i][j] = mp[j][i] = 1e9;
}
}
for (int i=1;i<=m;i++) {
scanf("%d %d %d",&u,&v,&w);
add(u,v,w);
}
Prim();
}
int main() {
run();
return 0;
}
Kruskal算法:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 2e5+10;
/**
* 存无向图的边 + 按边权升序排序
*/
struct node {
int u,v,w;
bool friend operator<(const node &x, const node &y) {
return x.w < y.w;
}
}e[N];
int n,m,k;
int fa[N];
vector<node>res;
/**
* 并查集
* 用于判断两个点是否存在一个集合中
*/
int get(int x) {
return fa[x] == x ? x : fa[x] = get(fa[x]);
}
void Kruskal() {
int sum = 0;
int tot = 0;
for (int i=1;i<=m;i++) {
int u = e[i].u;
int v = e[i].v;
int w = e[i].w;
/**
* 判断两个点是否已经处于一个集合中
* 如果相同,则跳过当前这条边,防止出现环
*/
int x = get(u);
int y = get(v);
if (x == y) continue;
/**
* 如果两个点不在一个集合中
* 则将它们合并成一个集合,并选取当前这条边
*/
fa[x] = y;
tot += w;
sum++;
res.push_back({u,v,w});
if (sum == n-1) break;
}
if (sum != n-1) {
puts("MST is not existed");
return;
}
printf("MST is existed and total weight = %d\n",tot);
puts("follwing the edges:");
for (auto g:res) {
printf("V%d->V%d Weight = %d\n",g.u,g.v,g.w);
}
}
void run() {
scanf("%d %d",&n,&m);
for (int i=1;i<=n;i++) {
fa[i] = i;
}
for (int i=1;i<=m;i++) {
scanf("%d %d %d",&e[i].u,&e[i].v,&e[i].w);
}
sort(e+1,e+m+1);
Kruskal();
}
int main() {
run();
return 0;
}
4. 分析
Kruskal算法首先需要将m条边 E < u , v , w > E<u,v,w> E<u,v,w>根据w进行升序排序,然后遍历这m条边,每次遍历需要判断 u u u , v v v 是否处于一个集合中,并查集的查询复杂度为 O ( l o g N ) O(logN) O(logN) 。故总时间复杂度为 O ( E l o g E ) O(ElogE) O(ElogE),适用于稀疏图。
Prim算法需要进行n次收录操作,每次收录操作过程中还需要遍历所有的点集 V V V来找到当前点集 S e t Set Set最小的边权,即遍历整个 d i s [ i ] dis[i] dis[i] 数组找到最小值。故总时间复杂度为 O ( V 2 ) O(V^2) O(V2),适用于稠密图。
5. 源码
https://github.com/a894985555/Algorithm/tree/main/%E6%9C%80%E5%B0%8F%E7%94%9F%E6%88%90%E6%A0%91