搜索与图论之最小生成树与二分图

目录

1.Prime算法

思路:类似于djikstra算法

堆优化Prime算法

大话数据结构Prime算法模板,可以保存最小生成树中的边集和路径

2.Kruskal算法

思路: 

 二分图:二分图_百度百科 (baidu.com)

3.染色法判断二分图

染色法思路 

说明

深入理解邻接表

4.匈牙利算法:二分图的最大匹配

最大匹配概念:

 算法思路

综上所述,这是一个很现实的算法

关于ST数组:

关于时间复杂度:

为什么数据范围是稠密图,选择用邻接表存储图:



1.Prime算法

思路:类似于djikstra算法

  1. 初始化从顶点1为第一个加入集合的点
  2. 找到不在集合中的一个点,他距离集合的距离最小,如果这个距离是我们初始化的0x3f3f3f3f,说明不存在最小生成树,因为根本就不连通
  3. 用这个点更新所有点到集合的最短距离
  4. 其中,dist数组不再表示从起点1到该点的距离,他表示从某集合中的某一个点到该点的距离

 例题引入:858. Prim算法求最小生成树 - AcWing题库

AC代码 

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 510, M = 100100, INF = 0x3f3f3f3f;

int g[N][N];    //稠密图邻接矩阵
int dist[N];    //最小生成树中每个点到集合(最小生成树顶点集合)当中的距离
int n, m, res;  //res为最小生成树的权值
bool st[N]; //判断一个点是否已经加入到最小生成树的集合当中

int prime()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;    //初始化从1节点开始拓展最小生成树
    
    for(int v = 1; v <= n; v ++ )
    {
        int t = -1;     //用来标记加入集合的顶点
        for(int i = 1; i <= n; i ++ )
        {
            if(st[i] == false && (t == -1 || dist[i] < dist[t])) //找到当前状态下不在集合中的最小权值边的顶点
                t = i;
        }
        
        st[t] = true;   //加入集合,标记一下
        
        if(dist[t] == INF)  //如果加入集合的顶点对应的最小变为INF,说明不存在最小生成树
            return INF;
        
        res += dist[t];
        
        for(int i = 1; i <= n; i ++ )   //用新加入集合的顶点更新最小生成树
            dist[i] = min(dist[i], g[t][i]);   //g[t][i]表示该顶点到集合的距离 
    }
    
    return res;
}

int main()
{
    cin >> n >> m;
    
    for(int i = 1; i <= n; i ++ )   //邻接矩阵初始化
        for(int j = 1; j <= n; j ++ )
            if(i == j)  g[i][j] = 0;
            else   g[i][j] = INF;
    
    for(int i = 0; i < m; i ++ )
    {
        int a, b, c;
        cin >> a >> b >> c;
        g[a][b] = g[b][a] = min(g[a][b], c);    //无向图考虑成特殊的有向图
    }
    
    int t = prime();
    
    if(t == INF)    cout << "impossible" << endl;
    else    cout << t << endl;
    
    // for(int i = 1; i <= n; i ++ )   cout << dist[i] << " ";
    // cout << endl;
    
    return 0;
}


堆优化Prime算法

优化思路和堆优化djikstra算法类似,但是在稠密图中,kruskal算法比Prime算法要更好一点,代码更短,思路更简单,所以很少(几乎不)使用堆优化Prime算法。


大话数据结构Prime算法模板,可以保存最小生成树中的边集和路径

861. 二分图的最大匹配 - AcWing题库


2.Kruskal算法

思路: 

  1. 用结构体存储边集数组,按照边的权值从小到大对边排序
  2. 从头到尾遍历边,只要该边加入集合后不会构成回路,就把这条边插入集合当中
  3. 如果最后插入的边数小于N-1条,说明构不成一个最小生成树
  4. 通过并查集思想判断加入一条边会不会构成回路

例题引入:859. Kruskal算法求最小生成树 - AcWing题库 

AC代码 

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 100010, M = 200010, INF = 0x3f3f3f3f;

int far[N]; //前驱数组(并查集的父亲数组)
int n, m;

struct Edge
{
    int a, b, w;
    
