文章目录
题目
标题和出处
标题:连接所有点的最小费用
难度
5 级
题目描述
要求
给定一个数组 points \texttt{points} points,表示二维平面上的一些点的整数坐标,其中 pointsi = x i , y i \texttt{pointsi = x}_\\texttt{i}\\texttt{, y}_\\texttt{i}\\texttt{} pointsi = xi, yi。
连接点 x i , y i \texttt{x}_\\texttt{i}\\texttt{, y}_\\texttt{i}\\texttt{} xi, yi 和点 x j , y j \texttt{x}_\\texttt{j}\\texttt{, y}_\\texttt{j}\\texttt{} xj, yj 的费用为它们之间的曼哈顿距离 : |x i − x j | + |y i − y j | \texttt{|x}\texttt{i} - \texttt{x}\texttt{j}\texttt{|} + \texttt{|y}\texttt{i} - \texttt{y}\texttt{j}\texttt{|} |xi−xj|+|yi−yj|,其中 |val| \texttt{|val|} |val| 表示 val \texttt{val} val 的绝对值。
返回将所有点连接的最小总费用。只有任意两点之间恰好有一条简单路径时,才认为所有点都已连接。
示例
示例 1:

输入: points = \[0,0,2,2,3,10,5,2,7,0] \texttt{points = \[0,0,2,2,3,10,5,2,7,0]} points = \[0,0,2,2,3,10,5,2,7,0]
输出: 20 \texttt{20} 20
解释:

