最小生成树
提示:例题中的注释是精华
算法分析
稠密图适用于朴素版Prim算法
稀疏图适用于堆优化版的Prim算法
== 用法:稠密图用朴素Prim算法稀疏图用Kruskal算法 ==
朴素Prim算法
内容
- dist[i] <- + ∞ \infty ∞
- for (i = 0; i < n; i ++)
- 找到集合外距离最近的点
- 用t更新其他点到
集合
的距离 - s[t] = true
由于最小生成树中没有环所以,有负权边也没关系
在一个集合可以通过判断是否有同一个父亲结点,用并查集判断
模板
时间复杂度是O( n 2 n^2 n2 + m)
int n; // n表示点数
int g[N][N]; // 邻接矩阵,存储所有边
int dist[N]; // 存储其他点到当前最小生成树的距离
bool st[N]; // 存储每个点是否已经在生成树中
// 如果图不连通,则返回INF(值是0x3f3f3f3f), 否则返回最小生成树的树边权重之和
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];
st[t] = true;
for (int j = 1; j <= n; j ++ ) dist[j] = min(dist[j], g[t][j]);
}
return res;
}
/*
prim算法是从顶点出发,将已经连通的顶点看作一个集合,每次将结点与集合连通的权值最小的边连接,然后将结点添加到集合中,当结点全部添加到集合中后就形成了最小生成树了
*/
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 510, INF = 0x3f3f3f3f;
int g[N][N], dist[N];
bool st[N];
int n, m;
int prime()
{
memset(dist, 0x3f, sizeof dist);
int res = 0;// 最小生成树中的权重之和
for (int i = 0; i < n; i ++)
{
int t = -1;// t表示集合外与集合相连的边最小的结点
for (int j = 1; j <= n; j ++)
if (!st[j] && (t == -1 || dist[j] < dist[t]))// 集合外的,第一次直接赋值,值更小的
t = j;
st[t] = true;// 加入集合
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]);// 更新与集合相连的最小值
}
return res;
}
int main()
{
cin >> n >> m;
memset(g, 0x3f, sizeof g);
for (int i = 0; i < m; i ++)
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
g[a][b] = g[b][a] = min (g[a][b], c);// 无向图要将两个方向的边都赋上权重
}
int res = prime();
if (res == INF) puts("impossible");
else printf("%d", res);
return 0;
}
Kruskal算法
内容
- 将所有边按权重从小到大排序O(m l o g m logm logm)
- 枚举每一条边a, b, 权重c
- if a, b不连通 将这条边加到集合中
模板
时间复杂度是O( m l o g n mlogn mlogn)
int n, m; // n是点数,m是边数
int p[N]; // 并查集的父节点数组
struct Edge // 存储边
{
int a, b, w;
bool operator< (const Edge &W)const
{
return w < W.w;
}
}edges[M];
int find(int x) // 并查集核心操作
{
if (p[x] != 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 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) return INF;
return res;
}
例题
AcWing 859. Kruskal算法求最小生成树
/*
Kruskal算法是从边出发构造集合的,首先将边按照权重进行排序,然后从小到大加入到图中,只要不够成环就是允许的
而是否构成环可以通过并查集来判断有相同的父节点就说明是连成环了
若最后节点全部填入到集合中就说明可以生成最小生成树
*/
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010, M = N * 2, INF = 0x3f3f3f3f;
int n, m;
int p[N];
struct Edge
{
int a, b, w;
}edges[M];// 无向图,要是双倍的边存储不然要SE,呜呜
int find (int x)// 并查集核心
{
if (x != p[x]) p[x] = find (p[x]);
return p[x];
}
bool cmp(Edge a, Edge b)// 排序用的比较函数
{
return a.w < b.w;
}
int Kruskal()
{
for (int i = 1; i <= n; i ++) p[i] = i; // 初始化祖宗结点
int res = 0, cnt = 0;// cnt 用来判断是否生成树,res用来求最小生成树权重之和
sort (edges, edges + m, cmp);
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) return INF; // cnt 记录的不是结点而是边,n个节点最小生成树要有n - 1 条边 cnt 不满足n - 1就未生成
return res;
}
int main ()
{
cin >> n >> m;
for (int i = 0; i < m; i ++)
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
edges[i] = {a, b, c};// 因为edge表示的是边的信息, 在用并查集维护是只需要查节点的祖宗结点对方向不做要求
}
int res = Kruskal();
if (res > INF / 2) puts("impossible");
else printf("%d\n", res);
return 0;
}
二分图
算法分析
二分图:可以将所有的点划分到左右两边使得集合内部没有边,集合内部没有边连接,而两边这两个集合有边连接,并不一定是奇数个节点哦
性质:当且仅当图中不含奇数边的环
染色法
一条边的两边一定要属于不同集合
由于图中不存在奇数环所以染色过程中一定没有矛盾
如果能用染色法完美地染一遍的一定是二分图,出现矛盾就不是
内容
- for (i = 1; i <= n; i ++)
- if (i 未染色)dfs(i,1)// 第一个是结点,第二个是染的颜色
模板
时间复杂度是O(n + m)
int n; // n表示点数
int h[N], e[M], ne[M], idx; // 邻接表存储图
int color[N]; // 表示每个点的颜色,-1表示未染色,0表示白色,1表示黑色
// 参数:u表示当前节点,c表示当前点的颜色
bool dfs(int u, int c)
{
color[u] = c;
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if (color[j] == -1)
{
if (!dfs(j, !c)) return false;
}
else if (color[j] == c) return false;
}
return true;
}
bool check()
{
memset(color, -1, sizeof color);
bool flag = true;
for (int i = 1; i <= n; i ++ )
if (color[i] == -1)
if (!dfs(i, 0))
{
flag = false;
break;
}
return flag;
}
/*
二分图就是将结点分为左右两边,两边的集合内没用边连接
这就要求一个节点上的换不能为奇数个
通过染色最后看一个节点上的颜色是否有冲突来判断是否是二分图
*/
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010, M = N * 2;// 无向图,边存两倍
int h[N], e[M], ne[M], idx = 0;// 稀疏图用邻接表存储
int col[N];// 用col数组是否等于零就能判断节点是否被染色
int n, m;
void add (int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}
// dfs遍历结点相邻的结点将其染色,返回的是是否发生冲突
bool dfs(int u, int c)// u是结点下标,c是被染的颜色
{
col[u] = c;
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];// 相邻结点
if (!col[j])//未染色
if(!dfs(j, 3 - c)) return false;// 就给它染色
if (col[j] == c) return false; // 发生冲突,也是BadEnd递归的最后一层
}
return true;// 相邻结点全部遍历玩,HappyEnd递归的最后一层
}
int main()
{
cin >> n >> m;
memset(h, -1, sizeof h);
for (int i = 0; i < m; i ++)
{
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 (!col[i])// 如果未染色
if(!dfs(i, 1)) flag = false;// 我们就将其染色
}
puts(flag ? "Yes" : "No");
return 0;
}
匈牙利算法
内容
给定一个二分图求它的最大匹配(匹配的是边的数量)
没有两条边是公用一个点的视为匹配成功
模板
时间复杂度是O(nm)
int n1, n2; // n1表示第一个集合中的点数,n2表示第二个集合中的点数
int h[N], e[M], ne[M], idx; // 邻接表存储所有边,匈牙利算法中只会用到从第一个集合指向第二个集合的边,所以这里只用存一个方向的边
int match[N]; // 存储第二个集合中的每个点当前匹配的第一个集合中的点是哪个
bool st[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;
}
// 求最大匹配数,依次枚举第一个集合中的每个点能否匹配第二个集合中的点
int res = 0;
for (int i = 1; i <= n1; i ++ )
{
memset(st, false, sizeof st);
if (find(i)) res ++ ;
}
/*
二分图的最大匹配,在匹配的时候只需要考虑从左边到右边考虑,因此存的是一个有向边
实例化为联谊现场,左边可能看上多个妹子,当后面也看上这个妹子是可以让上一个找备胎
由于上面的人可以重复考虑,因此这里st[]数组在每个男人找妹子是都要初始化为0,这样可以让他找已经匹配过的妹子,找到妹子后在赋值为1,让上一个男人不许在考虑你选的妹子
*/
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 510, M = 100010;
int h[N], e[M], ne[M], idx = 0;
bool st[N]; // 每轮将每个妹子都要考虑一遍,并且在本轮不能让给别人
int matched[N];// 妹子此时找到的男人
int n1, n2, m;// n1 -> 男人的数量, n2 -> 妹子的数量, m 男人心仪的妹子
void add (int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}
int find(int x)
{
for (int i = h[x]; ~i; i = ne[i])
{
int j = e[i];
if (!st[j])//这轮这个妹子没考虑过的话
{
st[j] = true;
if (matched[j] == 0 || find(matched[j]))// 这个妹子还没男友的话,或者她男友还有备胎的话(这里进行了递归 在下一层的时候由于本轮男子将他绿了(st[true]), 他之后找备胎了,hh)
{
matched[j] = x;// 横刀夺爱or一见钟情
return true;
}
}
}
return false;// 活该单身
}
int main()
{
memset(h, -1, sizeof h);
cin >> n1 >> n2 >> m;
for (int i = 0; i < m; i ++)
{
int a, b;
scanf("%d%d", &a, &b);
add(a, b);// 只考虑从左边找右边
}
int res = 0;// 匹配了多少对
for(int i = 1; i <= n1; i ++)// 询问每个人的意见
{
memset(st, 0, sizeof st);// 让每个妹子都能被考虑即使她有对象
if (find(i)) res ++;
}
printf("%d", res);
}
参考文献
学习自AcWing