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

news2025/1/10 11:40:28

文章目录

    • 最小生成树
      • Prim
      • Kruskal
    • 二分图
      • 染色法
      • 匈牙利算法
    • 最小生成树练习题
      • 858. Prim算法求最小生成树
      • 859. Kruskal算法求最小生成树
    • 二分图练习题
      • 860. 染色法判定二分图
      • 861. 二分图的最大匹配

最小生成树

image.png
最小生成树针对无向图,有向图不会用到

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;
}

二分图

image.png

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

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

图中不含有奇数环,那么图是二分图
必要性:遍历一个奇数环,假设起点属于集合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题库
image.png

#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题库
image.png

#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题库
image.png

#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题库
image.png

#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;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/717310.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Error in parsing ‘.arclint‘ file, in key ‘bin‘ for linter ‘pylint‘

背景&#xff1a; Run arc diff --preview to create code revision on remote terminal, but exception happened. nnhhh:~/ppp$ arc diff --preview Linting...Exception Error in parsing .arclint file, in key bin for linter pylint. None of the configured binaries …

剑指offer28.对称的二叉树

我一开始想到的是用之前的镜像二叉树方法把树转换成他的镜像树放进队列&#xff0c;在这之前把树自己放进队列。然后比较这两个队列。但这样是有问题的&#xff0c;比如题目给的[1,2,2,null,3,null,3] 这个示例就不能通过&#xff0c;于是看了题解。豁然开朗&#xff0c;其实只…

服务器上安装虚拟机以及编译FastDDS以及ShapesDemo开源项目

&#x1f941;作者&#xff1a; 华丞臧 &#x1f4d5;​​​​专栏&#xff1a;【C】 各位读者老爷如果觉得博主写的不错&#xff0c;请诸位多多支持(点赞收藏关注)。如果有错误的地方&#xff0c;欢迎在评论区指出。 推荐一款刷题网站 &#x1f449;LeetCode 文章目录 前言一、…

namecheap 域名服务器 设置为Cloudflare

Namecheap 设置 自定义 域名服务器 登录Namecheap 帐户。进入后&#xff0c;将鼠标悬停在页面右上角的“帐户”选项上&#xff0c;然后选择“域列表”或选择左侧边栏中的“域列表” 参考 如何在 Cloudflare 帐户中域设置 DNS 记录

Simulink中Selector的使用

文章目录 0.prolog1 Starting and ending indices (port)2. Starting index (port)3. Starting index (dialog)4. Index vector (dialog)5. Index vector (port)Reference 0.prolog Index mode有两种&#xff0c;[one-based, zero-based]&#xff0c;分别是从1开始计数&#x…

波函数:描述量子世界的数学工具

亲爱的读者&#xff0c; 欢迎回到我们的量子力学系列文章。在前两篇文章中&#xff0c;我们介绍了量子力学的起源和基本概念。今天&#xff0c;我们将深入探讨量子力学的核心数学工具——波函数。 波函数是量子力学中的关键概念&#xff0c;它描述了一个量子系统的状态。波函…

Java转Go:java开发者转学go语言,请给我一些建议和学习推荐

在做开发时遇到最无理的需求就是部门没了&#x1f602; 目录 做开发时你遇到最无理的需求是什么&#xff1f;方向一&#xff1a;分享那些你遇到的无理需求方向二&#xff1a;面对这些无理需求时你是怎么做的&#xff1f;方向三&#xff1a;怎么避免遇见这些无理需求 java开发者…

赛效:怎么在线给Word文档加图片水印

1&#xff1a;在电脑网页上打开云组件&#xff0c;点击“Word转换”菜单里的“Word加水印&#xff08;图片&#xff09;”。 2&#xff1a;点击选择文件添加Word文档。 3&#xff1a;点击“选择水印图片”上传做水印的图片。 4&#xff1a;水印图片添加成功后可以选择水印角度&…

电商小程序开发指南:吸引并留住用户的秘诀

电商小程序作为微信生态内的新产品&#xff0c;有许多开发方面的内容需要学习&#xff0c;比如电商小程序的定位、功能、设计等。电商小程序是由商家开发并在微信平台上运行的小程序。它可以与微信公众号一起使用&#xff0c;也可以单独使用。 从传统电商到社交电商&#xff0…

