给你一个points 数组,表示 2D 平面上的一些点,其中 points[i] = [xi, yi] 。
连接点 [xi, yi] 和点 [xj, yj] 的费用为它们之间的 曼哈顿距离 :|xi - xj| + |yi - yj| ,其中 |val| 表示 val 的绝对值。
请你返回将所有点连接的最小总费用。只有任意两点之间 有且仅有 一条简单路径时,才认为所有点都已连接。
示例 1:
输入:points = [[0,0],[2,2],[3,10],[5,2],[7,0]]
输出:20
解释:
我们可以按照上图所示连接所有点得到最小总费用,总费用为 20 。
注意到任意两个点之间只有唯一一条路径互相到达。
示例 2:
输入:points = [[3,12],[-2,5],[-4,1]]
输出:18
示例 3:
输入:points = [[0,0],[1,1],[1,0],[-1,1]]
输出:4
示例 4:
输入:points = [[-1000000,-1000000],[1000000,1000000]]
输出:4000000
示例 5:
输入:points = [[0,0]]
输出:0
提示:
1 <= points.length <= 1000
-106 <= xi, yi <= 106
所有点 (xi, yi) 两两不同。
解题思路
如果连通图G的一个子图是一棵包含G的所有顶点的树,则该子图称为G的生成树。 生成树是连通图的包含图中的所有顶点的极小连通子图。 图的生成树不惟一。 从不同的顶点出发进行遍历,可以得到不同的生成树。
求连接所有点得到最小总费用,即求最小生成树:一副连通加权无向图中一棵权值最小的生成树。
最小生成树的两种经典算法:Prim算法和Kruskal算法
一、Prim算法
算法描述
- 输入:一个加权连通图,其中顶点集合为V,边集合为E;
- 初始化:Vnew = {x},其中x为集合V中的任一节点(起始点),Enew = {},为空;
- 重复下列操作,直到Vnew = V:
a. 在集合E中选取权值最小的边<u, v>,其中u为集合Vnew中的元素,而v不在Vnew集合当中,并且v∈V(如果存在有多条满足前述条件即具有相同权值的边,则可任意选取其中之一);
b. 将v加入集合Vnew中,将<u, v>边加入集合Enew中; - 输出:使用集合Vnew和Enew来描述所得到的最小生成树。
就是不断从新的顶点集合(初始只有一个顶点)中选取与其相邻的不在新顶点集合中的顶点的所有相连边中权值最短的边加入到新边集合中,该顶点加入新顶点集合。这样得到的新顶点集合和新边集合就是最小生成树。
代码
- 数据结构
- 集合
V
:最开始的所有顶点集合 - 集合
Vnew
:存放已经加入到最小生成树的顶点 - 一维数组
lowcost
:记录V中所有顶点离最小生成树Vnew
的最短距离(已加入最小生成树的顶点则记为-1
) - 一维数组
visit
:记录v中顶点的访问情况(1
已加入最小生成树) - 集合
Enew
:只是求最小权值的话就无需记录边了
- 集合
- 步骤
V
中随机选择一个顶点加入Vnew
,更新lowcost
和visit
。- 重复以下步骤直到所有顶点的
visit
置为-1
:- 遍历
lowcost
,寻找最小值,将对应顶点加入Vnew
,更新该顶点的visit
为-1
,更新所有V
中未加入最小生成树的顶点的lowcost
值
- 遍历
- code1(通过样例65/72:超时)//待分析
#include "bits/stdc++.h" using namespace std; const int maxn = 0x7f7f7f; class Solution { public: int ManhattanDistance(int x, int y) { return abs(points[x][0] - points[y][0]) + abs(points[x][1] - points[y][1]); } int Prim(int start) { vector<int> vNew = {start}; // 最小生成树顶点集合 vector<int> lowCost(n, maxn); // 记录还未加入最小生成树的顶点到已加入最小生成树中顶点的距离 lowCost[start] = -1; // 已加入最小生成树的顶点,cost置为-1 int result = 0; // 记录最小生成树权值和 while (1) { int curMinCost = maxn; // 记录当前lowCost中的最小cost int curPoint = -1; // 记录即将加入最小生成树的顶点 for (int i = 0; i < vNew.size(); ++i) { // 更新lowCost for (int j = 0; j < n; ++j) { lowCost[j] = min(lowCost[j], ManhattanDistance(vNew[i], j)); // 获取lowCost最小值 if (lowCost[j] != -1 && lowCost[j] < curMinCost) { curMinCost = lowCost[j]; curPoint = j; } } } // 将curPoint加入最小生成树,更新最小生成树的权值和 if (curPoint != -1) { vNew.emplace_back(curPoint); // 更新最小生成树 result += curMinCost; // 更新最小生成树权值和 lowCost[curPoint] = -1; // 更新已加入最小生成树顶点的lowCost } else { break; } } return result; } int minCostConnectPoints(vector<vector<int>>& points) { this->points = points; n = points.size(); return Prim(0); } private: vector<vector<int>> points; int n; };
- code2
实际只用了lowCost
,顶点直接用下标记录,是否已加入生成树可用lowCost
值是否为-1
判断,因为权值(曼哈顿距离)不会为负值;
整数最大值使用内置INT_MAX
#include "bits/stdc++.h" using namespace std; class Solution { public: // 计算曼哈顿距离 int ManhattanDistance(int x, int y) { return abs(points[x][0] - points[y][0]) + abs(points[x][1] - points[y][1]); } // 普利姆算法计算最小生成树 int Prim(int start) { vector<int> lowCost(n, INT_MAX); int result = 0; // 记录最小生成树权值和 // 加入顶点start,更新lowCost lowCost[start] = -1; // lowCost=-1记录已加入最小生成树的顶点 for (int i = 0; i < n; ++i) { lowCost[i] = min(lowCost[i], ManhattanDistance(i, start)); } // 重复从未加入最小生成树的顶点中选取最小权边,n个顶点只需选取n-1次(除去初始顶点start) for (int i = 0; i < n - 1; ++i) { int minIndex = -1; int minValue = INT_MAX; // 遍历查找当前最小权边 for (int j = 0; j < n; ++j) { if (lowCost[j] == -1) { continue; } // 更新当前最小权边 if (lowCost[j] < minValue) { minValue = lowCost[j]; minIndex = j; } } // 将minIndex加入最小生成树,并更新最小生成树权值和 lowCost[minIndex] = -1; result += minValue; // 更新lowCost for (int j = 0; j < n; ++j) { lowCost[j] = min(lowCost[j], ManhattanDistance(minIndex, j)); } } return result; } int minCostConnectPoints(vector<vector<int>>& points) { this->points = points; n = points.size(); return Prim(0); } private: vector<vector<int>> points; int n; };
二、Kruskal算法
算法描述
Kruskal 算法是一种贪心算法,其思想很简单:
- 首先对所有的边进行排序,然后从小到大依次遍历每条边,同时判断每条边是否同源(属于同一个联通分量)
- 如果同源,跳过;
- 如果不同源,将两个连通分量合并,直到所有顶点属于同一个连通分量
很明显就是并查集
与Prim算法比较:
- Prim算法是以顶点为基础(每次寻找离Vnew最近的顶点);
- Kruskal算法是以边为基础,每次从边集合中寻找最小的边(不管两个顶点属于V还是Vnew),然后判断该边的两个顶点是否同源(属于同一个连通分量)。
- 二者使用的数据结构通常不大一样,Kruskal因为涉及对边按权值排序,所以会使用结构体,Prim则一般直接使用二维数组存储边的权值。
代码
#include "bits/stdc++.h"
using namespace std;
class UnionFind {
public:
// 构造函数初始化
UnionFind(int n) {
pre.resize(n);
for (int i = 0; i < n; ++i) {
pre[i] = i; // 初始将自己作为自己的根节点
}
}
// 查找根节点
int Find(int x) {
return x == pre[x] ? x : Find(pre[x]);
}
// 合并两个节点
void Union(int x, int y) {
pre[Find(x)] = Find(y); // 将其中一个的根结点置为另一个的根结点的父结点
}
private:
vector<int> pre;
};
struct Eage {
int start;
int end;
int value;
Eage(int start, int end, int value) : start(start), end(end), value(value) {}
};
static bool Cmp(const Eage& a, const Eage& b) {
return a.value < b.value;
}
class Solution {
public:
// 计算曼哈顿距离
int ManhattanDistance(vector<vector<int>>& points, int x, int y) {
return abs(points[x][0] - points[y][0]) + abs(points[x][1] - points[y][1]);
}
int minCostConnectPoints(vector<vector<int>>& points) {
int n = points.size();
vector<Eage> eages;
int result = 0;
// 重建数据结构
for (int i = 0; i < n; ++i) {
for (int j = i + 1; j < n; ++j) { // 不要重复加入边,例如(i,j)和(j,i)
eages.emplace_back(Eage(i, j, ManhattanDistance(points, i, j)));
}
}
// 排序
sort(eages.begin(), eages.end(), Cmp);
// 依次选择最小权边
UnionFind uf(n); // note:是顶点数不是边数
int curLen = 1;
for (const auto& eage : eages) {
// 该边的起点和终点不在同一个联通分量,即可加入最小生成树
if (uf.Find(eage.start) != uf.Find(eage.end)) {
result += eage.value; // 加入最小生成树
uf.Union(eage.start, eage.end); // 合并成一个联通分量
++curLen;
// n个节点均加入最小生成树即可
if (curLen == n) {
break;
}
}
}
return result;
}
};
参考: