首页 > 代码库 > 《啊哈!算法》 第八章 更多精彩的算法

《啊哈!算法》 第八章 更多精彩的算法

 第一节  镖局运镖-图的最小生成树

所谓最小生成树,就是在一个具有N个顶点的带权连通图G中,如果存在某个子图G‘,其包含了图G中的所有顶点和一部分边,且不形成回路,并且子图G‘的各边权值之和最小,则称G‘为图G的最小生成树。

最小生成树的三个性质

  • 最小生成树不能有回路
  • 最小生成树可能是一个,也可能有多个
  • 最小生产树的边的个数等于顶点的个数减一

Kruskal 算法(选边法)

其核心思想是:首先按照边的权值进行从小到大排序,每次从剩余的边中选择权值较小且边的两个顶点不在同一个集合的边(就是不会产生回路的边),加入到生产树中,直到加入了n-1条边为止。其实现是 贪心+并查集

#include <iostream>#include <vector>#include <algorithm>using namespace std;struct edge{    int u;    int v;    int w;    edge(int uu =0, int vv= 0, int ww = 0):u(uu),v(vv),w(ww){}    bool operator<(const edge& a) const{        return w < a.w;    }};vector<int> f;//建立并查集void make_set(int n){    for(int i = 0 ; i <= n; ++ i) f.push_back(i);}//查询并查集int find_set(int x){    if(f[x] == x) return x;    else{        f[x] = find_set(f[x]);        return f[x];    }}//合并子集bool union_set(int x, int y){    int t1 = find_set(x), t2 = find_set(y);    if(t1!=t2){        f[t2] = t1;        return true;    }    return false;}int main(){    int n,m;    cin >> n >> m;    vector<edge> e(m+1);    for(int i = 1 ; i <= m; ++ i){        cin >> e[i].u >> e[i].v >> e[i].w;    }    sort(e.begin(),e.end());    int sum = 0, cnt = 0;    make_set(n);    for(int i = 1; i <= m ; ++ i){        if(union_set(e[i].u,e[i].v)){            cnt++;            sum+=e[i].w;        }        if(cnt == n-1) break;    }    cout<<sum<<endl;}
Kruskal算法

 n代表顶点数,m代表边数,对边的快速排序时间复杂度是O(MlogM),在m条边中找出n-1条边是O(MlogN),所以Krusal算法时间复杂度为O(MlogM+MlogN),通常M大于N,因此最终时间复杂度为O(MlogM)。

第二节 再谈最小生成树

Prim算法(选点法)

其核心思想是:首先选择任意一个顶点加入生成树中,接下来找出一条边添加到生成树中,这需要枚举每一树顶点(已被选入生产树的顶点)到每一个非树顶点所有的边,然后找出最短的边加入到生成树中。

#include <iostream>#include <vector>#include <algorithm>#define INF 100000using namespace std;int main(){    int n,m;    cin >> n >> m;    vector<vector<int> > e(n,vector<int>(n,INF));    for(int i = 0 ; i <  n; ++ i) e[i][i] = 0;    for(int i = 0 ; i < m; ++ i){        int u,v,w;        cin>> u >> v >> w;        --u;--v;        e[u][v] = w; e[v][u] = w;    }    vector<int> dist(e[0].begin(),e[0].end());    vector<bool> visit(n,false);    visit[0 ] = true;    int cnt = 1,sum = 0;    while(cnt < n){        int minw = INF,index = 0;        for(int i = 0; i  < n ; ++ i){            if(!visit[i] && dist[i] <  minw){                minw = dist[i];                index = i;            }        }        visit[index] = true;        cnt++;        sum +=minw;        for(int k = 0; k < n; ++ k){            if(!visit[k] && dist[k]> e[index][k]){                dist[k] = e[index][k];            }        }    }    cout<<sum<<endl;}
Prim算法

上述Prim算法如果使用邻接矩阵来保存图的话,时间复杂度是O(N^2),观察代码很容易发现,时间主要浪费在每次都要遍历所有点找一个最小距离的顶点,对于这个操作,我们很容易想到用堆来优化,使得每次可以在log级别的时间找到距离最小的点,然后使用邻接表存储图,整个算法的时间复杂度会降到O(mlogn)

 

Kruskal算法更适用于稀疏图,没有使用堆优化的Prim算法适用于稠密图,使用了堆优化的Prim算法更适用于稀疏图