    bool operator< (const Edge &W)const
    {
        return w < W.w;
    }
}edges[M];

int find(int x) //并查集
{
     if(far[x] != x) far[x] = find(far[x]);
     return far[x]; //这里要返回far[x]不是返回x
}

int kruskal()
{
    int res = 0, cut = 0;   //res表示最小生成树的权值,cut表示选取的边数
    
    sort(edges, edges + m);
    
    for(int i = 0; i < m; i ++ )
    {
        int a = edges[i].a, b = edges[i].b, w = edges[i].w;
        
        a = find(a), b = find(b);   //注意找到a和b的根前驱,而不是直接前驱
        if(a != b)  //如果没有形成换
        {
            far[b] = a;   //b指向他的前驱a
            res += w;
            cut ++ ;

        }
    }
    
    if(cut < n - 1) //如果选取的边数小于n-1,那么就不足以构成一个联通无回路,也就不存在最小生成树
        return INF;
    
    return res;
}

int main()
{
    scanf("%d%d", &n, &m);
    
    for(int i = 0; i < m; i ++ )
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        edges[i] = {a, b, c};
    }
    
    for(int i = 0 ; i <= n; i ++ )  far[i] = i; //初始化前驱数组 
    
    int t = kruskal();
    
    if(t == INF)    puts("impossible");
    else printf("%d\n", t);

    
    return 0;
}


 二分图:二分图_百度百科 (baidu.com)


3.染色法判断二分图

例题引入:860. 染色法判定二分图 - AcWing题库

染色法思路 

  1. 因为二分图的点集可以划分成两个区域,我们可以用两种颜色分别给两个区域的点染色
  2. 二分图一条边的两个端点肯定分别属于这两个点集,不可能属于同一个点集,否则这就不是一个二分图
  3. 因为整个的联通分量可能大于1,即该图可能不连通。我们可以深度优先遍历每一个联通分量,给起始点染色为1,那么与起始点相邻的点就得染色为2,下一个点染色为1,以此类推,如果出现相邻两个点颜色相同,那么改图不是二分图。

AC代码

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 100010, M = 200010;   //因为该题是无向图,所以边数要乘2

int h[N], e[M], ne[M], idx;
int color[N];
int n, m;

void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}

bool dfs(int u, int c)
{
    
    color[u] = c;   //给起点染色
    
    for(int i = h[u]; i != -1; i = ne[i] )   //给该点所在的联通量染色
    {
        int j = e[i];  //千万不要忘了邻接表存储的是点的下标
        
        if(!color[j])
        {
            if(!dfs(j, 3 - c))  //因为我们染色除了1就是2,所以下一个点的染色可以用3-c(当前颜色)表示
                return false;   //如果下一个顶点染色失败,直接返回fasle
        }
        else if(color[j] == c)  return false;   //一条边的两端是同一种颜色
    }
    
    return true;
    
}

int main()
{
    cin >> n >> m;
    
    memset(h, -1, sizeof h);
    
    while(m -- )
    {
        int a, b;
        cin >> a >> b;
        add(a, b);  add(b, a);  //无向图处理成特殊的有向图
    }
    
    int flag = true;
    for(int i = 1; i <= n; i ++ )
    {
        if(!color[i])   //没有染色过
        {
            if(!dfs(i, 1))  //如果染色出现矛盾     
            {
                flag = false;             
                break;
            } 
        }
    }
    
    if(flag)    cout << "Yes" << endl;
    else    cout << "No" << endl;
    
    return 0;
}

说明

一个减少代码行数的操作:因为我们每一个点的染色只能是1或者2,我们用c表示上一个点的染色方案,那么下一个点的染色方案就是 3-c 。

深入理解邻接表

  1. 理解邻接表中h[N], e[M], ne[M]为什么大小是N或者M。
  2. h[i] 表示点 i 构成的邻接表的头指向的点,存储的是点的信息
  3. e[i] 表示这条边指向的点,存储的是点的信息,但有多少边就有多少 e[i]
  4. ne[i] 表示下一条边指向的节点,存储的是点的信息,但有多少边就有多少 e[i]
  5. idx 表示待加入边的编号
  6. e[i]ne[i] 的 i 都是边号(也可以称为顶点序号)不是点号


