最小生成树
首先明白这几个概念:
树(Tree):如果一个无向连通图中不存在回路,则这种图称为树。
生成树 (Spanning Tree):无向连通图G的一个子图如果是一颗包含G的所有顶点的树,则该子图称为G的生成树。
生成树是连通图的极小连通子图。这里所谓极小是指:若在树中任意增加一条边,则将出现一条回路;若去掉一条边,将会使之变成非连通图。
1) 应用场景
设想有9个村庄,这些村庄构成如下图所示的地理位置,每个村庄的直线距离都不一样。若要在每个村庄间架设网络线缆,若要保证成本最小,则需要选择一条能够联通9个村庄,且长度最小的路线。
2) 最小生成树概念
- 一个带权值的图:网。所谓最小成本,就是用n-1条边把n个顶点连接起来,且连接起来的权值最小。
- 最小生成树不能有回路。
- 最小生成树可能是一个,也可能是多个。
最小生成树的算法一般有两种,分别为 Kruskal算法 和 Prim算法
Kruskal算法
知识点:并查集:数据结构——并查集
克鲁斯卡尔算法的基本思想:始终选择当前可用(所选的边一定不会构成回路)的最小权植边。
具体步骤:
1. 建立一个升序优先级队列(或者用容器进行排序)。
2. 每次选择堆顶元素(当前可用权值最小的边)。
3. 若该边的两个顶点落在不同的连通分量上,选择这条边;若这条边的两个顶点落到同一连通分量上,舍弃这条边。反复执行2,3,直到所有的都在同一连通分量上。
几个例题:
板子:
#include <iostream> #include <stdio.h> #include <queue> using namespace std; int N, M; int pre[5001]; class Node { public: int from, to, weight; Node(int f, int t, int w) { from = f; to = t; weight = w; } bool operator<(Node m)const { return m.weight < weight; } }; int find(int x) { if (x == pre[x])return pre[x]; else return pre[x] = find(pre[x]); } int kurskal(priority_queue<Node>&q, int cnt) { int res = 0; while (!q.empty() && cnt > 1) { Node m = q.top(); q.pop(); int fx = find(m.from), fy = find(m.to); if (fx != fy) { pre[fx] = fy; cnt--; res += m.weight; } } if (cnt == 1) return res; else return -1; } int main() { priority_queue<Node>q; //n个节点,m条边 cin >> N >> M; for (int i = 0; i <= 5000; i++) { pre[i] = i; } int from, to, weight; for (int i = 0; i < M; i++) { scanf("%d%d%d", &from, &to, &weight); q.push(Node(from, to, weight)); } int res = kurskal(q, N); if (res == -1) { //无解 } else { //cout << res << endl; } return 0; }
HDU-1863:http://acm.hdu.edu.cn/showproblem.php?pid=1863
#include <iostream> #include <queue> using namespace std; int pre[105]; //并查集 class edge { public: int from, to, weight; edge(int f, int t, int w) { from = f; to = t; weight = w; } bool operator < (edge n) const { return n.weight < weight; } }; int M, N; int Find(int x) { if (pre[x] == x)return x; return pre[x] = Find(pre[x]); } int kruskal(priority_queue<edge>& q) { for (int i = 1; i <= M; i++) pre[i] = i; int res = 0; while(!q.empty()){ edge e = q.top(); q.pop(); int fx = Find(e.from); int fy = Find(e.to); if (fx != fy) { pre[fx] = fy; M--; res += e.weight; } } if (M != 1) return -1; return res; } int main() { while (cin >> N >> M) { int c1, c2, c3; if (N == 0) break; priority_queue<edge> v; for (int i = 0; i < N; i++) { cin >> c1 >> c2 >> c3; v.push(edge(c1, c2, c3)); } int res = kruskal(v); if (res == -1) cout << "?" << endl; else cout << res << endl; } return 0; }
HDU-1879:http://acm.hdu.edu.cn/showproblem.php?pid=1879
#include <iostream> #include <vector> #include <algorithm> using namespace std; int pre[110]; int N; class edge { public: int from, to, weight; edge(int f, int t, int w) { from = f; to = t; weight = w; } }; bool cmp(edge e1, edge e2) { return e1.weight < e2.weight; } int Find(int x) { if (pre[x] == x)return x; return pre[x] = Find(pre[x]); } void Kruskal(vector<edge>v) { int res = 0, cnt = N; for (int i = 0; i < v.size() && cnt > 1; i++) { int fx = Find(v[i].from); int fy = Find(v[i].to); if (fx != fy) { pre[fx] = fy; cnt--; res += v[i].weight; } } cout << res << endl; } int main() { while (scanf("%d", &N)) { if (N == 0)break; vector<edge>v; int n = N * (N - 1) / 2; for (int i = 1; i <= N; i++)pre[i] = i; while (n--) { int from, to, weight, flag; scanf("%d%d%d%d", &from, &to, &weight, &flag); if (flag == 1) { int fx = Find(from); int fy = Find(to); if (fx != fy) { pre[fx] = fy; N--; } } else v.push_back(edge(from, to, weight)); } sort(v.begin(), v.end(), cmp); Kruskal(v); } return 0; }
P3366 : https://www.luogu.org/problemnew/show/P3366
#include <iostream> #include <stdio.h> #include <queue> using namespace std; int N, M; int pre[5001]; class Map { public: int from, to, weight; Map(int f,int t,int w) { from = f; to = t; weight = w; } bool operator<(Map m)const { return m.weight < weight; } }; void initialize() { for (int i = 0; i <= 5000; i++) { pre[i] = i; } } int find(int x) { if (x == pre[x])return pre[x]; else return pre[x] = find(pre[x]); } int main() { priority_queue<Map>q; cin >> N >> M; initialize(); int c1, c2, c3; for (int i = 0; i < M; i++) { scanf("%d%d%d",&c1,&c2,&c3); q.push(Map(c1, c2, c3)); } int res = 0; while (!q.empty()) { Map m = q.top(); q.pop(); int fx = find(m.from), fy = find(m.to); if (fx != fy) { pre[fx] = fy; N--; res += m.weight; } } if (N == 1) { cout << res << endl; } else { cout << "orz" << endl; } return 0; }
Prim算法
Prim算法思想:首先将图的点分为两部分,一种是访问过的u(第一条边任选),一种是没有访问过的v
1: 每次找u到v的权值最小的边。
2: 然后将这条边中的v中的顶点添加到u中,直到v中边的个数=顶点数-1
板子:
#include <iostream> #include <stdio.h> #include <queue> using namespace std; class Node { public: int to, weight; Node* next ; void push(int to, int weight) { Node* s = new Node; s->to = to; s->weight = weight; s->next = next; next = s; } bool operator<(Node n)const { return n.weight < weight; } }*head; int n, m; bool fuck[5005]; int lowcost[5005]; const int inf = 1 << 30; priority_queue<Node>q; void initiliaze() { fuck[1] = true; for (int i = 1; i <= n; i++) { lowcost[i] = inf; } Node* p = head[1].next; while (p) { lowcost[p->to] = p->weight; q.push(*p); p = p->next; } } int prim() { int res = 0, cnt = n; while (!q.empty() && cnt > 1) { Node s = q.top(); q.pop(); if (!fuck[s.to]) { fuck[s.to] = true; cnt--; res += s.weight; Node* p = head[s.to].next; while (p) { if (!fuck[p->to]) { q.push(*p); if (p->weight < lowcost[p->to]) lowcost[p->to] = p->weight; } p = p->next; } } } if (cnt == 1)return res; else return - 1; } int main() { cin >> n >> m; head = new Node[n + 1]; for (int i = 1; i <= n; i++) { head[i].next = NULL; } for (int i = 0; i < m; i++) { int from, to, weight; scanf("%d%d%d", &from, &to, &weight); head[from].push( to, weight); head[to].push( from, weight); } initiliaze(); int res = prim(); if (res == -1)cout << "orz" << endl; else cout << res << endl; return 0; }
图示步骤:
建立一个dis数组,初始值为节点1(任意一个都可以)到各点的值,规定到自己是0,到不了的是inf。
找到其中权值最小的,1→4
将dis[4]赋值为0,标记为已访问过,同时借助4节点更新dis数组。
找到其中权值最小,4→3
后面的操作都是一样的了。
最后整个dis数组都是0了,最小生成树也就出来了,如果dis数组中还有 inf 的话,说明这不是一个连通图。
HDU-1863:http://acm.hdu.edu.cn/showproblem.php?pid=1863
用邻接矩阵实现的,inf表示到不了,0表示已经访问过的。
#include <iostream> #include<cstring> #include<algorithm> #define inf INT_MAX using namespace std; int N, M; int map[106][105]; int dis[105]; int prim(){ int min, v, res = 0; dis[1] = 0; for (int i = 2; i <= M; i++){ dis[i] = map[1][i]; } for (int i = 2; i <= M; i++){ v = 0; for (int j = 2; j <= M; j++){ //到不了的,访问过的不进行比较 if (dis[j] != inf && dis[j] != 0 && (v == 0 || dis[j] < dis[v])) { v = j; } } if (v == 0)//没找够M-1条线,就没路了 return -1; res += dis[v]; dis[v] = 0; for (int j = 2; j <= M; j++){ if (map[v][j] < dis[j]) { dis[j] = map[v][j]; } } } return res; } int main(){ int i, a, b, c; while (cin >> N >> M) { if (N == 0)break; for (int i = 1; i <= M; i++) { for (int j = 1; j <= M; j++) { map[i][j] = inf; } } for (i = 1; i <= N; i++){ cin >> a >> b >> c; map[a][b] = map[b][a] = c; } int ans = prim(); if (ans == -1)cout << "?" << endl; else cout << ans << endl; } return 0; }
两者区别:
点多边少可以用Kruskal,因为Kruskal算法每次查找最短的边。 点少边多可以用Prim,因为它是每次加一个顶点。
prim算法适合稠密图,其时间复杂度为O(n^2),其时间复杂度与边得数目无关,而kruskal算法的时间复杂度为O(eloge)跟边的数目有关,适合稀疏图。