拓扑排序 详解 + 并查集 详解 + 最小生成树(MST)详解 【普利姆算法 + 优先队列优化 & 克鲁斯卡尔算法】
本人QQ :2319411771 邮箱 : cyb19950118@163.com
若您发现本文有什么错误,请联系我,我会及时改正的,谢谢您的合作!
本文为原创文章,转载请注明出处
本文链接 :http://www.cnblogs.com/Yan-C/p/3943940.html 。
哎呀,好久了啊,想写这篇博文好久了,但是因为懒的原因 一直迟迟没动手啊。
今天,终于在长久的懒惰下,突然来了那么一点热度。把这篇博文写一下。
本文分为以下几个部分 :
1、 拓扑排序
2、 并查集
3、 普利姆算法 & 优先队列优化
4、 克鲁斯卡尔算法
前情提要 : 本文的存图方式 只有两种 : 邻接矩阵 or 前向星。
1、 拓扑排序
我们起床穿裤子和鞋子时,相信大部分人的顺序是这样的,先穿上内裤,然后再穿上裤子,再穿上袜子,然后才是鞋子。 那么,我们把这些步骤分解:
(1)穿内裤
(2)穿裤子
(3)穿袜子
(4)穿鞋子
我们把这四个步骤,按照上述的顺序 给排一下,这就是所谓的拓扑排序。
当然这个排序的顺序是唯一的,如果你先进行(2)然后(1)(3)(4),哦,不,你不是超人,请不要这样做, 又假如你按照(1)(2)(4)(3), 那显然也是不行的。
拓扑排序 也可以描述一个暑假写作业的过程 : 语文作业,数学作业,英语作业,生物作业,化学作业,物理作业。
(1) 语文
(2) 数学
(3) 英语
(4) 生物
(5) 化学
(6) 物理
你可以是(1)(2)(3)(4)(5)(6),也可以是(6)(5)(4)(3)(2)(1),再者英语老师比较凶,那么可以是(3)(1)(2)(4)(5)(6)。等等其他的排序方式。
那么这个排序又是不唯一的。
因此 拓扑排序可能是唯一的又有可能是不唯一的。
就像 3个篮球队进行比赛。 编号分别为 1 , 2 , 3。
1打赢了2
2打赢了3
3打赢了1。 问谁是最后的冠军。 各一胜一负你问我谁是冠军 ,这不是扯蛋嘛。 So,这是不能判断谁是冠军的, 因为这个事件存在一个 环,互相牵制,进行排序是不行产生结果的。
如果这样 :
1打赢了2
3打赢了2
那么最后的冠军可能是不确定的,因为你不知道1和3 谁强。 所以只能是 1,3并列了,你如果喜欢大数在前 那就是3 1 2,反之,就是1 3 2了。
拓扑排序其实就是这个样子。
前面大篇幅的扯犊子,主要是介绍什么是拓扑排序。 那么我们要讨论一下,怎么样进行拓扑排序呢? 哎,这个问题好!
插播 :
我们再次的从 1 2 3 这三支队伍的冠军争夺赛说起。
1打赢了2 因为2输了一场比赛,所以要给2做一标记。因此2号的菊花上就出现了一杆长枪。 我们称这个标记为 入度 那么2的入度就是 1了。
3打赢了2 因为2又输了一场比赛,又是一杆长枪啊。为什么受伤的总是2。 那么2的入度 就++了 变成了2。
好了 这就是 什么是 入度 了。 如果你还不是很懂入度是什么。那我告诉你,入度 在这里就是2号被打败了几次。
那我们 就要 进入正题了。
拓扑排序 :
由AOV网构造拓扑序列的拓扑排序算法主要是循环执行以下两步,直到不存在入度为0的顶点为止。
循环结束后,若输出的顶点数小于网中的顶点数,则输出“有回路”信息,否则输出的顶点序列就是一种拓扑序列。 (摘自 : 百度百科)
我们继续 以题来进行讲解和理解的加深。
1 Description 2 有N个比赛队(1<=N<=500),编号依次为1,2,3,。。。。,N进行比赛,比赛结束后,裁判委员会要将所有参赛队伍从前往后依次排名,但现在裁判委员会不能直接获得每个队的比赛成绩,只知道每场比赛的结果,即P1赢P2,用P1,P2表示,排名时P1在P2之前。现在请你编程序确定排名。 3 4 5 Input 6 输入有若干组,每组中的第一行为二个数N(1<=N<=500),M;其中N表示队伍的个数,M表示接着有M行的输入数据。接下来的M行数据中,每行也有两个整数P1,P2表示即P1队赢了P2队。 7 8 9 Output 10 给出一个符合要求的排名。输出时队伍号之间有空格,最后一名后面没有空格。 11 12 其他说明:符合条件的排名可能不是唯一的,此时要求输出时编号小的队伍在前;输入数据保证是正确的,即输入数据确保一定能有一个符合要求的排名。 13 14 15 Sample Input 16 17 4 3 18 1 2 19 2 3 20 4 3 21 22 23 24 Sample Output 25 26 1 2 4 3
题目链接:在这
因为数据较小,我们可以使用邻接矩阵进行存储。 这是第一种方法。
题解在这 :
1 #include <cstdio> 2 #include <cstring> 3 #include <iostream> 4 #include <algorithm> 5 #include <queue> 6 #define LL long long 7 8 const int INF = 1e9+7; 9 const int VM = 503;// 点的个数 10 11 bool G[VM][VM];//图 12 int deg[VM];//各个顶点的入度 计数 13 14 void toposort(int n) {//拓扑排序 15 int k = 0; 16 17 for (int i = 1; i <= n; i++) {//共进行|G.V|次操作 18 for (int j = 1; j <= n; j++) {//遍历所有的顶点 找入度为0的 19 if (deg[j] == 0) {//找到 20 printf("%d%c", j, i == n ? '\n' : ' ');//输出 21 deg[j]--;//去掉这个点 让deg[j] = -1; 22 k = j;//记录这个点 23 break;//跳出循环 24 } 25 } 26 for (int j = 1; j <= n; j++)//遍历所有的点 27 if (G[k][j] == true) {//找被此点打败过的点 28 G[k][j] = false;//标记为找到过 29 deg[j]--;//让这个点的入度-1 30 } 31 } 32 } 33 34 int main() { 35 int n, m; 36 37 while (scanf("%d %d", &n, &m) == 2) {//多组输入, 获取n, m 38 memset(G, 0, sizeof(G));//初始化 39 memset(deg, 0, sizeof(deg));//初始化 40 while (m--) { 41 int u, v; 42 scanf("%d %d", &u, &v);//获取 u,v u打败过v 43 if (G[u][v] == false) {//防止重边 如果被同一个对手打败多次,也太伤v的心了 44 G[u][v] = true;//标记为真 45 deg[v]++;//v的入度++ 一杆长枪入洞了。 46 } 47 } 48 toposort(n);//调用函数 49 } 50 return 0; 51 }
主函数 对数据的获取 和存图。
1 int main() { 2 int n, m; 3 4 while (scanf("%d %d", &n, &m) == 2) {//多组输入, 获取n, m 5 memset(G, 0, sizeof(G));//初始化 6 memset(deg, 0, sizeof(deg));//初始化 7 while (m--) { 8 int u, v; 9 scanf("%d %d", &u, &v);//获取 u,v u打败过v 10 if (G[u][v] == false) {//防止重边 如果被同一个对手打败多次,也太伤v的心了 11 G[u][v] = true;//标记为真 12 deg[v]++;//v的入度++ 一杆长枪入洞了。 13 } 14 } 15 toposort(n);//调用函数 16 } 17 return 0; 18 }
拓扑排序的函数 :
1 void toposort(int n) {//拓扑排序 2 int k = 0; 3 4 for (int i = 1; i <= n; i++) {//共进行|G.V|次操作 5 for (int j = 1; j <= n; j++) {//遍历所有的顶点 找入度为0的 6 if (deg[j] == 0) {//找到 7 printf("%d%c", j, i == n ? '\n' : ' ');//输出 8 deg[j]--;//去掉这个点 让deg[j] = -1; 9 k = j;//记录这个点 10 break;//跳出循环 11 } 12 } 13 for (int j = 1; j <= n; j++)//遍历所有的点 14 if (G[k][j] == true) {//找被此点打败过的点 15 G[k][j] = false;//标记为找到过 16 deg[j]--;//让这个点的入度-1 17 } 18 } 19 }
此算法的时间复杂度为 O(n * n) 复杂度挺高的呢。
那我们要想办法优化啊。
来了 , 第二种 时间复杂度为 O(V + E) 在这个算法中 我们用到了 前向星 和 优先队列。
1 #include <cstdio> 2 #include <cstring> 3 #include <iostream> 4 #include <algorithm> 5 #include <queue> 6 #define LL long long 7 8 using namespace std; 9 10 const int INF = 1e9+7; 11 const int VM = 503;// 点的个数 12 13 struct node {//前向星的结构体 14 int v;//输队编号 15 int next; 16 }; 17 node edge[VM * 4];//结构体数组 18 int head[VM];//头指针数组 19 int cnt;//下标 20 int deg[VM];//入度数组 21 22 void toposort(int n) { 23 priority_queue<int, vector<int>, greater<int> > que;//优先队列 24 25 for (int i = 1; i <= n; i++)//找所有点 26 if (deg[i] == 0) {//入度为 0 27 que.push(i);//加入队列 28 deg[i]--;//入度 变为 -1 29 } 30 int k = 1; 31 while (que.empty() == false) {//队列不为空 32 int u = que.top();//取出队首的数 33 que.pop();//删除 34 printf("%d%c", u, k++ == n ? '\n' : ' ');//输出 35 for (int i = head[u]; i != -1; i = edge[i].next) {//与该点相连的 36 node e = edge[i];//便于书写 37 deg[e.v]--;//点的入度 -1 38 if (deg[e.v] == 0)//若此点的 入度为 0 39 que.push(e.v);//放入队列 40 } 41 } 42 } 43 44 int main() { 45 int n, m; 46 int i; 47 48 while (scanf("%d %d", &n, &m) == 2) {//多组输入 ,获取n,m 49 memset(head, -1, sizeof(head));//初始化 50 memset(deg, 0, sizeof(deg));//初始化 51 cnt = 0;//初始化 52 while (m--) { 53 int u, v; 54 scanf("%d %d", &u, &v);//获取u,v 55 for (i = head[u]; i != -1; i = edge[i].next)//查找重边 56 if (edge[i].v == v)//输入重复数据 57 break;//不再储存 58 if (i == -1) {//若不是重复数据 59 deg[v]++;//加边 60 edge[cnt].v = v; 61 edge[cnt].next = head[u]; 62 head[u] = cnt++; 63 } 64 } 65 toposort(n);//调用函数 66 } 67 return 0; 68 }
所用到的数据结构 :
1 priority_queue<int, vector<int>, greater<int> > que;//优先队列 2 struct node {//前向星的结构体 3 int v;//输队编号 4 int next; 5 }; 6 node edge[VM * 4];//结构体数组 7 int head[VM];//头指针数组 8 int cnt;//下标
主函数对数据的获取和 图的存储
1 int main() { 2 int n, m; 3 int i; 4 5 while (scanf("%d %d", &n, &m) == 2) {//多组输入 ,获取n,m 6 memset(head, -1, sizeof(head));//初始化 7 memset(deg, 0, sizeof(deg));//初始化 8 cnt = 0;//初始化 9 while (m--) { 10 int u, v; 11 scanf("%d %d", &u, &v);//获取u,v 12 for (i = head[u]; i != -1; i = edge[i].next)//查找重边 13 if (edge[i].v == v)//输入重复数据 14 break;//不再储存 15 if (i == -1) {//若不是重复数据 16 deg[v]++;//加边 17 edge[cnt].v = v; 18 edge[cnt].next = head[u]; 19 head[u] = cnt++; 20 } 21 } 22 toposort(n);//调用函数 23 } 24 return 0; 25 }
拓扑排序函数
1 void toposort(int n) { 2 priority_queue<int, vector<int>, greater<int> > que;//优先队列 3 4 for (int i = 1; i <= n; i++)//找所有点 5 if (deg[i] == 0) {//入度为 0 6 que.push(i);//加入队列 7 deg[i]--;//入度 变为 -1 8 } 9 int k = 1; 10 while (que.empty() == false) {//队列不为空 11 int u = que.top();//取出队首的数 12 que.pop();//删除 13 printf("%d%c", u, k++ == n ? '\n' : ' ');//输出 14 for (int i = head[u]; i != -1; i = edge[i].next) {//与该点相连的 15 node e = edge[i];//便于书写 16 deg[e.v]--;//点的入度 -1 17 if (deg[e.v] == 0)//若此点的 入度为 0 18 que.push(e.v);//放入队列 19 } 20 } 21 }
拓扑排序 讲解 完毕。
2、并查集
并查集从字面上最起码可以看出是一个集合,而且是能并(合并吗?) 能查的集合。集合也就是分组,一组一组的数据,这一组就是一个集合嘛。
并查集是一种用来管理元素分组情况的数据结构。 并查集,并查集,那么他的功能肯定就是 并 和 查。
他可以高效的进行 :
并 合并元素a和元素b所在的组。
查 查询元素a和元素b是否属于同一组。
并查集可以进行合并 但是却不能进行分割。
并查集的结构 是 树形结构,但是他却不是二叉树,因为是树,所以必定有根节点,根节点就是这个集合,这个分组中最大的统领着。
对于并查集呢,主要是有两部分函数构成, 一个是union()函数 也就是我们所说的并(合并),另一个是find()函数 也就是所说的查函数。
对于并查集不会画图真的是好纠结。
对于并查集,大家看这个大牛的博客的讲解吧, 如果大家不想看的话,可以直接看下面的代码讲解,注释还是很清晰的。
http://www.cnblogs.com/cyjb/p/UnionFindSets.html
看完讲解 大家可以看一下这些题目加深一下。(脑子有点乱乱的,原谅我的“乱来”)。
我们对并查集的初始化
1 for (int i = 1; i <= n; i++) 2 par[i] = i;//这是初始化
这是find() 函数
1 int find(int x) {//查找函数 2 if (par[x] == x)//若本身就是根节点 ,那么return 3 return x; 4 return find(par[x]);//不是的话,继续查找 5 }
这是合并函数
1 void unite(int x, int y) {//合并函数 2 x = find(x);//查找x的根节点 3 y = find(y);//查找y的根节点 4 if (x == y)//若这两个数的结点是一样的,那么他们本来就是一个分组里的了,所以没有操作 5 return ; 6 par[x] = y;//不然的话,就让其中的一个点成为另一个点的根节点。 7 }
还有一个判断的 same函数
1 bool same(int x, int y) { 2 return find(x) == find(y); 3 }
same函数 下面的题解中都是 直接判断的,所以就把same这个函数直接放在了里面,就这样 same 函数 被我隐藏了。
这上面的 查找函数和合并函数 都是未经优化的,是比较原始的,下面我们用它做一道题。
题目链接在这 我就是题目链接
1 #include <cstdio> 2 #include <cstring> 3 #include <iostream> 4 #include <algorithm> 5 #include <queue> 6 #define LL long long 7 8 using namespace std; 9 10 const int INF = 1e9+7; 11 const int VM = 100003; 12 13 int par[VM]; 14 15 int find(int x) {//查找函数 16 if (par[x] == x)//若本身就是根节点 ,那么return 17 return x; 18 return find(par[x]);//不是的话,继续查找 19 } 20 21 void unite(int x, int y) {//合并函数 22 x = find(x);//查找x的根节点 23 y = find(y);//查找y的根节点 24 if (x == y)//若这两个数的结点是一样的,那么他们本来就是一个分组里的了,所以没有操作 25 return ; 26 par[x] = y;//不然的话,就让其中的一个点成为另一个点的根节点。 27 } 28 29 int main() { 30 int n, m; 31 int t = 0; 32 33 while (scanf("%d %d", &n, &m), n + m) { 34 for (int i = 1; i <= n; i++) 35 par[i] = i;//这是初始化 36 while (m--) { 37 int u, v; 38 scanf("%d %d", &u, &v); 39 unite(u, v); 40 } 41 int ans = 0; 42 for (int i = 1; i <= n; i++) 43 if (i == par[i]) 44 ans++;//计数 45 printf("Case %d: %d\n", ++t, ans);//输出 46 } 47 return 0; 48 }
既然说了上面是未优化的,那这儿就要说一下优化的喽。
我们的代码需要用到路径压缩 和 这课树(也就是分组)的高度。
若不知道这两个东东的 话 ,还是这位大牛的 http://www.cnblogs.com/cyjb/p/UnionFindSets.html
find函数的优化
int find(int x) {//查找函数 if (par[x] == x)//若本身就是根节点 ,那么return return x; return par[x] = find(par[x]);//不是的话,继续查找,并且进行路径压缩。 //上面为递归版本 /* int a = x; while (a != par[a])//一直找到a的 根节点 a = par[a]; while (x != par[x]) {//路径压缩 int t = par[x]; par[x] = a; x = t; } return a; */ //上面为非递归版本 }
合并函数的优化
1 void unite(int x, int y) {//合并函数 2 x = find(x);//查找x的根节点 3 y = find(y);//查找y的根节点 4 if (x == y)//若这两个数的结点是一样的,那么他们本来就是一个分组里的了,所以没有操作 5 return ; 6 if (rank[x] < rank[y]) //不然的话, 如果x这个分组的高度小于y分组的高度 7 par[x] = y;//将x并到 y这个分组中,并且是x的父节点是y 8 else { 9 par[y] = x;//不然就是y的父节点为x 10 if (rank[x] == rank[y])//若两个分组的高度相同 11 rank[x]++;//x 的分组高度++ 12 } 13 }
在这给出 这个题目的 优化的代码。
1 #include <cstdio> 2 #include <cstring> 3 #include <iostream> 4 #include <algorithm> 5 #include <queue> 6 #define LL long long 7 8 using namespace std; 9 10 const int INF = 1e9+7; 11 const int VM = 100003; 12 13 int par[VM]; 14 int rank[VM]; 15 16 int find(int x) {//查找函数 17 if (par[x] == x)//若本身就是根节点 ,那么return 18 return x; 19 return par[x] = find(par[x]);//不是的话,继续查找,并且进行路径压缩。 20 21 //上面为递归版本 22 /* 23 int a = x; 24 while (a != par[a])//一直找到a的 根节点 25 a = par[a]; 26 while (x != par[x]) {//路径压缩 27 int t = par[x]; 28 par[x] = a; 29 x = t; 30 } 31 return a; 32 */ 33 //上面为非递归版本 34 } 35 36 void unite(int x, int y) {//合并函数 37 x = find(x);//查找x的根节点 38 y = find(y);//查找y的根节点 39 if (x == y)//若这两个数的结点是一样的,那么他们本来就是一个分组里的了,所以没有操作 40 return ; 41 if (rank[x] < rank[y]) //不然的话, 如果x这个分组的高度小于y分组的高度 42 par[x] = y;//将x并到 y这个分组中,并且是x的父节点是y 43 else { 44 par[y] = x;//不然就是y的父节点为x 45 if (rank[x] == rank[y])//若两个分组的高度相同 46 rank[x]++;//x 的分组高度++ 47 } 48 } 49 50 int main() { 51 int n, m; 52 int t = 0; 53 54 while (scanf("%d %d", &n, &m), n + m) { 55 for (int i = 1; i <= n; i++) 56 par[i] = i;//这是初始化 57 memset(rank, 0, sizeof(rank));//树的高度 为 0 初始化 58 while (m--) { 59 int u, v; 60 scanf("%d %d", &u, &v); 61 unite(u, v); 62 } 63 int ans = 0; 64 for (int i = 1; i <= n; i++) 65 if (i == par[i]) 66 ans++;//计数 67 printf("Case %d: %d\n", ++t, ans);//输出 68 } 69 return 0; 70 }
再来一个题 : 题目题目
1 #include <cstdio> 2 #include <cstring> 3 #include <iostream> 4 #include <algorithm> 5 #include <queue> 6 #define LL long long 7 8 using namespace std; 9 10 const int INF = 1e9+7; 11 const int VM = 100003; 12 13 int par[VM]; 14 int rank[VM]; 15 16 int find(int x) {//查找函数 17 if (par[x] == x)//若本身就是根节点 ,那么return 18 return x; 19 return par[x] = find(par[x]);//不是的话,继续查找,并且进行路径压缩。 20 21 //上面为递归版本 22 /* 23 int a = x; 24 while (a != par[a])//一直找到a的 根节点 25 a = par[a]; 26 while (x != par[x]) {//路径压缩 27 int t = par[x]; 28 par[x] = a; 29 x = t; 30 } 31 return a; 32 */ 33 //上面为非递归版本 34 } 35 36 void unite(int x, int y) {//合并函数 37 x = find(x);//查找x的根节点 38 y = find(y);//查找y的根节点 39 if (x == y)//若这两个数的结点是一样的,那么他们本来就是一个分组里的了,所以没有操作 40 return ; 41 if (rank[x] < rank[y]) //不然的话, 如果x这个分组的高度小于y分组的高度 42 par[x] = y;//将x并到 y这个分组中,并且是x的父节点是y 43 else { 44 par[y] = x;//不然就是y的父节点为x 45 if (rank[x] == rank[y])//若两个分组的高度相同 46 rank[x]++;//x 的分组高度++ 47 } 48 } 49 50 int main() { 51 int n, m; 52 53 while (scanf("%d %d", &n, &m) == 2) { 54 for (int i = 0; i < n; i++) 55 par[i] = i;//这是初始化 56 memset(rank, 0, sizeof(rank));//树的高度 为 0 初始化 57 while (m--) { 58 int u, v; 59 scanf("%d %d", &u, &v); 60 unite(u, v); 61 } 62 int ans = 0; 63 for (int i = 1; i < n; i++) 64 if (find(par[0]) == find(par[i])) 65 ans++;//计数 66 printf("%d\n", ans);//输出 67 } 68 return 0; 69 }
不会画图,就不好讲了。
并查集 算是马马虎虎的说完了吧。。。。
插播 :
什么是 生成树?什么 又是 最小生成树?
给定一个无向图,如果它的某个子图中的任意两个顶点都互相联通并且是一棵树,那么这棵树就是 生成树 。
也就是说,在一个图中,有 n 个顶点 ,若有 n - 1 条边,能使得所有的顶点相连 ,就是 生成树了。
如果你给这些边 加上权值 ,那 权值 总和最小的额生成树 就是最小生成树
再插 :
最小生成树 有两种方法 一种 : 普利姆算法 另一种 : 克鲁斯卡尔。
3、 普利姆算法 & 优先队列优化
prim算法和Dijkstra算法十分相似,都是从某个顶点出发,不断加边的算法。
1. 假设有一棵树只包含一个顶点的v的树T。
2.贪心的选取T和其他顶点之间相连的最小权值的边,并将它加入T中。
3.不断重复1,2 知道所有的点相连生成一棵最小生成树。(此算法的正确性,不给予证明)
下面开始练题。
题目 : 我是题目 请点击
1 #include <cstdio> 2 #include <cstring> 3 #include <iostream> 4 #include <algorithm> 5 6 using namespace std; 7 8 const int INF = 1e9+7; 9 const int VM = 103; 10 11 int G[VM][VM];//存图 12 13 void prim(int n) { 14 int dis[VM];//记录 边的权值 15 bool vis[VM];//记录为否访问 16 int ans = 0;// 17 18 memset(vis, 0, sizeof(vis));//初始化 19 for (int i = 1; i <= n; i++) 20 dis[i] = G[1][i];//初始化 21 dis[1] = 0;// 22 vis[1] = true;// 1 点标记为已访问 23 int i; 24 for (i = 2; i <= n; i++) {//进行 n - 1 次操作 25 int u = INF;//初始化 26 int k; 27 for (int j = 1; j <= n; j++) {//遍历所有顶点 28 if (!vis[j] && u > dis[j]) {//在所有的未加入的点中 找一个最小的权值 29 k = j;//记录下标 30 u = dis[j];//更新最小值 31 } 32 } 33 if (u == INF)//若图是不连通的 34 break;//提前退出 35 vis[k] = true;//标记为已加入 36 ans += u;//加权值 37 for (int j = 1; j <= n; j++) {//遍历所有的点 38 if (!vis[j] && dis[j] > G[k][j])//对未加入的点&&能找到与此点相连且的权值最小的边 39 dis[j] = G[k][j];//进行更新 40 } 41 } 42 //输出 43 if (i - 1 == n) 44 printf("%d\n", ans); 45 else 46 printf("?\n"); 47 } 48 49 int main() { 50 int n, m; 51 52 while (scanf("%d %d", &n, &m), n) {//对边数 和点数的获取 53 for (int i = 1; i <= m; i++) {//初始化 54 for (int j = 1; j <= m; j++) { 55 G[i][j] = i == j ? 0 : INF; 56 } 57 } 58 while (n--) { 59 int u, v, w; 60 scanf("%d %d %d", &u, &v, &w);//获取 数据 61 if (G[u][v] > w)//防止重边&&存两点之间的最短距离 62 G[u][v] = G[v][u] = w; 63 } 64 prim(m);//调用函数 65 } 66 return 0; 67 }
主函数对数据的获取及图的存储
1 int main() { 2 int n, m; 3 4 while (scanf("%d %d", &n, &m), n) {//对边数 和点数的获取 5 for (int i = 1; i <= m; i++) {//初始化 6 for (int j = 1; j <= m; j++) { 7 G[i][j] = i == j ? 0 : INF; 8 } 9 } 10 while (n--) { 11 int u, v, w; 12 scanf("%d %d %d", &u, &v, &w);//获取 数据 13 if (G[u][v] > w)//防止重边&&存两点之间的最短距离 14 G[u][v] = G[v][u] = w; 15 } 16 prim(m);//调用函数 17 } 18 return 0; 19 }
普利姆函数
1 void prim(int n) { 2 int dis[VM];//记录 边的权值 3 bool vis[VM];//记录为否访问 4 int ans = 0;// 5 6 memset(vis, 0, sizeof(vis));//初始化 7 for (int i = 1; i <= n; i++) 8 dis[i] = G[1][i];//初始化 9 dis[1] = 0;// 10 vis[1] = true;// 1 点标记为已访问 11 int i; 12 for (i = 2; i <= n; i++) {//进行 n - 1 次操作 13 int u = INF;//初始化 14 int k; 15 for (int j = 1; j <= n; j++) {//遍历所有顶点 16 if (!vis[j] && u > dis[j]) {//在所有的未加入的点中 找一个最小的权值 17 k = j;//记录下标 18 u = dis[j];//更新最小值 19 } 20 } 21 if (u == INF)//若图是不连通的 22 break;//提前退出 23 vis[k] = true;//标记为已加入 24 ans += u;//加权值 25 for (int j = 1; j <= n; j++) {//遍历所有的点 26 if (!vis[j] && dis[j] > G[k][j])//对未加入的点&&能找到与此点相连且的权值最小的边 27 dis[j] = G[k][j];//进行更新 28 } 29 } 30 //输出 31 if (i - 1 == n) 32 printf("%d\n", ans); 33 else 34 printf("?\n"); 35 }
上面的算法的时间复杂度为O(V * V),是不是和Dijkstra很相似呢?那么可不可用优化Dijkstra算法的方法来优化这个呢? 当然可以
1 #include <cstdio> 2 #include <cstring> 3 #include <iostream> 4 #include <algorithm> 5 #include <queue> 6 #define LL long long 7 8 using namespace std; 9 10 const int INF = 1e9+7; 11 const int VM = 103; 12 13 typedef pair<int, int>P;//对组 14 struct node {//前向星 结构体 15 int v, w; 16 int next; 17 }; 18 node edge[4 * VM];//前向星数组 19 int head[VM];//头指针数组 20 int cnt;//计数 21 22 void add(int u, int v, int w) {//加边函数 23 edge[cnt].v = v;//顶点 24 edge[cnt].w = w;//权值 25 edge[cnt].next = head[u];//下一个 26 head[u] = cnt++;//头指针 27 } 28 29 void prim(int n) {//普利姆函数 30 bool vis[VM];//标记是否访问过 31 int dis[VM];//记录权值 32 int ans = 0;//最小生成树的总值 33 int count = 0;//计数 34 priority_queue<P, vector<P>, greater<P> >que;//权值从小到大的队列 35 36 fill(dis, dis + VM, INF);//初始化 37 memset(vis, 0, sizeof(vis));//初始化 38 dis[1] = 0;//初始化 39 que.push(P(0, 1));//将 1点 和 dis[1] = 0 放入队列 40 while (que.empty() == false) {//队列不为空时 41 P p = que.top();//取出队首 42 que.pop();//删除 43 int u = p.second;// 44 if (vis[u] == true)//若此顶点已经加入生成树 45 continue;// 46 vis[u] = true;//否则,就标记为加入 47 ans += dis[u];// 48 count++;//加入点个数 49 for (int i = head[u]; i != -1; i = edge[i].next) {//遍历与该点相邻的点 50 node e = edge[i]; 51 if (dis[e.v] > e.w) {//更新他们的权值 52 dis[e.v] = e.w;// 53 que.push(P(dis[e.v], e.v));//放入队列 54 } 55 } 56 } 57 //输出 58 if (count == n) 59 printf("%d\n", ans); 60 else 61 printf("?\n"); 62 } 63 64 int main() { 65 int n, m; 66 67 while (scanf("%d %d", &n, &m), n) {//边的个数 顶点个数 68 memset(head, -1, sizeof(head));//初始化 69 cnt = 0;//初始化 70 while (n--) { 71 int u, v, w; 72 scanf("%d %d %d", &u, &v, &w);//获取数据 73 add(u, v, w);//加边 74 add(v, u, w);//无向图 75 } 76 prim(m);//普利姆算法 77 } 78 return 0; 79 }
主函数对数据的获取 和 图的存储
1 int main() { 2 int n, m; 3 4 while (scanf("%d %d", &n, &m), n) {//边的个数 顶点个数 5 memset(head, -1, sizeof(head));//初始化 6 cnt = 0;//初始化 7 while (n--) { 8 int u, v, w; 9 scanf("%d %d %d", &u, &v, &w);//获取数据 10 add(u, v, w);//加边 11 add(v, u, w);//无向图 12 } 13 prim(m);//普利姆算法 14 } 15 return 0; 16 }
prim函数
1 void prim(int n) {//普利姆函数 2 bool vis[VM];//标记是否访问过 3 int dis[VM];//记录权值 4 int ans = 0;//最小生成树的总值 5 int count = 0;//计数 6 priority_queue<P, vector<P>, greater<P> >que;//权值从小到大的队列 7 8 fill(dis, dis + VM, INF);//初始化 9 memset(vis, 0, sizeof(vis));//初始化 10 dis[1] = 0;//初始化 11 que.push(P(0, 1));//将 1点 和 dis[1] = 0 放入队列 12 while (que.empty() == false) {//队列不为空时 13 P p = que.top();//取出队首 14 que.pop();//删除 15 int u = p.second;// 16 if (vis[u] == true)//若此顶点已经加入生成树 17 continue;// 18 vis[u] = true;//否则,就标记为加入 19 ans += dis[u];// 20 count++;//加入点个数 21 for (int i = head[u]; i != -1; i = edge[i].next) {//遍历与该点相邻的点 22 node e = edge[i]; 23 if (dis[e.v] > e.w) {//更新他们的权值 24 dis[e.v] = e.w;// 25 que.push(P(dis[e.v], e.v));//放入队列 26 } 27 } 28 } 29 //输出 30 if (count == n) 31 printf("%d\n", ans); 32 else 33 printf("?\n"); 34 }
此算法的时间复杂度为O(E*log(V)); 是不是很棒!
次算法结束。
4、 克鲁斯卡尔算法
克鲁斯卡尔算法就是利用了并查集这一方法,通过对所有边从小到大排序后,判断这两个顶点是否在一个分组中,若在一个分组中,说明这两个点已经加入到生成树之中,若不在一个分组中
就可以直接加上这个边了。
还是以上一题为例
1 #include <cstdio> 2 #include <cstring> 3 #include <iostream> 4 #include <algorithm> 5 #include <queue> 6 #define LL long long 7 8 using namespace std; 9 10 const int INF = 1e9+7; 11 const int VM = 103; 12 13 struct node {//边的结构体 14 int u, v, w; 15 }; 16 node edge[VM * 2]; 17 int rank[VM];//分组的高度 18 int par[VM];//父节点 19 20 bool cmp(const node &a, const node &b) { 21 return a.w < b.w;//按w从小到大排序 22 } 23 24 int find(int x) { 25 if (par[x] == x)//若根节点为本身 26 return x; 27 return par[x] = find(par[x]);//路径压缩 28 } 29 30 bool same(int x, int y) {//判断为否在同一分组中 31 return find(x) == find(y); 32 } 33 34 void unite(int x, int y) { 35 x = find(x);//查找根节点 36 y = find(y);//查找根节点 37 if (x == y)//若以在同一分组 38 return ; 39 if (rank[x] < rank[y])//y所在分组的高度 大于x的 40 par[x] = y;//将y作x的父节点。 41 else { 42 par[y] = x;//将x作为y的父节点 43 if (rank[x] == rank[y])//若两个分组高度相同 44 rank[x]++;//x分组所在高度++ 45 } 46 } 47 48 int main() { 49 int n, m; 50 51 while (scanf("%d %d", &n, &m), n) {//获取边的个数 和顶点个数 52 int cnt = 0;// 53 for (int i = 1; i <= m; i++)//初始化 54 par[i] = i; 55 memset(rank, 0, sizeof(rank));//初始化 56 while (n--) { 57 scanf("%d %d %d", &edge[cnt].u, &edge[cnt].v, &edge[cnt].w);//获取数据 58 cnt++; 59 } 60 sort(edge, edge + cnt, cmp);//按权值从小到大排序 61 int ans = 0;//最小生成树 权值 62 int count = 0;//计数 63 for (int i = 0; i < cnt; i++) {//对所有的边 64 node e = edge[i]; 65 if (!same(e.u, e.v)) {//若两点不属于一个分组 66 ans += e.w;//权值总和 67 unite(e.u, e.v);//合并两点 68 count++;//计数 69 } 70 } 71 //输出 72 if (count == m - 1) 73 printf("%d\n", ans); 74 else 75 printf("?\n"); 76 } 77 return 0; 78 }
自己感觉这个专题写的好糟糕啊,哎,深夜了,睡觉吧。明天又是新的一天啊。因为明天,不,今天8点就要开始新学期第一堂课了啊。
作者:blueppo
出处:http://www.cnblogs.com/Yan-C/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。