1. 明确概念:
子图:从原图中选中一些顶点和边组成的图,称为原图的子图。
生成子图:选中一些边和所有顶点组成的图,称为原图的生成子图。
生成树:如果生成子图恰好是一棵树,则称为生成树。
最小生成树:权值之和最小的生成树,则称为最小生成树。
2. Prim算法
图示+伪代码(摘自oi-wiki)
证明
从任意一个结点开始,将结点分成两类:已加入的,未加入的。
每次从未加入的结点中,找一个与已加入的结点之间边权最小值最小的结点。
然后将这个结点加入,并连上那条边权最小的边。
重复n-1次即可。
证明:还是说明在每一步,都存在一棵最小生成树包含已选边集。
基础:只有一个结点的时候,显然成立。
归纳:如果某一步成立,当前边集为F,属于T这棵 MST,接下来要加入边e。
如果e属于T,那么成立。
否则考虑T+e中环上另一条可以加入当前边集的边f。
首先,f的权值一定不小于e的权值,否则就会选择f而不是e了。
然后,f的权值一定不大于e的权值,否则T+e-f就是一棵更小的生成树了。
因此,e和f的权值相等,T+e-f也是一棵最小生成树,且包含了F。
存图
链式前向星存图,建双边。
int head[MAXN], cnt;
struct node {
int v, w, next;
}e[MAXM];
void add(int u, int v, int w) {
e[cnt].v = v;
e[cnt].w = w;
e[cnt].next = head[u];
head[u] = cnt++;
}
void adde(int u, int v, int w) {
add(u, v, w);
add(v, u, w);
}
void init() {
memset(head, -1, sizeof(head));
cnt = 0;
}
Prim
变量定义
lowcost[i]:从i点到生成树的最短距离
vis[i]:i点已经访问过,即已经加入U战队
res:用来存最小生成树的边权之和
主要思路
首先初始化lowcost[2~n]的值为INF,将起点加入U战队,标为已访问。
接着遍历起点的邻接点,更新lowcost的值为边权。
再进入循环,循环n-1次,因为最小生成树是n-1条边。
找最小--> 加入U战队 --> 更新邻接点lowcost[]值。
bool vis[MAXN];
int lowcost[MAXN];
int res = 0;
int n, m;
int prim(int u) {
for (int i = 2; i <= n; i++) {
lowcost[i] = INF;
}
vis[u] = 1;
for (int i = head[u]; ~i; i = e[i].next) {
int v = e[i].v;
lowcost[v] = min(lowcost[v], e[i].w);
}
for (int i = 1; i < n; i++) {
int mn = INF;
int v = -1;
for (int j = 1; j <= n; j++) {
if (!vis[j] && lowcost[j] < mn) {
mn = lowcost[j];
v = j;
}
}
if (v == -1) return 0;
vis[v] = 1;
res += mn;
for (int j = head[v]; ~j; j = e[j].next) {
int x = e[j].v;
if (!vis[j] && lowcost[x] > e[j].w) {
lowcost[x] = e[j].w;
}
}
}
return res;
}
main函数
按题意编写。
int main() {
init();
scanf("%d%d", &n, &m);
int u, v, w;
for (int i = 1; i <= m; i++) {
scanf("%d%d%d", &u, &v, &w);
adde(u, v, w);
}
int ans = prim(1);
if (ans == 0) {
printf("orz\n");
}
else {
printf("%d\n", ans);
}
return 0;
}
时间复杂度
Prim算法的时间复杂度为 O(n²) ,其中n为顶点数。
3. Kruskal算法
图示+伪代码(摘自oi-wiki)
证明
思路很简单,为了造出一棵最小生成树,我们从最小边权的边开始,按边权从小到大依次加入,如果某次加边产生了环,就扔掉这条边,直到加入了 n-1 条边,即形成了一棵树。
证明:使用归纳法,证明任何时候 K 算法选择的边集都被某棵 MST 所包含。
基础:对于算法刚开始时,显然成立(最小生成树存在)。
归纳:假设某时刻成立,当前边集为 F,令 T 为这棵 MST,考虑下一条加入的边 e。
如果 e 属于 T,那么成立。
否则,T+e 一定存在一个环,考虑这个环上不属于 F 的另一条边 f(一定只有一条)。
首先,f 的权值一定不会比 e 小,不然 f 会在 e 之前被选取。
然后,f 的权值一定不会比 e 大,不然 T+e-f 就是一棵比 T 还优的生成树了。
所以,T+e-f 包含了 F,并且也是一棵最小生成树,归纳成立。
前置知识——并查集
这里贴一份并查集模板代码(洛谷P3367【模板】并查集)
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1e4 + 5;
int fa[MAXN];
int n, m;
void init() {
for (int i = 0; i < MAXN; i++) {
fa[i] = i;
}
}
int find(int x) {
if (fa[x] == x) return x;
return fa[x] = find(fa[x]);
}
void merge(int x, int y) {
x = find(x);
y = find(y);
fa[x] = y;
}
bool ask(int x, int y) {
return find(x) == find(y);
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin >> n >> m;
init();
int x, y, z;
for (int i = 1; i <= m; i++) {
cin >> z >> x >> y;
if (z == 1) {
merge(x, y);
}
else {
if (ask(x, y)) {
cout << "Y" << endl;
}
else {
cout << "N" << endl;
}
}
}
return 0;
}
存图方式
这里我们定义一个边集数组(结构体),然后直接输入。
struct node {
int u, v, w;
}e[MAXM];
// 输入
for (int i = 1; i <= m; i++) {
scanf("%d%d%d", &e[i].u, &e[i].v, &e[i].w);
}
并查集处理
① 初始化
void init() {
for (int i = 1; i <= n; i++) {
fa[i] = i;
}
}
② 合并
int merge(int a, int b) {
int x = fa[a];
int y = fa[b];
if (x == y) return 0;
else {
for (int i = 1; i <= n; i++) {
if (fa[i] == y) {
fa[i] = x;
}
}
}
return 1;
}
当然,这种合并方式略显特殊,我们还可以采取我们熟悉的常规方式:
int find(int x) {
if (fa[x] == x) {
return x;
}
return fa[x] = find(fa[x]);
}
int merge(int a, int b) {
int x = find(a);
int y = find(b);
if (x == y) return 0;
fa[y] = x;
return 1;
}
Kruskal
变量定义
ans:最小生成树边权之和
cnt:最小生成树已包含的边数
主要思路
首先给边集数组进行排序,按照边权值从小到大排
遍历已排好的m条边,如果能合并,cnt++,ans+=e[i].w
bool cmp(node a, node b) {
return a.w < b.w;
}
int Kruskal() {
int ans = 0;
int cnt = 0;
sort(e + 1, e + m + 1, cmp);
for (int i = 1; i <= m; i++) {
if (merge(e[i].u, e[i].v)) {
ans += e[i].w;
cnt++;
if (cnt == n - 1) return ans;
}
}
return 0;
}
main函数
按题意编写即可。
int main() {
while (~scanf("%d", &n) && n) {
init();
scanf("%d", &m);
for (int i = 1; i <= m; i++) {
scanf("%d%d%d", &e[i].u, &e[i].v, &e[i].w);
}
printf("%d", Kruskal());
}
return 0;
}
时间复杂度
Kruskal算法的时间复杂度为 O(ElogE) ,其中E为边数。
感谢各位神犇的阅读!祝生活愉快~