题目:
给你一个points 数组,表示 2D 平面上的一些点,其中 points[i] = [xi, yi] 。
连接点 [xi, yi] 和点 [xj, yj] 的费用为它们之间的 曼哈顿距离 :|xi - xj| + |yi - yj| ,其中 |val| 表示 val 的绝对值。
请你返回将所有点连接的最小总费用。只有任意两点之间 有且仅有 一条简单路径时,才认为所有点都已连接。
示例一:
输入:points = [[0,0],[2,2],[3,10],[5,2],[7,0]]
输出:20
解释:
我们可以按照上图所示连接所有点得到最小总费用,总费用为 20 。
注意到任意两个点之间只有唯一一条路径互相到达。
示例二:
输入:points = [[-1000000,-1000000],[1000000,1000000]]
输出:4000000
示例三:
输入:points = [[0,0]]
输出:0
基本思想:
Kruskal 算法
, Kruskal算法是基于贪心的思想得到的。起初每个端点作为独立的集合
,把所有的边按照权值先从小到大排列,接着按照顺序选取每条边
,如果这条边的两个端点不属于同一集合
,那么就将它们合并
,直到所有的点都属于同一个集合为止。
如何合并?使用并查集
(一般题目,并查集的书写是固定的,稍难的可能要增加一些变量甚至修改数据的存储结构)。换而言之,Kruskal算法就是基于并查集的贪心算法
具体流程:
- 将图G看做一个森林,每个顶点为一棵独立的树
- 将所有的边加入集合S,即一开始S = E
- 从S中拿出一条最短的边(u,v),如果(u,v)不在同一棵树内,则连接u,v合并(使用并查集)这两棵树,同时将(u,v)加入生成树的边集E’
- 重复(3)直到所有点属于同一棵树,边集E’就是一棵最小生成树
class Solution {
public int minCostConnectPoints(int[][] points) {
int n=points.length;
//并查集,n作为点的个数
UnionFind unionFind=new UnionFind(n);
//使用List存储每条边
List<Edge> edges=new ArrayList<>();
//遍历每两个点的组合,并记录他们的边的距离,并使用Edge对象存储
for(int i=0;i<n;i++){
for(int j=i+1;j<n;j++){
edges.add(new Edge(dis(points,i,j),i,j));
}
}
//对边进行排序
Collections.sort(edges,(a,b)->(a.len-b.len));
//num说明当前连通图的点的个数,当num=n时,所有点就都连通了
int num=1;
//作为最短长度
int len=0;
//Kruskal算法,逐个遍历,并选择没有连通的两个点
for(Edge edge:edges){
int x=edge.x;
int y=edge.y;
//判断是否连通,没有连通就合并
if(unionFind.union(x,y)){
len+=edge.len;
num++;
}
//判断是否全部都连通了,是,则退出
if(num==n){
break;
}
}
return len;
}
//计算两点间距离
public int dis(int[][] points,int x,int y){
return Math.abs(points[x][0]-points[y][0])+Math.abs(points[x][1]-points[y][1]);
}
//并查集
class UnionFind{
private int[] parent;//数组,用来记录每个点的父节点
private int[] rank;//数组,用来记录以当前节点为根节点的树的深度(高度)
public UnionFind(int n){
parent=new int[n];
rank=new int[n];
for(int i=0;i<n;i++){
parent[i]=i;//初始化,每个节点父节点为自身
rank[i]=1;//初始化,每个节点都是单独的,所以深度为1
}
}
//合并函数
public boolean union(int x,int y){
int rootX=find(x);//找到x的根节点
int rootY=find(y);//找到y的根节点
if(rootX==rootY){
//如果相等,说明已经在同一树上,不用合并
return false;
}
//rank的引入是为了降低树的深度
if(rank[rootX]==rank[rootY]){
parent[rootX]=rootY;
rank[rootY]++;//修改深度
}else if(rank[rootX]<rank[rootY]){
//如果rootX为根节点的树深度小,则让其指向rootY
parent[rootX]=rootY;
}else{
parent[rootY]=rootX;
}
return true;
}
public int find(int x){
//if这部分是路径压缩,用来降低树的高度,可以画个树感受一下
if(x!=parent[x]){
parent[x]=find(parent[x]);
}
return parent[x];
}
}
//边的对象
class Edge{
int len;
int x;
int y;
public Edge(int len,int x,int y){
this.len=len;
this.x=x;
this.y=y;
}
}
}
时间复杂度:
Kruskal 算法通常要先对边按权值从小到大排序,这一步的时间复杂度为为O(|Elog|E|)。Kruskal算法的实现通常使用并查集,来快速判断两个顶点是否属于同一个集合。最坏的情况可能要枚举完所有的边,此时要循环|E|次,所以这一步的时间复杂度为O(|E|α(V)),其中α为Ackermann函数,其增长非常慢,我们可以视为常数。所以Kruskal算法的时间复杂度为O(|Elog|E|)
题目中E的个数通过两层for循环得到,故为O(n2),n为节点数。
故时间复杂度为O(n2log(n2))
空间复杂度:
O(n2)。并查集使用 O(n)的空间,边集数组需要使用 O(n2) 的空间