蓝桥杯_图论_3 (最小生成树 【prim】+【Kruskal】- 二分图【染色法】+【匈牙利】)

最小生成树

①prim普利姆算法
	朴素版 O(n^2^)(代码简单) 【稀疏图】
	堆优化版 O(mlogn)(不常用)
②Kruskal克鲁斯卡尔 O(mlogm)    【稠密图】

二分图
染色法O(n + m)
匈牙利算法 O(mn) 实际运行时间远小于O(mn)

prim普利姆算法 【朴素版】

给定一个n个点m条边的无向图,图中可能存在重边和自环,边权可能为负数。
求最小生成树的树边权重之和,如果最小生成树不存在则输出impossible。
给定一张边带权的无向图G=(V, E),其中V表示图中点的集合,E表示图中边的集合,n=|V|,m=|E|。
由V中的全部n个顶点和E中n-1条边构成的无向连通子图被称为G的一棵生成树,其中边的权值之和最小的生成树被称为无向图G的最小生成树。
输入格式
第一行包含两个整数n和m。
接下来m行,每行包含三个整数u,v,w,表示点u和点v之间存在一条权值为w的边。
输出格式
共一行,若存在最小生成树,则输出一个整数,表示最小生成树的树边权重之和,如果最小生成树不存在则输出impossible。
数据范围
1≤n≤500,
1≤m≤105,
图中涉及边的边权的绝对值均不超过10000。
输入样例:
4 5
1 2 1
1 3 2
1 4 3
2 3 2
3 4 4
输出样例:
6

思路旧:
dist[i] 初始正无穷 [连通路径集合]
for(int i = 0 ;i < n;i++)
{
t = 找到集合外距离最最近的点 , 集合指已经确定路径的点(已经加入连通块)
用t更新它到集合的距离
st[t] = true; //标记此点已访问
}
可与迪杰斯特拉算法对比【不能说很像,只能说一模一样emmm~】 【思路,遍历,初始 ~类似】
【把思路翻译成时间复杂度】

简记:
main: 邻接表初始化,取重边最小权
prim: n次加点
				类似Dijkstra判断最小权,j:1-->n
					if(i && dist[t] == INF) return INF;	//不可达 ,图是不连通的,不存在最小生成树 
					if(i) res += dist[t]; // 先累加再更新,防止负环
				选取完,更新最小边权 ,j:1-->n
				加入集合 st[t] = true;



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

const int N = 510,INF = 0x3f3f3f3f;

int n,m;
int g[N][N];
int dist[N];
int st[N];

int prim()
{
	memset(dist,0x3f,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])) //    (还没有找到任何一个点||当前距离大于起点到j的距离)
				t = j;

		if(i && dist[t] == INF) return INF;	//不可达 ,图是不连通的,不存在最小生成树 
		if(i) res += dist[t]; // 先累加再更新,防止负环

		for(int j = 1 ; j <= n ; j++) dist[j] = min(dist[j] , g[t][j]);  //【选取第j边的最短距(非路径整体最短距)】
		st[t] = true;
	}

	return res;
}

int main()
{
	scanf("%d%d",&n,&m);

	memset(g,0x3f,sizeof g);

	while(m --)
	{
		int a,b,c;
		scanf("%d%d%d",&a,&b,&c);
		g[a][b] = g[b][a] = min(g[a][b] , c); //树是无环连通无向图
	}

	int t = prim();

	if(t == INF) puts("impossible");
	else printf("%d\n",t);

	return 0;
}

859. Kruskal算法求最小生成树 【贪心】

