Prim算法
基本思想是对图G[V, E]设置集合S来存放已被访问的顶点,然后执行n次以下两个步骤
- 每次从集合V-S(即未访问的点)中选择和集合S最近的一个顶点u,访问u并将其加入集合S,同时把这条离集合S最近的边加入最小生成树中
- 令顶点u作为集合S和集合V-S连接的接口,优化从u能到达的未访问顶点v与集合S的最短距离
实现该算法需要两个概念:
-
集合S的实现
实现方法和Dijkstra中相同,用一个bool型数组(或map)visit[]来表示顶点是否已经被访问 -
存储顶点Vi(0 <= i <= n - 1)与集合S的最短距离
用数组d[]来存放顶点Vi与集合S的最短距离,初始时只有起点s的d[s] = 0,其余顶点都赋一个INT_MAX表示还不可到达
这里可以发现prim算法和Dijkstra算法使用的思想几乎完全相同,只不过在Dijkstra中数组d[]的含义是起点s到达顶点Vi的最短距离,而Prim算法中d[]表示顶点Vi与集合S的最短距离
邻接矩阵版
int n, G[MAXV][MAXV];
int d[MAXV];
map<int, bool> visit;
int prim()
{
fill(d, d + MAXV, INT_MAX);
d[0] = 0;
int ans = 0; //用于存放最小生成树的边权之和
for (int i = 0; i < n; i++)
{
int u = -1, MIN = INT_MAX;
for (int j = 0; j < n; j++)
{
if (visit[j] == false && d[j] < MIN)
{
u = j;
MIN = d[j];
}
}
if (u == -1)
return -1;
visit[u] = true;
ans += d[u]; //将距离最小的边加入最小生成树
for (int v = 0; v < n; v++)
{
//v没被访问 && u能到达v && 以u为中介点使得v离集合S更近
if (visit[v] == false && G[u][v] != INT_MAX && G[u][v] < d[v])
d[v] = G[u][v];
}
}
return ans;
}
邻接表版
struct node
{
int v; //边的目标顶点
int dis; //边权
};
vector<node> Adj[MAXV];
int n;
int d[MAXV];
map<int, bool> visit;
int prim()
{
fill(d, d + MAXV, INT_MAX);
d[0] = 0;
int ans = 0; //用于存放最小生成树的边权之和
for (int i = 0; i < n; i++)
{
int u = -1, MIN = INT_MAX;
for (int j = 0; j < n; j++)
{
if (visit[j] == false && d[j] < MIN)
{
u = j;
MIN = d[j];
}
}
if (u == -1)
return -1;
visit[u] = true;
ans += d[u]; //将距离最小的边加入最小生成树
for (int j = 0; j < Adj[u].size(); j++)
{
int v = Adj[u][j].v;
if (visit[v] == false && Adj[u][j].dis < d[v])
d[v] = Adj[u][j].dis;
}
}
return ans;
}
Kruskal算法
基本思想是在初始状态时隐去所有边,这样图中每个顶点自成一个连通块,然后执行下面的步骤:
- 对所有边按边权从小到大排序
- 按边权从小到大测试所有边,如果当前测试边所链接的两个顶点不在同一个连通块中(即不形成圈),则把这条测试边加入当前最小生成树中,否则舍弃这条边
- 执行步骤2,直到最小生成树中的边数等于总顶点数减1或是测试完所有边时结束,而当结束时如果最小生成树的边数小于总顶点数减1,说明该图不连通
总结一下就是每次选择图中最小边权的边,如果边的两端的顶点在不同的连通块中,就把这条边加入最小生成树中。
可是怎么判断两个点是否在同一个连通块中呢?这里把连通块看作集合,问题就可以转换为判断两个点是否在同一个集合中,这里可以用并查集的方法。并查集可以通过查询两个结点所在集合的根结点是否相同来判断是否在同一集合中,而其中的合并功能也恰好可以解决将测试边加入最小生成树这一问题。
struct edge
{
int u, v;
int cost;
}E[MAXV];
int father[N]; //并查集
bool cmp(edge a, edge b) { return a.cost < b.cost; }
int findFather(int x)
{
int a = x;
while (x != father[x])
x = father[x];
//路径压缩 可以使得判断更方便
while (a != father[a])
{
int z = a;
a = father[a];
father[z] = x;
}
return x;
}
int kruskal(int n, int m) //n是顶点数,m是边数
{
int ans = 0, num_edge = 0;
//这里假设顶点编号范围[1,n]
for (int i = 1; i <= n; i++)
father[i] = i; //初始化
sort(E, E + m, cmp);
for (int i = 0; i < m; i++)
{
int faU = findFather(E[i].u);
int faV = findFather(E[i].v);
if (faU != faV) //如果不在一个集合中
{
father[faU] = faV; //合并(即测试边放入最小生成树)
ans += E[i].cost; //边权之和增加测试边的边权
num_edge++; //当前生成树的边数加1
if (num_edge == n - 1) //边数等于顶点数减1时结束
break;
}
}
if (num_edge != n - 1)
return n - 1;
else
return ans;
}