最小生成树
问题描述:一个有n户人家的村庄,有m条路连接着。村里现在要修路,每条路都有一个代价,现在请你帮忙计算下,最少需要花费多少的代价,就能让这n户人家连接起来。
示例
输入:3,3,[[1,3,3],[1,2,1],[2,3,1]]
返回值:2
说明:见下图,n户人家相连接不一定需要直达,所以只需要修建1与2之间,2与3之间的道路即可。
方法一
思路分析
本题题目即为最小生成树,因此求解最小生成树的算法有:普里姆算法—Prim算法、克鲁斯卡算法。因此方法一首先介绍Prim算法。其算法思想步骤如下:
- 设置初始集合$S$,初始为空,设总的节点集合$V$
- 首先图中任意节点1放入初始节点集合中$S={1}$,寻找与1有关联的边,寻找其权值最小的边,且连接的点2在集合$V-S$,如果寻找到则将其放入$S={1,2}$,并将该边的权值记录下来
- 接着寻找与$1,2$相关联的边,方法与上述相同
- 当$S=V$,最小生成树生成,程序结束
图解
输入:4,5,[[1,3,3],[1,2,1],[2,3,1],[2,4,2],[3,4,4]]
返回值:4
算法过程如图所示
C++核心代码
class Solution { public: /** * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可 * * 返回最小的花费代价使得这n户人家连接起来 * @param n int n户人家的村庄 * @param m int m条路 * @param cost intvector<vector<>> 一维3个参数,表示连接1个村庄到另外1个村庄的花费的代价 * @return int */ int miniSpanningTree(int n, int m, vector<vector<int> >& cost) { // write code here vector<int> visited(n,0); visited[0] = 1; int len = 0; vector<int> adjset(n); vector<vector <int> > map(n ,vector<int>(n,INT_MAX)); for(int i=0;i<m;i++) { map[cost[i][0]-1][cost[i][1]-1]=cost[i][2]; map[cost[i][1]-1][cost[i][0]-1]=cost[i][2]; } for(int i=0;i<n;i++) map[i][i]=0; for (int i=0;i<n;i++) adjset[i] = map[i][0];//计算只有一个节点与其他节点的距离值 for (int i=1;i<n;i++) { int nlen = INT_MAX; int nid=0; for (int j=0;j<n;j++) { if (!visited[j] && adjset[j]<nlen) { nlen = adjset[j]; nid = j; } } len += nlen; visited[nid] = 1;//标记已经访问过的点 for (int j=0;j<n;j++) { if (!visited[j] && map[j][nid]<adjset[j]) { adjset[j] = map[j][nid]; } } } return len; } };
复杂度分析
- 时间复杂度:从点出发,每次寻找一个点,代码中存在内外两层嵌套循环,循环次数为$1+2+3+..+n=n*(n+1)/2$,其时间复杂度为$O(n^2)$
- 空间复杂度:借助于一个$n$大小的标记数组用于标记点是否访问过,借助于一个大小为$n$距离节点用于存储已经标记的点到其他点的距离,借助于一个$n^2$的二维数组存储图,因此总的空间复杂度为$O(n^2)$
方法二
思路分析
克鲁斯卡尔算法的基本思想为:将所有边按照权值的大小进行升序排序,然后从小到大一一判断,条件为:如果这个边不会与之前选择的所有边组成回路,就可以作为最小生成树的一部分;反之,舍去。直到具有 n 个顶点的连通网筛选出来 n-1 条边为止。筛选出来的边和所有的顶点构成此连通网的最小生成树。
其中最难处理的是判断是否会产生回路,其方法为:在初始状态下给每个顶点赋予不同的标记,对于遍历过程的每条边,其都有两个顶点,判断这两个顶点的标记是否一致,如果一致,说明它们本身就处在一棵树中,如果继续连接就会产生回路;如果不一致,说明它们之间还没有任何关系,可以连接。
图解
C++核心代码
class Solution { public: /** * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可 * * 返回最小的花费代价使得这n户人家连接起来 * @param n int n户人家的村庄 * @param m int m条路 * @param cost intvector<vector<>> 一维3个参数,表示连接1个村庄到另外1个村庄的花费的代价 * @return int */ int p[100010]; int find(int x)//查找结点与路径 { if(x != p[x]) p[x] = find(p[x]); return p[x]; } int miniSpanningTree(int n, int m, vector<vector<int> >& cost) { // write code here for(int i = 0; i <= n; i++) p[i] = i;//对每一个顶点进行标记,标记不同 sort(cost.begin(),cost.begin() + m, [](vector<int>& l1, vector<int> & l2){ return l1[2] < l2[2];//按照权值从小到大排序 }); int sum = 0;//计算最短权值 for(int i = 0; i < m; i++) { if(find(cost[i][0]) != find(cost[i][1]))//不是同一个的话就进入循环 { sum += cost[i][2];//将值计算进最终结果中 p[find(cost[i][0])] = find(cost[i][1]);//将加入的边的两个点标记一致 } } return sum; } };
复杂度分析
- 时间复杂度:从边出发,首先需要对所有的边排序,接着每次寻找权值最小的边,因此总的时间复杂度为$O(m\log m)$
- 空间复杂度:借助于一个大小为$n$标记数组用于将每个节点标记为不同,空间复杂度为$O(n)$