第三章 搜索与图论(三)——最小生成树与二分图

这篇具有很好参考价值的文章主要介绍了第三章 搜索与图论(三)——最小生成树与二分图。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

最小生成树

第三章 搜索与图论(三)——最小生成树与二分图,AcWing算法课 课程记录,图论,算法
最小生成树针对无向图,有向图不会用到

Prim

求解稠密图的最小生成树

和Dijkstra的思想相似,两者都是基于贪心
区别在于Dijkstra求单源最短路,而Prim求最小生成树
最小生成树:用最少的边连通图中所有的点,使得这些边的权值和也最小
Prim中的dis数组含义:点到集合的最短距离,注意与Dijkstra对比,不是点到源点的最短距离!

外循环迭代n次,每次选择一个点加入集合
也可以理解为迭代n次,每次选择一条与集合距离最短的边加入集合,不过第一次迭代无法选择边,只能随便选一个点作为集合中的元素,后续的迭代就是选择边(选择一条边,连接集合内的点与集合外的点,并且该边的权值最小
选择完一个点后,更新集合外的点到集合的距离
由于新的点加入了集合,连接集合内外点的边发生了变化,需要更新dis数组,使集合外的点到集合的距离最小

总结下:

  1. 迭代n次
  2. 根据dis数组每次选择一个与集合距离最近的点
  3. 用该点更新dis数组

由于要求最小生成树,所以迭代的过程中用res记录最小生成树的权值和
当找不到距离集合最近的点时(距离为正无穷),退出迭代,因为无法找到最小生成树
模板:

const int INF = 0x3f3f3f3f;
int g[N][N], dis[N], st[N];

int prim()
{
    memset(dis, 0x3f, sizeof(dis));
    int res = 0;
    for (int i = 0; i < n; ++ i )
    {
        int t = -1;
        for (int j = 1; j <= n; ++ j)
            if (!st[j] && (t == -1 || dis[j] < dis[t])) t = j;
        
        if (i && dis[t] == INF) return INF;
        if (i) res += dis[t];
        st[t] = true;
        
        for (int j = 1; j <= n; ++ j )
            dis[j] = min(dis[j], g[t][j]);
    }
    return res;
}

用堆优化prim算法,可以求解稀疏图的最小生成树。不过稀疏图的最小生成树有更优的解法——Kruskal

Kruskal

求解稀疏图的最小生成树

  1. 将所有边按权值从小到大排序
  2. 枚举每条边,x,y,d ,若两点位于不同的集合,合并两集合(选择这条边

1为该算法的效率瓶颈,时间复杂度为O(logn),2的时间复杂度为 O(m)

开始时每个点自成集合(可以理解为连通块),贪心地选择权值最小且连接两个连通块的边,直到边的数量为n - 1(点的数量 - 1),最小生成树构造完成
其中:合并集合与查找某个元素所在集合的操作为并查集的基本操作,很简单
不需要用邻接矩阵或邻接表存储边,直接用结构体存储边,简单且方便排序。重载该结构体的小于运算符,按照权值进行小于比较

const int INF = 0x3f3f3f3f;
int p[N];
struct Edge
{
    int x, y, w;
    bool operator<(const Edge& e)
    {
        return w < e.w;
    }
}edges[M];

int find(int x)
{
    if (x != p[x]) p[x] = find(p[x]);
    return p[x];
}

int kruskal()
{
    sort(edges, edges + m);
    for (int i = 1; i <= n; ++ i ) p[i] = i;
    
    int x, y, w;
    int cnt = 0, res = 0;
    for (int i = 0; i < m; ++ i )
    {
        auto t = edges[i];
        x = t.x, y = t.y, w = t.w;
        x = find(x), y = find(y);
        if (x != y)
        {
            res += w;
            cnt ++ ;
            p[x] = y;
        }
    }
    if (cnt == n - 1) return res;
    return INF;
}

二分图

第三章 搜索与图论(三)——最小生成树与二分图,AcWing算法课 课程记录,图论,算法

二分图:将所有点划分成两个集合,使得所有边连接两个集合,即集合内没有边

一个图是二分图,当且仅当图中不含奇数环

图中不含有奇数环,那么图是二分图
必要性:遍历一个奇数环,假设起点属于集合A,那么下一个点一定属于集合B,下一个点一定属于集合A…这样推导到最后,结果是起点属于集合B,与二分图的性质矛盾

充分性:由于图中不不存在奇数环,所以染色过程不会出现矛盾,那么所有的点都能正确染色,使点集被分为两个集合
反证法:假设染色过程出现了矛盾,能否推出图中一定含有奇数环?


染色法

判断二分图

通过深搜或者广搜将每个点染上颜色
出现矛盾,不是二分图
不出现矛盾,就是二分图

维护一个color数组,由于需要染两种颜色,用1和2表示这两种颜色,0表示该点为染色
从任意一个点开始进行染色,染色的过程是一个dfs。先对当前点染色,若下一个要遍历的点未染色,那么染上和当前点相反的颜色。然后需要判断这个递归是否出现矛盾,若出现矛盾返回false
若下一个要遍历的点染色了,判断其颜色是否和当前点相同,若相同则说明该图不是一个二分图,返回false
dfs最后返回true
由于需要判断的图可能是非连通图,所以需要遍历每一个点,若当前点没有染色,则将其作为dfs的起点进行染色

模板:

bool dfs(int x, int c) // x表示点的编号,c表示要染的颜色
{
	color[x] = c;
	for (int i = h[x]; i != -1; i = ne[i])
	{
		int y = e[i]; // 获取下一点的编号
		if(!color[j]) 
		{
			if (!dfs(y, 3 - c)) // 3 - c将1变成2,2变成1,也就是相反颜色
				return false;
		}
		else if(color[j] == c) return false;
	}
	return true;
}

bool flag = true;
for (int i = 1; i <= n; ++ i )
{
	if (!color[i])
	{
		if (!dfs(i, 1))		
		{	
			flag = falsebreak;	
		}
	}
}
if (flag == false) // 不是二分图
else // 是二分图

匈牙利算法

二分图的最大匹配

在二分图中求子图,子图中不存在邻接两条边的点,即两点通过一条边匹配
对于二分图的两个顶点集合,假设为n1n2
遍历n1的所有点,对于每个点,遍历邻接该点的所有边

  • 若另外一点未匹配,则选择该点,匹配成功
  • 若另外一点已经匹配,试着让其匹配的对象匹配其他点
    • 若其匹配的对象能够匹配其他点,那么两点匹配成功
    • 若其匹配的对象无法匹配其他点,那么两点匹配失败

时间复杂度为O(nm),这是最坏情况,一般情况下是线性复杂度
以上操作中,匹配是一个相同子问题,所以总问题可以用匹配这个子问题分解

对于逻辑中的一些细节:
遍历点的所有邻边时,需要注意对方是否已经匹配,这里用match数组表示对方已经匹配的点,默认值为0(没有0号点)表示还未匹配
当对方已经匹配,此时要看对方的匹配对象是否能匹配其他点。也就是假设对方已经和自己匹配,判断对方的匹配对象在不选择对方的情况下,是否能匹配成功
所以这里需要一个状态表示对方已经和自己匹配的假设,用st数组表示当前点正在考虑或考虑完成某点。每次遍历某点的邻边时,先设置st数组,以表示自己正在考虑对方
接着会出现两种情况:

  1. 对方没有匹配,此时和对方匹配成功
  2. 对方已经匹配,对方的匹配对象需要在不考虑对方的情况下,试着匹配其他点。不考虑对方的前提通过st数组限制,对方的匹配对象每次匹配前通过st数组判断试着匹配的对象是否被考虑,正在被考虑的点无法被匹配
  • 当前点试着匹配对方时,可能匹配成功,可能匹配失败。匹配失败的情况下,st数组对应位置表示该点被考虑完成(已经无法匹配),后续不需要再考虑该点了

考虑匹配是两个不同的概念,两者无法等价
已经被匹配的情况下,可以让对方的匹配对象重新匹配。若对方的匹配对象重新匹配失败,此时考虑完成,对方真的无法匹配
然后试着匹配下一条邻边的对象,若对方的匹配对象需要重新匹配,那么他就不应该试着匹配已经考虑完成的点
考虑完成的点一定无法匹配,虽然考虑完成分为匹配成功与匹配失败两种情况,但是匹配成功后,st数组失去了意义(此时要匹配n1的下一个点),因为该数组要被重置
只有无法匹配的情况下,考虑完成的点(st数组)才有意义,因为它将限制其他点的匹配与重新匹配。因此这时的st数组的含义变成了:该点是否被强绑定(该点的对象无法匹配其他点了,此时两点只能绑定在一起

匹配下一个n1中点的对象时,将st数组被重置为false,表示n2中所有的点未被考虑过

虽然是无向图,但是我们只要存储n1n2的边, n2n1的边没必要存
模板:

int h[N], e[N], ne[N], idx = 1;
bool st[N];
int match[N];

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 (match[j] == 0 || find(match[j]))
            { 
                match[j] = x;
                return true;
            }
        }
    }
    return false;
}

最小生成树练习题

858. Prim算法求最小生成树

858. Prim算法求最小生成树 - AcWing题库
第三章 搜索与图论(三)——最小生成树与二分图,AcWing算法课 课程记录,图论,算法

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 510;
const int INF = 0x3f3f3f3f;
int g[N][N], dis[N];
bool st[N];
int n, m;

int prim()
{
    memset(dis, 0x3f, sizeof(dis));
    int res = 0;
    for (int i = 0; i < n; ++ i )
    {
        int t = -1;
        for (int j = 1; j <= n; ++ j)
            if (!st[j] && (t == -1 || dis[j] < dis[t])) t = j;
        
        if (i && dis[t] == INF) return INF;
        if (i) res += dis[t];
        st[t] = true;
        
        for (int j = 1; j <= n; ++ j )
            dis[j] = min(dis[j], g[t][j]);
    }
    return res;
}

int main()
{
    memset(g, 0x3f, sizeof(g));
    
    scanf("%d%d", &n, &m);
    int x, y, d;
    while (m -- )
    {
        scanf("%d%d%d", &x, &y, &d);
        g[x][y] = g[y][x] = min(g[x][y], d);
    }
    
    int t = prim();
    
    if (t == INF) puts("impossible");
    else printf("%d\n", t);
    
    return 0;
}

