Lazy Prim介绍

0.前言

关于MST大家都知道Prim,但是Prim也有多种类别。这里介绍一下Lazy Prim。

因为前段时间有个项目需求就是这个。


1.切分定理

1.定义:在一给定的无向连通图G = (V, E) 中,找到一颗生成树,使得这V-1条边的权值之和最小,这样的生成树就是最小生成树。

而所谓的生成树,简单的说就是在所有的边之间找到V-1条边,使得所有顶点连通且没有形成环。

最小生成树其实是最小权重生成树的简称。

那么如何找到这样的最小生成树呢?这里,就可以用prim算法。这里,我们先介绍lazy prim算法。

在介绍lazy prim算法之前,我们先来介绍一个重要的定理——切分定理。它是prim算法的核心。

我们以一个实例作为讲解。

Lazy Prim介绍_权值

这样的一个图表示的各个边的权值如下:

好了,到底什么是切分定理呢?这里我们先介绍几个定义:

切分​:把图中的节点分为两部分,称为一个切分(Cut)

横切边​:如果一个边的两个端点,属于切分(Cut)不同的两边,这个边称为横切边(Crossing Edge);

切分定理​:给定任意切分,横切边中权值最小的边必然属于最小生成树。

如果文字描述不是太理解,小伙伴们可以看下面这张图。

Lazy Prim介绍_权值_02

我们看到这个图有两个部分,蓝色部分和红色部分。而所有绿色的边,就是所谓的横切边,因为绿色的边的两个端点属于属于不同的部分。

那么根据切分定理,我们知道0~7的这条边就是最小生成树的一条边,因为其权值最小。

那么问题来了,为什么这条边属于最小生成树?

这里我们给出证明:

我们看上面这个图,我们可以把这个图分为两个部分。由于图的连通性,那么在这两部分中必然有横切边,并且我们假定绿色的边权值最小。因为要保持连通性,在这三条边中必然要选择一条边使得这两部分连通。如果我们假定左右两部分都已经是最小生成树了,那么从三条边中选择,为了保持总权值最小,必然是选择绿色的边。这就是切分定理。

如果按照这个定理,聪明的小伙伴们可能对这个lazy prim算法有了眉目。既然给定任意切分,横切边中权值最小的边必然属于最小生成树,那么我们完全可以从第0个顶点开始,第0个顶点与其他部分切分,找到最小权值边,然后向下一个顶点扩散。再把这个顶点与第0个顶点当作同一部分与剩下的顶点切分,再找到下一个权值最小边,依此类推。当然,这里我们需要注意不能让这个树变成了环。也就是同一部分的顶点之间的边不能选择。

下面我们用几个图解释一下。

将 0 作为起始点,开始切分,逐步将蓝色阵营的顶点 转换到红色阵营中 。

具体的模拟过程,可以参考这篇博客。



2.思路

标记一个起点。

  • 每次把横切边都丢到优先队列
  • 取最小的加入生成树,然后标记这两个点。
  • 直到没有横切边。

这种算法时对应我们下面那个代码的,不过这个写法不是最优的。

代码见第一个java。


更体现lazy的一种:

  • 选取起点,加入与他相连的边到优先队列,标记起点。
  • 每次取出队首,如果不是横切边,pop掉之后不管。
  • 否则访问未标记点,然后加边到队列,标记点。更新MST的weight

代码见第二个cpp。

3.代码

代码用到了jar包,见注释。

需要每次遍历全图,时间不优。

Download link: https://algs4.cs.princeton.edu/code/algs4.jar
In in = new In(args[0]);
EdgeWeightedGraph G = new EdgeWeightedGraph(in);
boolean[] marked = new boolean[G.V()]; // In the early Monday tutorial, Boolean is a class whereas boolean is a primitive.
Queue<Edge> mst = new Queue<Edge>(); // A linked list could also work.
double weight = 0.0;
marked[0] = true;

while (true) {
// In order to find a minimum crossing edge, we loop through all the edges in the graph and
// add all of the crossing edges to a minimum priority queue.
MinPQ<Edge> minpq = new MinPQ<Edge>();

for (Edge e : G.edges()) {
int u = e.either();
int v = e.other(u);
// B) Check the condition if they are cross edges and insert to pq
if (marked[u] != marked[v])
minpq.insert(e);
}

// If the priority queue is empty, then this means there are no more crossing edges so we are done.
if (minpq.isEmpty()) {
break;
}

// If the priority queue is not empty, then the minimum element in it is a minimum crossing edge.

// C) get the edge minimum from pq
Edge cross_edge = minpq.delMin();
// D) Update the (current) MST and Mark the other vertex
mst.enqueue(cross_edge);
int u = cross_edge.either();

int v = cross_edge.other(u);
weight += cross_edge.weight();
if (!marked[v]) marked[v] = true;
if (!marked[u]) marked[u] = true;

}
// Once an MST has been found, print its edges and total weight.
for (Edge e : mst) {
StdOut.println(e);
}
StdOut.printf("%.5f\n", weight); //打印边权

更快点的做法。

#include<iostream>
#include<queue>
#define INF 1e4
using namespace std;
double G[8][8] =
{
//0 1 2 3 4 5 6 7
{INF,INF,0.26,INF,0.38,INF,0.58,0.16},
{INF,INF,0.36,0.29,INF,0.32,INF,0.19},
{0.26,0.36,INF,0.17,INF,INF,0.40,0.34},
{INF,0.29,0.17,INF,INF,INF,0.52,INF},
{0.38,INF,INF,INF,INF,0.35,0.93,0.37},
{INF,0.32,INF,INF,0.35,INF,INF,0.28},
{0.58,INF,0.40,0.52,0.93,INF,INF,INF},
{0.16,0.19,0.34,INF,0.37,0.28,INF,INF}
};//数组初始化
//定义一个结构体,表明这是一个由v、u连接的边,权值为w
//并将运算符重载,比较大小的是比较权值
struct Edge {
int v;
int u;
double w;
bool operator < (const Edge& a) const {
return w > a.w;
}
};
//标记数组,判断一个顶点是否已经在红色阵营了
bool marked[8];
//优先队列
priority_queue<Edge,vector<Edge>,less<Edge> > w;
//vis函数是用来把x顶点放入红色阵营
//这里是赋值为1
//并把所有与x顶点连接的边放入优先队列
void vis(int x) {
marked[x] = 1;
for (int i = 0; i < 8; i++) {
if (G[x][i] == INF)
continue;
w.push({ x,i,G[x][i] });
}
}
int main()
{
//先访问第0个顶点
vis(0);
//最小权值
double minw = 0;
while (!w.empty()) {
Edge e = w.top();
w.pop();
//如果顶点u和v的都在红色阵营,那么值一定相同,这时我们就跳过
if (marked[e.u] == marked[e.v])
continue;
//访问顶点u
vis(e.u);
minw += e.w;
}
cout << minw;
return 0;
}