给定一个n个点m条边的无向图,图中可能存在重边和自环,边权可能为负数。
求最小生成树的树边权重之和,如果最小生成树不存在则输出impossible。
给定一张边带权的无向图G=(V, E),其中V表示图中点的集合,E表示图中边的集合,n=|V|,m=|E|。
由V中的全部n个顶点和E中n-1条边构成的无向连通子图被称为G的一棵生成树,其中边的权值之和最小的生成树被称为无向图G的最小生成树。
输入格式
第一行包含两个整数n和m。
接下来m行,每行包含三个整数u,v,w,表示点u和点v之间存在一条权值为w的边。
输出格式
共一行,若存在最小生成树,则输出一个整数,表示最小生成树的树边权重之和,如果最小生成树不存在则输出impossible。
数据范围
输入样例:
4 5
1 2 1
1 3 2
1 4 3
2 3 2
3 4 4
输出样例:
6

思路:
将所有边按权重从小到大排序 O(mlogm)
枚举每条边a,b 权重w
if a,b不连通 ,将这条边加入集合

【数据结构837.连通块操作 - 并查集的简单应用】 O(m)

简记:【重载 '<'  +sort  +  并查集p 】

edges[N] 结构体数组: a,b,w  ,重载 '<' ,w从小到大排序
find 函数: 并查集
main : 初始化输入
	sort(edges,edges+m)排序m条边,取前n个即最小边权构造生成树
	for(int i = 1; i < m; i++) p[i] = i; //并查集初始化 ,边下标 i: 1 --> m 
	并查集合并:循环并成一个集合
按题目输出 cnt < n - 1  : impossible ; else : res


bool operator < (const Edge &W)const {  //重载' < '按权重排序 ,【记参数引用 +  2*const】 
		return w < W.w;
	}
res 最小生成树边权和
cnt 判是否无环(能构成生成树)

/*
用c++11新特性赋值 edges[i] = {a,b,w};
	Edge(){}
	Edge(int _a ,int _b , int _w) {
		a = _a;
		b = _b;
		w = _w;
	}
*/

#include<bits/stdc++.h>
using namespace std;


const int N = 100010;

int n,m;
int p[N];  //并查集

struct Edge 
{
	int a,b,w;
	
	bool operator < (const Edge &W)const {  //重载' < '按权重排序 ,【记参数引用 +  2*const】 
		return w < W.w;
	}
} edges[N];

int find(int x) { //并查集
	if(p[x] != x ) p[x] = find(p[x]); 
	return p[x];
}

int main() {
	scanf("%d%d",&n,&m);

	for(int i = 0; i < n; i++) {
		int a,b,w;
		scanf("%d%d%d",&a,&b,&w);
		edges[i] = {a,b,w};
		//edges[i] = Edge(a,b,w);
	}

	sort(edges,edges + m);  //重载小于号,按权w从小到大排序

	for(int i = 1; i < m; i++) p[i] = i; //并查集初始化

	int res = 0,cnt = 0;
	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); //找到集合的根【编号】

		if(a != b) { //合并集合操作  【所有点都加入集合,刚好n个点,n-1条边】
			p[a] = b;
			res += w;
			cnt ++ ;
		}
	}
	
	if(cnt < n - 1) puts("impossible");
	else printf("%d\n",res); 
	
	return 0;
}

二分图

二分图 <==> 当且仅当图中不含有奇数环 [顺时针编号:1 2 1 2 1 2 1 2 1 2]
证明:充分性 --> 必要性(能反推) <–
遍历点,二分编号1 ,2 所有1连通,所有2连通 , 分开染色
由于没有存在奇数环,所有染色过程当中一定是没有矛盾的

860. 染色法判定二分图O(n+m)

给定一个n个点m条边的无向图,图中可能存在重边和自环。
请你判断这个图是否是二分图。
输入格式
第一行包含两个整数n和m。
接下来m行,每行包含两个整数u和v,表示点u和点v之间存在一条边。
输出格式
如果给定图是二分图,则输出“Yes”,否则输出“No”。
数据范围
1≤n,m≤105
输入样例:
4 4
1 3
1 4
2 3
2 4
输出样例:
Yes

