图论之最小生成树与二分图问题(acwing模板篇)

目录导航:

最小生成树:

对于一个有n个点的图,边一定是大于等于n − 1条的,最小生成树,就是在这些边中选择

n -1 条出来连接所有的n个点,且这n − 1条边的边权之和是所有方案中最小的

再说人话一点:

原图中有n个点,边大于n-1条,将多余的边删掉,使得剩下的n-1条边的边长最小

 Prim算法构造最小生成树

大神写的图解很形象:

最小生成树_ZhuRanCheng的博客-CSDN博客_最小生成树

 概括一下就是:贪心算法

每次都选择最小的那条边,并将那条最小的边放入我已经更新好的集合,并用这个集合去更新其他的点

 算法思想:

inf=0x3f3f3f3f//很大的一个数
dist[i]<----inf//初始化距离为一个很大的数
for(i=0;i<n;i++)
{
	t<----找到集合外距集合最近的点
	用t更新其他点到集合的距离
	st[t]=true//标记
}

例题:

给定一个n个点m条边的无向图,图中可能存在重边和自环,边权可能为负数。

求最小生成树的树边权重之和,如果最小生成树不存在则输出impossible。

给定一张边带权的无向图G=(V, E),其中V表示图中点的集合,E表示图中边的集合,

n=|V|,m=|E|。

由V中的全部n个顶点和E中n-1条边构成的无向连通子图被称为G的一棵生成树,其中边的权值之和最小的生成树被称为无向图G的最小生成树

 题目意思也就是上面概括说的意思,如果两点之间不联通,那么也无法生成最小生成树

代码实现:

#include <iostream>
#include <cstring>
using namespace std;
const int N = 510, INF = 0x3f3f3f3f;

int n, m;
int g[N][N], dist[N];//稠密图
//邻接矩阵存储所有边
//dist存储其他点到S的距离;
bool st[N];//判断数组

int prim() {
    //如果图不连通返回INF, 否则返回res
    memset(dist, INF, sizeof dist);//初始化距离为一个很大的数
    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 || dist[t] > dist[j]))
                t = j;
        //寻找离集合S最近的点      
        if (i && dist[t] == INF) return INF;//第一次经过i=0 不执行该if
        //判断是否连通,有无最小生成树

        if (i) res += dist[t];//如果i>0 且t到集合S的距离为dist[t]
        //cout << i << ' ' << res << endl;
        st[t] = true;//标记
        //更新最新S的权值和
        for (int j = 1; j <= n; j++) dist[j] = min(dist[j], g[t][j]);//与dijstra算法不同的地方:
    }

    return res;
}

int main() {
    cin >> n >> m;//点数,边数
    int u, v, w;

    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++)
            if (i == j) g[i][j] = 0;//重边:到自己的距离初始化为0
            else g[i][j] = INF;//不为重编:i->j的距离初始化为INF

    while (m--)//m条边
    {
        cin >> u >> v >> w;//u->v的边长为w
        g[u][v] = g[v][u] = min(g[u][v], w);//将原先的INF的边长进行覆盖
    }
    int t = prim();//接收返回值
    //临时存储防止执行两次函数导致最后仅返回0
    if (t == INF) puts("impossible");//说明无法联通
    else cout << t << endl;//输出边长之和
}

 与dijstra算法的区别与联系:

可以看到整体与dijstra算法的思路一样,代码也差不多

差别就在于:Dijkstra算法是更新到起始点的距离,Prim是更新到集合S的距离

在代码上体现为:for (int j = 1; j <= n; j++) dist[j] = min(dist[j], g[t][j]);

而dijstra算法为:

for (int j = 1; j <= n; j++)//遍历1号点到n号点
            dist[j] = min(dist[j], dist[t] + g[t][j]);//1~j点的距离变为了:1~t的距离+t~j的距离


Kruskal算法

关于算法的图解仍是推荐:

