LeetCode 1584. 连接所有点的最小费用(Kruskal 算法、并查集)

题目:
给你一个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算法就是基于并查集的贪心算法

具体流程:

  1. 将图G看做一个森林,每个顶点为一棵独立的树
  2. 将所有的边加入集合S,即一开始S = E
  3. 从S中拿出一条最短的边(u,v),如果(u,v)不在同一棵树内,则连接u,v合并(使用并查集)这两棵树,同时将(u,v)加入生成树的边集E’
  4. 重复(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) 的空间

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值