思路:判断是不是二分图 (在染色中出现过矛盾,不是奇数环 ,不是二分图 ;反之能完整染色一遍就是二分图
for(int i = 1;i <= n;i++)
if(i未染色)
dfs(i,1) //染成1号颜色

简记:
main: 邻接表初始化
dfs(u,c):
	链表循环
	dfs(j,3 - c)    【1+2 = 3,互为减数 , 1 2 1 2 1 2 循环 】



#include<cstdio>
#include<cstring> 
const int N = 200010;

int n,m;
int h[N],e[N],ne[N],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;//当前节点的颜色是c
	
	for(int i = h[u];i != -1;i = ne[i])
	{
		int j = e[i];
		if(!color[j])  
		{	 //如果dfs成功就返回 true ,!dfs就不会执行,否则就失败返回false 
			if(!dfs(j,3 - c)) return false;  //c已用,改用3-c染成另一种颜色 【不断循环 1 2 1 2】  
		}
		else if(color[j] == c) return false;   //一条边的两边不能是一样的颜色 ,是就有矛盾,return false 
	}
	
	return true; //全都染色跳出循环 
}

int main()
{
	scanf("%d%d",&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)) // 定义若dfs : false
			{
				flag = false; //没有矛盾发生 
				break;
			 } 
		}
		
	if(flag)puts("Yes");	
	else puts("No");
	
	return 0;
}

861.二分图最大匹配(匈牙利算法)

想提升372.棋盘覆盖

题目描述
给定一个二分图,其中左半部包含 n1 个点(编号 1~n1),右半部包含 n2 个点(编号 1~n2),二分图共包含 m 条边。
数据保证任意一条边的两个端点都不可能在同一部分中。
请你求出二分图的最大匹配数。
二分图的匹配:给定一个二分图 G,在 G 的一个子图 M 中,M 的边集 {E} 中的任意两条边都不依附于同一个顶点,则称 M 是一个匹配。
二分图的最大匹配:所有匹配中包含边数最多的一组匹配被称为二分图的最大匹配,其边数即为最大匹配数
输入格式
第一行包含三个整数 n1、 n2 和 m。
接下来 m 行,每行包含两个整数 u 和 v,表示左半部点集中的点 u 和右半部点集中的点 v 之间存在一条边。
输出格式
输出一个整数,表示二分图的最大匹配数。
数据范围
1≤n1,n2≤500,
1≤u≤n1 ,
1≤v≤n2,
1≤m≤105
输入样例
2 2 4
1 1
1 2
2 1
2 2
输出样例
2

思路:
给定一个二分图 ,求匹配数量最多的匹配
月老:男女牵线(不脚踏两只船) ,最多可以牵多少条线 男看上的姑娘【可选连接匹配】
若两个男生喜欢同一个姑娘,这个时候冲突,就先看姑娘喜欢那个男生,若有【没有找到匹配的男生,有已经女生的喜欢,则此女生可以换链】
只有全部的链都不满足男的需求,【或已经相互选择】,才放弃
【最后悔的是错过一件事,而不是做错一件事】
扩展难题:372.棋盘覆盖

简记:

main:邻接表 add
find 匈牙利算法:  第x头结点的链表遍历
		if(match[j] == 0 || find(match[j])) //如果这个妹子还没有匹配任何男生 || 虽然匹配这个男生,但是这个男生可以找的下家(有多个匹配match[j]指向的男生),妹子就可以选择空出来,配对当前男生
		{
				match[j] = x;
				return true;
			}


#include<cstring>
#include<algorithm>
#include<bits/stdc++.h>
const int N = 510, M = 100010;  //虽然是无向图,但是只会找左边指向右边 【my_选择性映射】,存一边即可 M只需100010 

