3.3 搜索与图论 | Prim、Kruskal、染色法、匈牙利算法
这是我的一个算法网课学习记录,道阻且长,好好努力
3.3.1 最小生成树
稠密图一般使用朴素的 Prim 算法,稀疏图一般使用 Kruskal 算法
3.3.1.1 普利姆 Prim 算法
Prim 算法用于处理稠密图(m ≈ n ^ 2)的最小生成树问题,与 Dijkstra 算法类似,Prim 算法主要通过贪心策略从某个点每一次找到边权最小的边(到集合中点最小的边作为距离),并将这条边的另一个顶点加入到最小生成树的点集之中,重复n次,直到点集中加入所有顶点。
Prim算法用于维护一个最小生成树的顶点集。值得注意的是,与 Dijkstra 算法相比,在更新dist
数组的时候是d[j]
与g[t][j]
进行比较,而不再是d[j]
与d[j] + g[t][j]
相比较。(维护的是点到点集的距离)
-
朴素版 Prim
时间复杂度是
O(n^2)
例题:AcWing 858. Prim算法求最小生成树(Prim算法)
给定一个 n 个点 m 条边的无向图,图中可能存在重边和自环,边权可能为负数。
求最小生成树的树边权重之和,如果最小生成树不存在则输出 impossible。
给定一张边带权的无向图 G=(V,E),其中 V 表示图中点的集合,E 表示图中边的集合,n=|V|,m=|E|。
由 V 中的全部 n 个顶点和 E 中 n−1 条边构成的无向连通子图被称为 G 的一棵生成树,其中边的权值之和最小的生成树被称为无向图 G 的最小生成树。
#include <cstring> #include <iostream> #include <algorithm> using namespace std; const int N = 510, INF = 0x3f3f3f3f; int n, m; int g[N][N]; int dist[N]; // 维护点距离点集的最短距离 bool 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])) 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]); // 累加后再更新,避免负环影响 st[t] = true; // 更新t点状态 进入最小生成树点集中 } 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; }
-
堆优化版 Prim
时间复杂度是
O(mlogn)
与 Dijkstra 的堆优化很相似。用的很少
3.3.1.2 克鲁斯卡尔 Kruskal 算法
时间复杂度是O(mlogm)
带权图的最小生成树,就是原图中边的权值最小的生成树 ,所谓最小是指边的权值之和小于或者等于其它生成树的边的权值之和。
基本思想 :按照权值从小到大的顺序选择 n-1 条边,并保证这 n-1 条边不构成回路
步骤:
首先,将所有边按权重从小到大排序(算法时间复杂度瓶颈,O(mlogm)
)
然后,枚举每条边a,b,权重c,如果当前 a , b 不在一个集合中,则将该边加入集合中(并查集)
例题:AcWing 859. Kruskal算法求最小生成树
给定一个 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 = 200010;
int n, m;
int p[N];
struct Edge
{
int a, b, w;
bool operator< (const Edge &W) 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 < m; i ++ )
{
int a, b, w;
scanf("%d%d%d", &a, &b, &w);
edges[i] = {a, b, w};
}
sort(edges, edges + m);
for (int i = 1; i <= n; 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)
{
p[a] = b;
res += w;
cnt ++ ;
}
}
if (cnt < n -1) puts("impossible");
else printf("%d\n", res);
return 0;
}
3.3.2 二分图
3.3.2.1 染色法
时间复杂度是O(m+n)
判断一个图是不是二分图
经典性质:一个图是二分图,当且仅当图中不含奇数环
由于图中不含奇数环,因此整个染色过程是一定不存在矛盾的
首先随意选取一个未染色的点进行染色,然后尝试将其相邻的点染成相反的颜色。如果邻接点已经被染色并且现有的染色与它应该被染的颜色不同,那么就说明不是二分图。而如果全部顺利染色完毕,则说明是二分图。染色结束后的情况(记录在数组中)便将图中的所有节点分为了两个部分,即为二分图的两个点集。
例题:ACwing 860 - 染色法判断二分图(染色法)
给定一个n个点m条边的无向图,图中可能存在重边和自环。
请你判断这个图是否是二分图
#include <iostream>
#include <cstring>
using namespace std;
const int N = 100010, M = 200010;
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; // 将该点染成c色 这里c有1与2两种
for (int i = h[u]; i != -1; i = ne[i]) // 使用邻接表遍历边集
{
int j = e[i];
if (!color[j])
{
if (!dfs(j, 3 - c)) return false;
}
else if (color[j] == c) 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]) // 如果没有被染过则dfs该点,将这个连通块全部染色并判断
{
if (!dfs(i, 1))
{
flag = false;
break;
}
}
if (flag) puts("Yes");
else puts("No");
return 0;
}
}
3.3.2.2 匈牙利算法
时间复杂度是O(mn)
,实际运行时间一般远小于O(mn)
月老红娘匹配hhh 算法学习笔记(5):匈牙利算法 - 知乎 (zhihu.com)
例题:ACwing 861 - 二分图的最大匹配(匈牙利算法)
给定一个二分图,其中左半部包含n1个点(编号1 - n1),右半部包含n2个点(编号1 - n2),二分图共包含m条边。
数据保证任意一条边的两个端点都不可能在同一部分中。
请你求出二分图的最大匹配数。
二分图的匹配:给定一个二分图G,在G的一个子图M中,M的边集{E}中的任意两条边都不依附于同一个顶点,则称M是一个匹配。
二分图的最大匹配:所有匹配中包含边数最多的一组匹配被称为二分图的最大匹配,其边数即为最大匹配数。
#include <cstring>
#include <iostream>
using namespace std;
const int N = 510, M = 100010;
int n1, n2, m;
int h[N], e[M], ne[M], idx; // 邻接表
int match[N]; // 对应的点
bool st[N]; // 避免重复
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 或者说在已经match中能找到下一个可以腾出来
{
match[j] = x;
return true;
}
}
}
return false;
}
int main()
{
scanf("%d%d%d", &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 ++ ) // 对n1中每一个点进行匹配
{
memset(st, false, sizeof st);
if (find(i)) res ++ ;
}
printf("%d\n", res);
return 0;
}