文章目录
最小生成树
①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;
}