算法:Kruskal

Kruskal算法是解决最小生成树问题的一种算法。其算法过程是

  • 一开始,所有点都是孤立的点。
  • 对所有的边从小到大排序
  • 每次选取权值最小且其两个顶点还不连通的边
  • 连通选取的边的两个顶点
  • 重复这个过程,直到所有的顶点连通

问题:什么时候停止循环?
答案:选取了n-1条边
问题:为什么是n-1条边?选取了n-1条边就保证n个顶点连通了?
答案:n个顶点连通,且不构成回路,需要n-1条边。那存在n-1条边,并且不构成回路,那么通过这n-1条边,n个顶点就是连通的。

在Kruskal算法中,每次选取一条权值最小边且其两个顶点还不连通,每次选一条边,有三种情况:一是两个顶点自成一派,都是孤立的点,二是一个顶点在已知连通分量里,另一个顶点是孤立的点,三是两个顶点在两个未连通的已知连通分量里,第一种情况的两个顶点连通后,成为单独的连通分量,这种情况增加一条边,增加两个顶点。第二种情况的两个顶点连通后,已知连通分量增加一个顶点,这种情况增加一条边,增加一个顶点。第三种情况的两个顶点连通后,两个未连通的已知连通分量连通了,这种情况增加一条边,顶点的个数不变。问题是选取n-1条这样的边,为什么n个顶点就都连通了?。如果n个顶点中存在n-1条不构成回路的边,那么这n个顶点就是连通的。可以简单的用数学归纳法证明。

当n=2时,如果存在1条边,显然这两个顶点是连通的。
假设n = m,并且存在m-1条不构成回路的边,m个顶点是连通的。当n=m+1时,即在顶点数为m的情况下,增加一个点和一条边,因为要不构成回路,增加的边不能加在原来的m个顶点里,只能加在新增的点上,用来连接新增的点和原来m个点中的任何一个,所以当n=m+1,并且存在m条不构成回路的边的情况下,m+1个顶点也是连通的。

因为每次选择的都是权值最小的边,所以最后的结果就是最小生成树。这是贪心策略的体现。

还要说明的一点是查询两个点是否已经在同一连通分量上,用的是并查集。

代码如下:

//表示边
struct Edge {
    int u;//起点
    int v;//终点
    int w;//权重
};

///假设有编号从1到n的n个顶点,m条边
class Kruskal {
    vector<int> boss;
public:
    static bool compreEdge(Edge &e1, Edge &e2) {
        return e1.w < e2.w;
    }
    vector<Edge> minimumSpanningTree(vector<Edge> &edges,int n) {
        for(int i = 0;i <= n;i++) {
            boss.emplace_back(i);
        }
        sort(edges.begin(), edges.end(), compreEdge);
        vector<Edge> ans;
        int count = 0;
        for(int i = 0;i < edges.size();i++) {
            Edge edge = edges[i];
            if (merge(edge.u, edge.v)) {
                ans.emplace_back(edge);
                count++;
            }
            if (count == n - 1) {
                break;
            }
        }
        return ans;
    }
    
    int getBoss(int u) {
        if (boss[u] == u) {
            return u;
        }
        else {
            boss[u] = getBoss(boss[u]);
            return boss[u];
        }
    }
    
    bool merge(int u, int v) {
        int t1 = getBoss(u);
        int t2 = getBoss(v);
        if (t1 == t2) {
            return false;
        }
        boss[t2] = t1;
        return true;
    }
    
};