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

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

所谓最小生成树,就是在一个具有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 100000
using 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算法更适用于稀疏图

第五节 二分图最大匹配

  二分图的定义是:如果一个图的所有顶点可以被分为X和Y两个集合,并且所有边的两个顶点恰好一个属于集合X,另一个属于集合Y,即每个集合内的顶点没有边相连,则此图就是 二分图。

  如何判断一个图是否为二分图?首先将任意一个顶点着红色,然后将其相邻的顶点着蓝色,如果按照这样的着色方式可以将全部顶点着色的话,并且相邻的顶点着色不同,那么该图就是二分图。

增广路:是一条路径的起点和终点都是未配对的点

如果我们已经找到一种匹配方案,如何确定当前这个匹配方案已经是最大的匹配?如果在当前匹配方案下再也找不到增广路,那么当前匹配就是最大匹配

具体算法步骤:

  • 首先从任意一个未被配对的点u开始(一般从第1个点开始),从点u的边中任意选择一条边(u->v)开始配对。

    如果此时点v没有配对成功,则配对成功,此时便找到增广路。

    如果此时点v已经配对,则递归深度搜索寻找配对。如果寻找成功,则找到一条增广路,此时需要更新原来的配对关系

  • 如果刚才所选的边配对失效,要从u的边中重新选一条边进行尝试,直到u配对成功或者所有边都被尝试为止
  • 接下来对剩余的点配对,直到所有的点都尝试完
  • 输出配对数
/*
 *匈牙利算法
 */
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

bool dfs(int u, vector<bool>& visit, vector<int>& match, vector<vector<bool> >& e){
    int n = visit.size()-1;
    for(int i = 1; i <= n; ++ i){
        if(!visit[i] && e[u][i]){
            visit[i] = true;
            if(!match[i] || dfs(match[i],visit,match,e)){
                match[i] = u;
                match[u] = i;
                return true;
            }
        }
    }
    return false;
}

int main(){
    int n,m;
    cin >> n >> m;
    vector<vector<bool> > e(n+1,vector<bool>(n+1,0));
    for(int i = 0 ; i  < m ; ++ i){
        int u,v;
        cin >> u >> v;
        e[u][v] = true;
        e[v][u] = true;
    }    
    int sum = 0;
    vector<int> match(n+1,0);
    for(int i =1 ; i <=n ; ++ i){
        vector<bool> visit(n+1,false);
        if(dfs(i,visit,match,e)) sum++;        //寻找增广路径,如果找到,配对数加1
    }
    cout<<sum<<endl;

}
二分图最大匹配

 如果二分图有n个点,那么最多找到n/2条增广路径。如果图中共有m条边,那么每找到一条增广路径最多把所有边遍历一遍,所花得时间是m,

总时间复杂度是O(NM);

原文地址:https://www.cnblogs.com/xiongqiangcs/p/3810295.html