24-正则表达式,应用场景

一、是什么 是一种用来匹配字符串的强有力的武器 它的设计思想是用一种描述性的语言定义一个规则&#xff0c;凡是符合规则的字符串&#xff0c;我们就认为它“匹配”了&#xff0c;否则&#xff0c;该字符串就是不合法的 在 JavaScript中&#xff0c;正则表达式也是对象&…

Spring Boot 缓存应用实践

缓存是最直接有效提升系统性能的手段之一。个人认为用好用对缓存是优秀程序员的必备基本素质。本文结合实际开发经验&#xff0c;从简单概念原理和代码入手&#xff0c;一步一步搭建一个简单的二级缓存系统。 一、通用缓存接口 1、缓存基础算法 FIFO&#xff08;First In Fir…

LVS负载均衡群集与LVS-NAT部署实战配置

文章目录 一.什么是集群1.群集的含义 二.集群使用在那个场景三.集群的分类1.负载均衡器群集2.高可用群集3.高性能运算群集 四.负载集群的架构1.第一层&#xff0c;负载调度器2.第二层&#xff0c;服务器池3.第三层&#xff0c;共享存储 五.负载均衡集群的工作模式1.地址转换 &a…

STM32中static和extern的用法

static&#xff1a; A. static变量 称为静态变量。根据变量的类型可以分为静态局部变量和静态全程变量。 1. 静态局部变量 它与局部变量的区别在于: 在函数退出时, 这个变量始终存在, 但不能被其它 函数使用, 当再次进入该函数时, 将保存上次的结果。其它与局部变量一样。…

记一次自建靶场三层代理内网渗透过程

为方便您的阅读&#xff0c;可点击下方蓝色字体&#xff0c;进行跳转↓↓↓ 01 向日葵RCE外网突破02 Frp内网隧道搭建03 获取域内出网主机权限04 三层隧道搭建访问内网不出网主机 01 向日葵RCE外网突破 端口扫描探测存活端口&#xff0c;发现存在172.16.16.128:49773端口 访问…

【RPC】—Protobuf编码原理

Protobuf编码原理 ⭐⭐⭐⭐⭐⭐ Github主页&#x1f449;https://github.com/A-BigTree 笔记链接&#x1f449;https://github.com/A-BigTree/Code_Learning ⭐⭐⭐⭐⭐⭐ Spring专栏&#x1f449;https://blog.csdn.net/weixin_53580595/category_12279588.html SpringMVC专…

【跨域认证】详解JWT,JWT是什么?

JSON Web Token&#xff08;缩写 JWT&#xff09;是目前最流行的跨域认证解决方案&#xff0c;本文介绍它的原理和用法。 一、跨域认证的问题 互联网服务离不开用户认证。一般流程是下面这样。 1、用户向服务器发送用户名和密码。 2、服务器验证通过后&#xff0c;在当前对话&…

[SSM]MyBatis使用javassist生成类和接口代理机制

目录 六、使用javassist生成类 6.1Javassist的使用 6.2使用Javassist生成DaoImpl类 七、MyBatis中接口代理机制及使用 7.1在之前的web应用中使用接口代理机制 7.2使用接口代理机制完成之前的CRUD(部分代码) 六、使用javassist生成类 6.1Javassist的使用 引入javassist依…

王道考研计算机网络第五章知识点汇总

5.1.1 传输层概述 复用&#xff1a;好比家里面每个人都要写信&#xff0c;向信箱里面投入信件&#xff0c;然后由邮递员取走。 分用&#xff1a;就是每个人都收到了各自的回信&#xff0c;然后从信箱中取走各自的信 5.2 UDP协议 注意&#xff1a;用户数据报和检验和都是指的整…

数学建模——插值(下)

本文是面向数学建模准备的&#xff0c;是介绍性文章&#xff0c;没有过多关于原理的说明&#xff01;&#xff01;&#xff01; 目录 一、2维插值原理及公式 1、二维插值问题 2、最邻近插值 3、分片线性插值 4、双线性插值 5、二维样条插值 二、二维插值及其Matlab工具箱…