最小生成树_ZhuRanCheng的博客-CSDN博客_最小生成树

 概括:还是贪心!

将每条边按照边长进行排序,边长小的在上!那么就得到了某条边到某条边的最短距离,按照次序进行连接,也就是说先连接在上面的,在加边构造的过程中,可能会出现回路:1->2->3->1,这是不被允许的,所以跳过此边不选,最后形成了没有回路的最小生成树!

 实现功能:

(1)排序,按照边长排序即可

(2)重点:判断加了这条边是否会形成回路

可以联想到数据结构那一节的连通块,只要证明这两条边的祖先是同一个,那么就来联通了

(3)将该边加入res,最后返回res即可

代码简述:

算法思路:①将所有边按权重从小到大排序
②枚举每条边 a,b,权重是c

if a,b不连通 

​	将这条边加入集合

例题:

给定一个n个点m条边的无向图,图中可能存在重边和自环,边权可能为负数。

求最小生成树的树边权重之和,如果最小生成树不存在则输出impossible。

给定一张边带权的无向图G=(V, E),其中V表示图中点的集合,E表示图中边的集合,n=|V|,m=|E|。

由V中的全部n个顶点和E中n-1条边构成的无向连通子图被称为G的一棵生成树,其中边的权值之和最小的生成树被称为无向图G的最小生成树。

 代码实现:

#include<iostream>
#include<algorithm>
using namespace std;
const int N = 2e5 + 5;
int n, m;
struct Edge //定义结构体,方便进行对应,并排序
{
	int u, v, w;
	bool operator<(const Edge& a) const //排序的预处理
	{
		return w < a.w;
	}
}edge[N];
int p[N];
int find(int x)//并查集中的寻找相同的祖先
{
	return p[x] == x ? x : p[x] = find(p[x]);
}
int main() 
{
	int n, m;
	scanf("%d%d", &n, &m);
	for (int i = 0; i < m; i++) 
	{
		int u, v, w;
		scanf("%d%d%d", &u, &v, &w);
		edge[i] = { u,v,w };
	}
	sort(edge, edge + m);//对m条边进行排序
	for (int i = 1; i <= n; i++) p[i] = i;//存储节点
	int cnt = 0, sum = 0;//cnt:次数  sum:边长的总和
	for (int i = 0; i < m; i++)//遍历每条边
	{
		int a = edge[i].u, b = edge[i].v, w = edge[i].w;
		a = find(a);//寻找a的祖宗节点
		b = find(b);//寻找b的祖宗节点
		if (a != b)//如果祖宗节点不同:证明a,b未连通
		{
			cnt++;//将a,b连通,次数++
			sum += w;//将边长加入sum总和
			p[a] = b;//将a节点连接到b节点
		}
	}
	if (cnt < n - 1) puts("impossible");
	else printf("%d", sum);
}

核心:

明白未连接前的祖宗节点是不同的,连接后是相同的,如果是1->2->3  3->1  :我们不能连接,为什么?因为它们的祖宗节点是一样的,所以跳过这种情况,这就完美实现了克鲁斯卡尔算法


二分图

重要结论:一个图是二分图,当且仅当图中不含有奇数环

(1)染色法判断二分图

问题:

给定一个n个点m条边的无向图,图中可能存在重边和自环。

请你判断这个图是否是二分图

 图解:

二分图算法总结(染色法、匈牙利算法)_wmy0217_的博客-CSDN博客_染色法算法

 概述:将起点染为1,与起点相连的点染为2,依次类推,相邻的点染的颜色不能相同,如果没有冲突,那么该图就是二分图,如果有冲突,那么该图就不是二分图

深度优先搜索做法:

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 1e5 + 10, M = 2e5 + 10;
int n, m;
int h[N], e[M], ne[M], idx; //邻接表的存储
int color[N];

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; //当前这个点u的颜色是 c 

	for (int i = h[u]; i != -1; i = ne[i])//链表的遍历
	{
		int j = e[i];//记录节点
		if (!color[j]) //u 的邻接点 j 未被染色  
		{
			dfs(j, 3 - c); // u的颜色如果是1,j就是3-1=2;u的颜色如果是2,j就是3-2=1 
		}
		else if (color[j] == c) return false; //两邻接点染相同颜色
	}
	return true;
}