int n1,n2,m;
int h[N],e[M],ne[M],idx; //数组越界会发生各种错误 , 如TLE 超时
int match[N];//为匹配初始0【不用初始了】  右边的点对应的(女对应的男)
bool st[N];//标记遍历确定【点是否使用】状态,初始false ,每轮判断用,每轮重新版变false 

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(match[j] == 0 || find(match[j])) //如果这个妹子还没有匹配任何男生 || 虽然匹配这个男生,但是这个男生可以找的下家(有多个匹配match[j]指向的男生),妹子就可以选择空出来,配对当前男生 
			{
				match[j] = x; //女生j配对男生x
				return true;
			}
		}
	}
	
	return false;
}

int main()
{
	
	scanf("%d%d%d",&n1,&n2,&m);
	
	memset(h,-1,sizeof h);//初始 
	
	while(m --)//录入数据 , 只要单边连,即为匹配,add一个方向即可 
	{
		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); //每轮判断状态回溯,match为总体  
		if(find(i)) res ++;
	}
	
	printf("%d\n",res);  //别加 & !!! 
	
	return 0;
}

845.八数码 【最小步数(bfs) 】 【课后习题】

在一个3×3的网格中,1~8这8个数字和一个“X”恰好不重不漏地分布在这3×3的网格中。
例如:
1 2 3
X 4 6
7 5 8
在游戏过程中,可以把“X”与其上、下、左、右四个方向之一的数字交换(如果存在)。
我们的目的是通过交换,使得网格变为如下排列(称为正确排列):
1 2 3
4 5 6
7 8 X
例如,示例中图形就可以通过让“X”先后与右、下、右三个方向的数字交换成功得到正确排列。
交换过程如下:
①1 2 3 ② 1 2 3 ③1 2 3 ④ 1 2 3
X 4 6 4 X 6 4 5 6 4 5 6
7 5 8 7 5 8 7 X 8 7 8 X
现在,给你一个初始网格,请你求出得到正确排列至少需要进行多少次交换。
输入格式
输入占一行,将3×3的初始网格描绘出来。
例如,如果初始网格如下所示:
1 2 3
x 4 6
7 5 8
则输入为:1 2 3 x 4 6 7 5 8
输出格式
输出占一行,包含一个整数,表示最少交换次数。
如果不存在解决方案,则输出”-1”。
输入样例:
2 3 4 1 5 x 7 6 8
输出样例
19

此题难点:①状态表示复杂【下一步能变成哪些状态】 ②BFS 队列 dist数组记录每个结点的距离【如何运用下标 存3 * 3】
思路: 法一:字符串"9个数" ,queue< string > unordored_map<string,int> dist [字母,位置]




#include<algorithm>
#include<unordored_map>
#include<queue>

int bfs(string start)
{
	string end = "12345678x";
	
	queue<string> q;
	unordored_map<string , int> d;  //到终点的距离 
	
	q.push(start);
	d[start] = 0;
	
	int dx[4] = {-1,0,1,0}, dy[4] = {0,1,0,-1};
	
	while(q.size())
	{
		string t = q.front(); // auto t    [c++11]
		q.pop();
		
		int distance = d[t];
		
		if(t == end) return distance;
		
		//状态转移 
		
		int k = t.find('x');   //algorithm 返回下标 
		int x = k / 3 , y = k % 3; //二维坐标求法 
		
		for(int i =  0;i < 4 ;i++)
		{
			int tx = x + dx[i] , ty = y + dy[i];
			if(tx >= 0 && tx < 3 && ty >= 0 && ty < 3 )
			{
				swap(t[k],t[3 * tx + ty]);  // 交换x与下一个状态【二维转一维 坐标】,判断  
				
				if(!d.count(t)) //还有没遍历过的  , 
				{
					d[t] = distance + 1;  //到终点的距离,step++ ,达到最终距离后结束 
					q.push(d);
				}
				
				swap(t[k],t[3 * tx + ty]); //恢复状态 ,找最优,所有解 
			}
		}
	}
	
	return -1;
}


int main()
{
	string start;
	for(int i = 0;i < 9;i++) //字符串读入 
	{
		char c;
		cin >> c;
		start += c;
	}
	
	cout << bfs(start) << endl;
	
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值