859. Kruskal算法求最小生成树

859. Kruskal算法求最小生成树 - AcWing题库
第三章 搜索与图论(三)——最小生成树与二分图,AcWing算法课 课程记录,图论,算法

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1e5 + 10, M = 2 * N;
const int INF = 0x3f3f3f3f;
int n, m;
int p[N];
struct Edge
{
    int x, y, w;
    bool operator<(const Edge& e)
    {
        return w < e.w;
    }
}edges[M];

int find(int x)
{
    if (x != p[x]) p[x] = find(p[x]);
    return p[x];
}

int kruskal()
{
    sort(edges, edges + m);
    for (int i = 1; i <= n; ++ i ) p[i] = i;
    
    int x, y, w;
    int cnt = 0, res = 0;
    for (int i = 0; i < m; ++ i )
    {
        auto t = edges[i];
        x = t.x, y = t.y, w = t.w;
        x = find(x), y = find(y);
        if (x != y)
        {
            res += w;
            cnt ++ ;
            p[x] = y;
        }
    }
    if (cnt == n - 1) return res;
    return INF;
}

int main()
{
    scanf("%d%d", &n, &m);
    int x, y, d;
    for (int i = 0; i < m; ++ i )
    {
        scanf("%d%d%d", &x, &y, &d);
        edges[i] = { x, y, d };
    }
    
    int t = kruskal();
    if (t == INF) puts("impossible");
    else printf("%d\n", t);
    
    return 0;
}

二分图练习题

860. 染色法判定二分图

860. 染色法判定二分图 - AcWing题库
第三章 搜索与图论(三)——最小生成树与二分图,AcWing算法课 课程记录,图论,算法

#include <cstring>
#include <iostream>
using namespace std;

const int N = 1e5 + 10, M = 2 * N;
int h[N], e[M], ne[M], idx = 1;
int color[N];
int n, m;

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

bool dfs(int x, int c)
{
    color[x] = c;
    for(int i = h[x]; i != -1; i = ne[i])
    {
        int y = e[i];
        if (!color[y]) 
        {
            if (!dfs(y, 3 - c)) return false;
        }
        else if (color[y] == c) return false;
    }
    return true;
}

int main()
{
    memset(h, -1, sizeof(h));
    scanf("%d%d", &n, &m);
    int x, y;
    while (m -- )
    {
        scanf("%d%d", &x, &y);
        add(x, y), add(y, x);
    }
    
    bool flag = true;
    for (int i = 1; i <= n; ++ i )
    {
        if (!color[i])
        {
            if (!dfs(i, 1))
            {
                flag = false;
                break;
            }
        }
    }
    if (flag) puts("Yes");
    else puts("No");
    
    return 0;
}

debug:把int color[N]写成bool color[N],看了半天,乐


861. 二分图的最大匹配

861. 二分图的最大匹配 - AcWing题库
第三章 搜索与图论(三)——最小生成树与二分图,AcWing算法课 课程记录,图论,算法文章来源地址https://www.toymoban.com/news/detail-533905.html

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 510, M = 1e5 + 10;
int h[N], e[M], ne[M], idx = 1;
int match[N];
bool st[N];
int n1, n2, m;

void add(int x, int y)
{
    e[idx] = y, ne[idx] = h[x], h[x] = 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 (match[j] == 0 || find(match[j]))
            { 
                match[j] = x;
                return true;
            }
        }
    }
    return false;
}