4.匈牙利算法:二分图的最大匹配

求二分图最大匹配可以用最大流或者匈牙利算法。

最大匹配概念:

给定一个二分图G,在G的一个子图M中,M的边集中的任意两条边都不依附于同一个顶点,则称M是一个匹配.(有点类似于单射的概念)

选择这样的边数最大的子集称为图的最大匹配问题(maximal matching problem)

如果一个匹配中,图中的每个顶点都和图中某条边相关联,则称此匹配为完全匹配,也称作完备匹配。

例题引入:861. 二分图的最大匹配 - AcWing题库

 算法思路

  1. 我们可以把二分图的两个点集A和B假象成男生和女生,那么问题就转化成了找男生和女生最多可以1对1结合的对数(我们默认只要男生钟情女生,女生就一定钟情男生)。
  2. 从第一个男生开始遍历,判断该男生是否可以找到心仪的女生,假设我们这里的男生都是专一(见好就收)的,如果该男生找到了一个心仪的女生,那么往后的女生他就不再考虑了。
  3. 如果一个男生X第一个钟情的女生K已经有归属Y(属于这个男生前面的某一个男生),这个男生X不会就此放弃,他会不撞南墙不回头,在X的强烈攻势下,Y只好选择另外一个女生,这个女生就归X所有了(成功被绿),但如果Y在接触完所有女生之后,还是只钟情于女生K,X就会被Y的深情感动,他放弃追求,变成了一个单身汉。
  4. 如果男生X第一个钟情的女生K没有归属,那么他们就可以喜结良缘了。

综上所述,这是一个很现实的算法

  1. 遍历点集N1,如果n1匹配到的N2点集中的n2没有被选用,匹配成功
  2. 否则,让n2匹配到的点匹配另外一个,如果n2匹配成功,n1就匹配成功
  3. 否则,n1匹配失败

AC代码

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 510, M = 100010;

int h[N], e[M], ne[M], idx;
int match[N];   //match[i]表示女生i的匹配对象
bool st[N]; //设置女生的状态,防止一个女生匹配多个男生
int n1, n2, m;

void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}

bool find(int x)
{
    for(int i = h[x]; i != -1; i = ne[i] )
    {
        int j = e[i];
        if(!st[j])  //如果这个女生没有被考虑过
        {
            st[j] = true;   //考虑过了,标记一下
            //要放在if的前面,否则j仍会选择match[j] 
            if(match[j] == 0 || find(match[j])) //如果该女生没有归属或者归属的人可以选择另一个
            {
                match[j] = x;
                return true;
            }
        }
    }
    
    return false;
    
}

int main()
{
    cin >> n1 >> n2 >> m;
    
    memset(h, -1, sizeof h);
    
    while(m -- )
    {
        int a, b;
        cin >> a >> b;
        add(a, b); //无论二分图是有向图还是无向图,因为我们只考虑单射,即只考虑点集A->B的情况而不考虑B->A,所以只需要加入一条A->B到的边即可
    }
    
    int res = 0;    //成功匹配的个数
    
    for(int i = 1; i <= n1; i ++ )
    {
        memset(st, false, sizeof st);   //初始化所有女生都没有考虑过
        if(find(i)) res ++ ;    //匹配成功
    }
    
    cout << res << endl;
      
    return 0;
}

关于ST数组:

st数组的主要作用在于: if(match[j] == 0 || find(match[j])) 这一步

在 find(match[i])的过程中,又调用了一次find函数,这时候如果没有把 st 标记一下,j 和 match[j]就会选择同一个女生

关于时间复杂度:

代码整体思路就是对于点集N中的x遍历整个点集M,时间复杂度最坏就是O(mn)

但是因为x不一定与点集M中的点都有连线,所以实际运行中的时间效率是比较高的,甚至是线性关系。

为什么数据范围是稠密图,选择用邻接表存储图:

因为本题需要用到找一个点x的相邻节点,所以使用邻接表更方便,邻接矩阵不具有查找相邻的元素这个性质。

posted @ 2022-05-05 08:41  光風霽月  阅读(31)  评论(0编辑  收藏  举报