可以按照上图所示连接所有点得到最小总费用,总费用为 20 \texttt{20} 20。
注意任意两个点之间只有一条路径互相到达。
示例 2:
输入: points = \[3,12,-2,5,-4,1] \texttt{points = \[3,12,-2,5,-4,1]} points = \[3,12,-2,5,-4,1]
输出: 18 \texttt{18} 18
数据范围
- 1 ≤ points.length ≤ 1000 \texttt{1} \le \texttt{points.length} \le \texttt{1000} 1≤points.length≤1000
- -10 6 ≤ x i , y i ≤ 10 6 \texttt{-10}^\texttt{6} \le \texttt{x}\texttt{i}\texttt{, y}\texttt{i} \le \texttt{10}^\texttt{6} -106≤xi, yi≤106
- 所有点 (x i , y i ) \texttt{(x}\texttt{i}\texttt{, y}\texttt{i}\texttt{)} (xi, yi) 各不相同
前言
可以将所有的点和连接点的边看成带权图,任意两个点之间存在一条边,每条边的权重等于这条边连接的两个点之间的曼哈顿距离。用 n n n 表示点的个数,则图中有 n ( n − 1 ) 2 \dfrac{n(n - 1)}{2} 2n(n−1) 条边。
这道题要求计算将所有点连接的最小总费用,等价于计算图中的最小生成树的权重之和。
最小生成树算法有 Kruskal 算法和 Prim 算法。
解法一
思路和算法
Kruskal 算法的做法是,每次在尚未选取的边中选取一条权重最小且不会产生环的边,将这条边作为最小生成树中的一条边,直到所有的点连接。
首先计算图中的所有边并使用优先队列存储。遍历每一对点,计算两个点之间的曼哈顿距离作为边的权重,将边加入优先队列,优先队列中的每条边包含边两端的点下标和边的权重。
得到图中的所有边之后,每次从优先队列中取出权重最小的边,如果取出的边不会产生环则将这条边作为最小生成树中的一条边,将这条边的权重加到总费用,并将这条边两端的点连接。当所有的点都连接时,即可得到最小总费用。
判断选取一条边是否会产生环的做法是,判断这条边连接的两个点是否属于同一个连通分量,如果属于同一个连通分量则选取这条边之后会产生环,如果不属于同一个连通分量则选取这条边之后不会产生环。选取一条边之后,需要将这条边连接的两个点合并到同一个连通分量。
连通性问题可以使用并查集解决。
代码
java
class Solution {
public int minCostConnectPoints(int[][] points) {
int totalCost = 0;
PriorityQueue<int[]> pq = new PriorityQueue<int[]>((a, b) -> a[2] - b[2]);
int n = points.length;
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
int cost = Math.abs(points[i][0] - points[j][0]) + Math.abs(points[i][1] - points[j][1]);
pq.offer(new int[]{i, j, cost});
}
}
UnionFind uf = new UnionFind(n);
while (uf.getCount() > 1) {
int[] edge = pq.poll();
int point0 = edge[0], point1 = edge[1], cost = edge[2];
if (uf.find(point0) != uf.find(point1)) {
totalCost += cost;
uf.union(point0, point1);
}
}
return totalCost;
}
}
class UnionFind {
private int[] parent;
private int[] rank;
private int count;
public UnionFind(int n) {
parent = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
}
rank = new int[n];
this.count = n;
}
public void union(int x, int y) {
int rootx = find(x);
int rooty = find(y);
if (rootx != rooty) {
if (rank[rootx] > rank[rooty]) {
parent[rooty] = rootx;
} else if (rank[rootx] < rank[rooty]) {
parent[rootx] = rooty;
} else {
parent[rooty] = rootx;
rank[rootx]++;
}
count--;
}
}
public int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
public int getCount() {
return count;
}
}
复杂度分析
-
时间复杂度: O ( n 2 log n ) O(n^2 \log n) O(n2logn),其中 n n n 是点的个数。用 m m m 表示边数,这道题中的边数是 m = n ( n − 1 ) 2 m = \dfrac{n(n - 1)}{2} m=2n(n−1)。将所有边加入优先队列的时间是 O ( m log m ) = O ( n 2 log ( n 2 ) ) = O ( n 2 log n ) O(m \log m) = O(n^2 \log (n^2)) = O(n^2 \log n) O(mlogm)=O(n2log(n2))=O(n2logn),并查集初始化的时间是 O ( n ) O(n) O(n),Kruskal 算法的时间复杂度是 O ( n + m log m ) = O ( n + n 2 log ( n 2 ) ) = O ( n 2 log n ) O(n + m \log m) = O(n + n^2 \log (n^2)) = O(n^2 \log n) O(n+mlogm)=O(n+n2log(n2))=O(n2logn),因此时间复杂度是 O ( n 2 log n ) O(n^2 \log n) O(n2logn)。
-
空间复杂度: O ( n 2 ) O(n^2) O(n2),其中 n n n 是点的个数。优先队列的空间是 O ( n 2 ) O(n^2) O(n2),并查集的空间是 O ( n ) O(n) O(n)。
解法二
思路和算法
Prim 算法的做法是,任选一个点开始构建最小生成树,初始时的最小生成树只有选定的点,每次在尚未选取的点中选取与最小生成树连接的边的权重最小的点,将该点和对应的边添加到最小生成树中,直到所有的点都添加到最小生成树中。
首先计算图中的所有边的权重并使用邻接矩阵存储。遍历每一对点,计算两个点之间的曼哈顿距离作为边的权重。
得到图中的所有边的权重之后,从 points 0 \textit{points}0 points0 开始构建最小生成树。创建数组 costs \textit{costs} costs,其中 costs i \textit{costs}i costsi 表示 points i \textit{points}i pointsi 与最小生成树的最短距离,初始时最小生成树中只有 points 0 \textit{points}0 points0,因此 costs i \textit{costs}i costsi 为 points i \textit{points}i pointsi 与 points 0 \textit{points}0 points0 之间的曼哈顿距离。
每次从尚未加入最小生成树的点中选择与最小生成树的距离最短的点,将该点与最小生成树的最短距离加到总费用,将该点对应的 costs \textit{costs} costs 中的值更新为 0 0 0,然后使用该点与其他点的距离更新所有尚未加入最小生成树的点与最小生成树的最短距离。当所有的点都加入最小生成树时,即可得到最小总费用。
由于这道题中任意两个点之间都存在一条边,因此是边稠密图,Prim 算法的性能优于 Kruskal 算法。
代码
java
class Solution {
public int minCostConnectPoints(int[][] points) {
int totalCost = 0;
int n = points.length;
int[][] matrix = new int[n][n];
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
int cost = Math.abs(points[i][0] - points[j][0]) + Math.abs(points[i][1] - points[j][1]);
matrix[i][j] = matrix[j][i] = cost;
}
}
boolean[] visited = new boolean[n];
visited[0] = true;
int[] costs = new int[n];
Arrays.fill(costs, Integer.MAX_VALUE);
costs[0] = 0;
for (int i = 1; i < n; i++) {
costs[i] = matrix[0][i];
}
for (int i = 1; i < n; i++) {
int minIndex = -1;
int minCost = Integer.MAX_VALUE;
for (int j = 0; j < n; j++) {
if (visited[j]) {
continue;
}
if (minCost > costs[j]) {
minIndex = j;
minCost = costs[j];
}
}
totalCost += minCost;
visited[minIndex] = true;
costs[minIndex] = 0;
for (int j = 0; j < n; j++) {
if (!visited[j]) {
costs[j] = Math.min(costs[j], matrix[minIndex][j]);
}
}
}
return totalCost;
}
}
复杂度分析
-
时间复杂度: O ( n 2 ) O(n^2) O(n2),其中 n n n 是点的个数。计算每一对点之间的曼哈顿距离的时间是 O ( n 2 ) O(n^2) O(n2),Prim 算法的时间复杂度是 O ( n 2 ) O(n^2) O(n2)。
-
空间复杂度: O ( n 2 ) O(n^2) O(n2),其中 n n n 是点的个数。邻接矩阵空间是 O ( n 2 ) O(n^2) O(n2),Prim 算法记录每个点是否访问过和每个点的最小开销的空间是 O ( n ) O(n) O(n),因此空间复杂度是 O ( n 2 ) O(n^2) O(n2)。