int main()
{
   scanf("%d%d%d", &n1, &n2, &m);
   memset(h, -1, sizeof(h));
   int x, y, res = 0;
   
   while (m -- )
   {
       scanf("%d%d", &x, &y);
       add(x, y);
   }
   
   for (int i = 1; i <= n1; ++ i )
   {
       memset(st, false, sizeof(st));
       if (find(i)) res ++ ;
   }
   
   printf("%d\n", res);
    return 0;
}

到了这里,关于第三章 搜索与图论(三)——最小生成树与二分图的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 第三章 图论 No.5最小生成树之虚拟源点,完全图与次小生成树

    虚拟源点:1146. 新的开始 1146. 新的开始 - AcWing题库 与一般的最小生成树问题不同,本题需要在建立电站的电井之间建立电网,在两个电站之间建立电网需要花费金额,可以看成一条具有权值的边 但是建立电网的前提是:其中一个电井需要建立电站,建立电站也需要费用 已经

    2024年02月14日
    浏览(40)
  • 第三章 图论 No.3 flody之多源汇最短路,传递闭包,最小环与倍增

    flody的四个应用: 多源汇最短路 传递闭包 找最小环 恰好经过k条边的最短路 倍增 多源汇最短路:1125. 牛的旅行 1125. 牛的旅行 - AcWing题库 直径概念:同一连通块中,两个距离最远的点之间的距离 如何求直径?由于图中存在着两个连通块,所以直接对全图做一个flody,就能更

    2024年02月14日
    浏览(44)
  • 【算法基础:搜索与图论】3.5 求最小生成树算法(Prim&Kruskal)

    最小生成树 有关树的定义 生成子图 :生成子图是从原图中选取部分节点以及这些节点之间的边所组成的图。生成子图中的所有节点和边都必须在原图中存在。 生成树 :一个连通 无向图 的 生成子图 ,同时要求是树。也即在图的边集中选择 n - 1 条,将所有顶点连通。 我们

    2024年02月16日
    浏览(40)
  • 搜索与图论(一)(深搜,广搜,树与图的存储遍历,拓扑排序)

    往深里搜,搜到叶子结点那里,回溯,到可以继续到叶子结点深搜的位置。 1、回溯一定要恢复现场 2、定义一个与当前递归层数有关的终止条件(题目要求的东西) 3、每层都用循环判断是否存在可以dfs的路 输出数字组合 全排列的思想解决n皇后问题,用三个bool数组描述限制

    2024年02月19日
    浏览(45)
  • 【算法基础:搜索与图论】3.2 树与图的dfs和bfs

    要学会建树、建图的通用方法。 dfs 和 bfs 的代码框架。 https://www.acwing.com/problem/content/848/ 在 dfs 的过程中,统计各个节点作为断点时的连通块最大值。 https://www.acwing.com/problem/content/849/ 看到最短距离就可以想到使用宽搜。 注意! :题目中说明了 a 和 b 表示存在一条从 a 走到

    2024年02月16日
    浏览(37)
  • 第三章 图论 No.13拓扑排序

    拓扑序和DAG有向无环图联系在一起,通常用于最短/长路的线性求解 裸题:1191. 家谱树 1191. 家谱树 - AcWing题库 差分约束+拓扑排序:1192. 奖金 1192. 奖金 - AcWing题库 由于图中所有边权都是正数,可以直接使用topsort求解差分约束问题 根据题意,要求一个最小值,使用最长路求解

    2024年02月12日
    浏览(43)
  • C++算法之旅、06 基础篇 | 第三章 图论

    常用代码模板3——搜索与图论 - AcWing 尽可能往深处搜,遇到叶子节点(无路可走)回溯, 恢复现场继续走 数据结构:stack 空间:需要记住路径上的点, (O(h)) 。 ⭐ BFS使用空间少; 无最短路 性质 每个DFS一定对应一个 搜索树 ;要考虑用什么 顺序 遍历所有方案;DFS就是递

    2024年02月10日
    浏览(43)
  • 第三章 图论 No.12欧拉回路与欧拉路径

    小学一笔画问题,每条边只经过一次 判断图是否存在欧拉回路:判断图是否连通(存在孤立边),再根据有向/无向具体判断 对于无向图来说, 欧拉路径 中,起点和终点的度数为奇数,中间点的度数为偶数 起点和终点:开始和结束时必须经过一条边,其余情况为:从一条边

    2024年02月12日
    浏览(41)
  • 第三章 图论 No.10无向图的双连通分量

    无向图有两种双连通分量 边双连通分量,e-DCC 点双连通分量,v-DCC 桥:删除这条无向边后,图变得不连通,这条边被称为桥 边双连通分量:极大的不含有桥的连通区域,说明无论删除e-DCC中的哪条边,e-DCC依旧连通 (该连通分量的任意边属于原图中的某条环)。此外,任意两

    2024年02月12日
    浏览(39)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包