int main()
{
	cin >> n >> m;
	memset(h, -1, sizeof(h));

	while (m--)
	{
		int a, b;
		scanf("%d%d", &a, &b);
		add(a, b), add(b, a); // 无向图建两条相反方向的边 
	}

	bool flag = true;
	for (int i = 1; i <= n; i++) //遍历图的所有点 
		if (!color[i])//未染色
		{
			if (!dfs(i, 1))//将1号节点染为颜色1,并向下进行染色 :如果dfs返回的结果是false,那么就标记flag=false
			{
				flag = false;
				break;
			}
		}

	if (flag)  cout << "Yes";
	else cout << "No";
}

广度优先搜索做法:

#include<iostream>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
typedef pair<int, int> PII; //first存点编号,second存颜色 
const int N = 1e5 + 10, M = 2e5 + 10;
int n, m;
int h[N], e[M], ne[M], idx; //邻接表的存储
int color[N];

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

bool bfs(int u)
{
	queue<PII> q;
	q.push({ u,1 });//入队
	color[u] = 1; //当前这个点u的颜色是 c 

	while (q.size()) //队列不空
	{
		PII t = q.front();
		q.pop();//取出队头

		int ver = t.first, c = t.second;
		for (int i = h[ver]; i != -1; i = ne[i])//遍历链表
		{
			int j = e[i];

			if (!color[j]) //未被染色
			{
				color[j] = 3 - c;
				q.push({ j,3 - c });//入队
			}
			else if (color[j] == c) return false; //两邻接点染相同颜色 
		}
	}
	return true;
}

int main()//与dfs一样的框架
{
	cin >> n >> m;
	memset(h, -1, sizeof(h));

	while (m--)
	{
		int a, b;
		scanf("%d%d", &a, &b);
		add(a, b), add(b, a); // 无向图建两条相反方向的边 
	}

	bool flag = true;
	for (int i = 1; i <= n; i++) //遍历图的所有点 
		if (!color[i])
		{
			if (!bfs(i))
			{
				flag = false;
				break;
			}
		}

	if (flag)  cout << "Yes";
	else cout << "No";
}

易错:

单链表的存储不一定要按着顺序连在一起的比如:

树形结构 :

 在单链表存储的结构:

 与dfs的差别就在于单链表的连接顺序,重点是在脑海里构建模拟dfs,bfs


匈牙利算法

算法思路:

具体例子:兄弟我喜欢那个妹子,要不你换一个另外一个喜欢你的妹子,这个妹子让我来

二分图算法总结(染色法、匈牙利算法)_wmy0217_的博客-CSDN博客_染色法算法

 

 

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 510, M = 1e5 + 10;
int n1, n2, m;
int h[N], e[M], ne[M], idx; //邻接表
int match[N];
bool st[N];

void add(int a, int b)//a->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]) // 遍历 x 男孩喜欢所有的女孩 
	{
		int j = e[i];
		if (!st[j]) // 如果st[j] = true 就不考虑这个女孩 
		{
			st[j] = true; // 标记 j 这个女孩,作用是如果这个女孩有男朋友,我们去找她男朋友有没有可能和别的女孩在一起时,就不需要考虑他现对象 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;
		scanf("%d%d", &a, &b);
		add(a, b); //只存一条男生到女生的边就够用啦 
	}
	int res = 0; // 当前匹配的数量
	for (int i = 1; i <= n1; i++) // 遍历每个男生
	{
		memset(st, false, sizeof(st)); //代表女孩还没有考虑过,每个女孩只考虑一次
		if (find(i)) res++;  // 男生匹配到了 
	}
	cout << res;
}

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值