1 最小生成树
从图中找出一颗覆盖所有节点的树,使得该树中所有边的权重和最小,此树就是一颗最小生成树。常用的最小生成树算法有两个:kruskal和prim算法,二者都是贪心算法。
注意:图和树的根本区别是:图可以有环,但树一定没有环。
2 kuskal算法
待求解的边的集合为A。
2.1 kruskal算法描述
在kruskal算法中,A是一个森林。其节点就是给定图的节点。每次加入到集合中的安全边永远是权重最小的连接两个不同分量的边。
这里说A是一个森林,是因为在该算法执行过程中,A包含很多颗树,也即多个连通分量。在算法执行过程中,就会逐步将中间过程中的所有连通分量连通,最后形成一个大的连通分量,这也就是一颗最小生成树。
2.2 kruskal算法实现原理
(1)首先给所有边按照权重从小到大进行排序;
(2)遍历已排序的边:如果此边的两个节点属于两个不同的集合(也就是属于两个不同的连通分量),则将此边加入到集合A,并联合这两个节点所隶属的两个集合;否则,不将此边加入到集合A。
(3)返回集合A。
2.3 kruskal算法代码实现
这里直接在例子中直接实现,具体如下。
2.3.1 最低成本连通所有城市
LeetCode 1135. 最低成本联通所有城市
想象一下你是个城市基建规划者,地图上有 N 座城市,它们按以 1 到 N 的次序编号。
给你一些可连接的选项 conections,其中每个选项 conections[i] = [city1, city2, cost] 表示将城市 city1 和城市 city2 连接所要的成本。(连接是双向的,也就是说城市 city1 和城市 city2 相连也同样意味着城市 city2 和城市 city1 相连)。
返回使得每对城市间都存在将它们连接在一起的连通路径(可能长度为 1 的)最小成本。该最小成本应该是所用全部连接代价的综合。如果根据已知条件无法完成该项任务,则请你返回-1。
unionfind的实现:
class UF
{
public:
UF(int n)
{
count = n;
parent = new int[n];
size = new int[n];
for (int i = 0; i < n; i++)
{
parent[i] = i;
size[i] = 1;
}
}
~UF()
{
delete[] parent;
delete[] size;
}
void Union(int p, int q)
{
int rootP = Find(p);
int rootQ = Find(q);
if (rootP == rootQ)
return;
// 小树接到大树下面,较平衡
if (size[rootP] > size[rootQ])
{
parent[rootQ] = rootP;
size[rootP] += size[rootQ];
}
else
{
parent[rootP] = rootQ;
size[rootQ] += size[rootP];
}
count--;
}
bool Connected(int p, int q)
{
int rootP = Find(p);
int rootQ = Find(q);
return rootP == rootQ;
}
int Find(int x)
{
while (parent[x] != x)
{
// 进行路径压缩
parent[x] = parent[parent[x]];
x = parent[x];
}
return x;
}
int Count()
{
return count;
}
private:
// 连通分量个数
int count;
// 存储一棵树
int* parent;
// 记录树的“重量”
int* size;
};
class Solution {
public:
int minimumCost(int n, vector<vector<int>>& connections) {
// Kruskal 算法
// 城市编号为 1...n,所以初始化大小为 n + 1
UF uf(n + 1);
// 对所有边按照权重从小到大排序
//std::sort(connections.begin(), connections.end(), [](vector<int>&a, vector<int>&b) -> bool { return a[2] < b[2]; });
std::sort(connections.begin(), connections.end(), [](vector<int>& a, vector<int>& b) { return a[2] < b[2]; });
// 记录最小生成树的权重之和
int mst = 0;
for (auto& edge : connections)
{
int u = edge[0];
int v = edge[1];
int weight = edge[2];
// 若这条边会产生环,则不能加入 mst
if (uf.Connected(u, v))
{
continue;
}
// 若这条边不会产生环,则属于最小生成树
mst += weight;
uf.Union(u, v);
}
// 保证所有节点都被连通
// 按理说 uf.count() == 1 说明所有节点被连通
// 但因为节点 0 没有被使用,所以 0 会额外占用一个连通分量
return uf.Count() == 2 ? mst : -1;
}
};
2.3.2 连接所有点的最小费用
LeetCode 1584 连接所有点的最小费用
给你一个points 数组,表示 2D 平面上的一些点,其中 points[i] = [xi, yi] 。连接点 [xi, yi] 和点 [xj, yj] 的费用为它们之间的 曼哈顿距离 :|xi - xj| + |yi - yj| ,其中 |val| 表示 val 的绝对值。
请你返回将所有点连接的最小总费用。只有任意两点之间 有且仅有 一条简单路径时,才认为所有点都已连接。
这也是一个标准的最小生成树问题:每个点就是无向加权图中的节点,边的权重就是曼哈顿距离,连接所有点的最小费用就是最小生成树的权重和。
unionfind的实现同上。
class Solution {
public:
int minCostConnectPoints(vector<vector<int>>& points) {
int n = points.size();
// 生成所有边及权重
vector<vector<int>> edges;
for (int i = 0; i < n; i++)
{
for (int j = i + 1; j < n; j++)
{
int xi = points[i][0], yi = points[i][1];
int xj = points[j][0], yj = points[j][1];
// 用坐标点在 points 中的索引表示坐标点
edges.push_back(vector<int>{i, j, abs(xi - xj) + abs(yi - yj)});
}
}
// 将边按照权重从小到大排序
std::sort(edges.begin(), edges.end(), [](vector<int>&a, vector<int>&b) {return a[2] < b[2];});
// 执行 Kruskal 算法
int mst = 0;
UF uf(n);
for (auto& edge : edges)
{
int u = edge[0];
int v = edge[1];
int weight = edge[2];
// 若这条边会产生环,则不能加入 mst
if (uf.Connected(u, v))
{
continue;
}
// 若这条边不会产生环,则属于最小生成树
mst += weight;
uf.Union(u, v);
}
return mst;
}
};
3 prim算法
3.1prim算法描述
在prim算法中,集合A是一棵树。每次加入到A中的安全边永远是连接A和A之外某个节点的边中权重最小的边。
这里说A是一棵树,A在初始化时仅为含有一个节点的树,然后再此树上不断地增加权重最小的边到此树中,在这个过程中始终保持A是一颗树,直到A中包含图中的所有节点。
3.2 prim算法实现原理
上图是算法导论的一个描述,实际上的实现与这个本质上是一样,在如下的题目中体现。
3.3 prim算法代码实现
3.3.1 最低成本连通所有城市
LeetCode 1135. 最低成本联通所有城市
想象一下你是个城市基建规划者,地图上有 N 座城市,它们按以 1 到 N 的次序编号。
给你一些可连接的选项 conections,其中每个选项 conections[i] = [city1, city2, cost] 表示将城市 city1 和城市 city2 连接所要的成本。(连接是双向的,也就是说城市 city1 和城市 city2 相连也同样意味着城市 city2 和城市 city1 相连)。
返回使得每对城市间都存在将它们连接在一起的连通路径(可能长度为 1 的)最小成本。该最小成本应该是所用全部连接代价的综合。如果根据已知条件无法完成该项任务,则请你返回-1。
class Solution {
public:
int minimumCost(int n, vector<vector<int>>& connections) {
// Prim 算法
//重载priority_queue的比较函数,priority_queue默认是大顶堆,重载的是<号
//默认情况下如果左边参数大于右边参数,则说明左边形参的优先级低于右边形参,会将左边的放到后面
//构建小顶堆时,我们实现一个>号的判断即可,大于返回true,优先级低,被放到后面,则小的会放前面
struct cmp
{
//[0]: from, [1]: to, [2]: weight
bool operator () (const vector<int>& a, const vector<int>& b)
{
return a[2] > b[2];
}
};
int selected = 0, ans = 0;
//图的邻接边实现,第一维是起点,二维是<终点、开销>
vector<vector<pair<int, int>>> edges(n+1);
//构建基于权重的最小堆
priority_queue<vector<int>, vector<vector<int>>, cmp> minHeap;
vector<int> visit(n+1, 0);
//初始化边集合
for (auto& conn : connections)
{
edges[conn[0]].push_back(make_pair(conn[1], conn[2]));
edges[conn[1]].push_back(make_pair(conn[0], conn[2]));
}
//本次选择1为起点,如果1点没有邻接边,则1永远是孤岛。本次选择1为起点
if (edges[1].size() == 0)
{
return -1;
}
selected = 1;
visit[1] = true;
//将起点1的邻接边插入到基于权重的最小堆中
for (int i = 0; i < edges[1].size(); ++i)
{
//[0]: from, [1]: to, [2]: weight
minHeap.push(vector<int>({ 1, edges[1][i].first, edges[1][i].second }));
}
//遍历最小堆
while (!minHeap.empty())
{
auto curr = minHeap.top(); minHeap.pop();
if (!visit[curr[1]])
{
visit[curr[1]] = true;
ans += curr[2];
//将 curr 的邻接边插入到基于权重的最小堆中
for (auto& e : edges[curr[1]])
{
minHeap.push(vector<int>({ curr[1], e.first, e.second }));
}
selected++;
if (selected == n)
{ //如果n个节点都在时,则结束循环
return ans;
}
}
}
return -1;
}
};
3.3.2 连接所有点的最小费用
LeetCode 1584 连接所有点的最小费用
给你一个points 数组,表示 2D 平面上的一些点,其中 points[i] = [xi, yi] 。连接点 [xi, yi] 和点 [xj, yj] 的费用为它们之间的 曼哈顿距离 :|xi - xj| + |yi - yj| ,其中 |val| 表示 val 的绝对值。
请你返回将所有点连接的最小总费用。只有任意两点之间 有且仅有 一条简单路径时,才认为所有点都已连接。
这也是一个标准的最小生成树问题:每个点就是无向加权图中的节点,边的权重就是曼哈顿距离,连接所有点的最小费用就是最小生成树的权重和。
class Solution {
public:
int minCostConnectPoints(vector<vector<int>>& points) {
int n = points.size(), selected = 0, ans = 0;
//图的邻接边实现,第一维是起点,二维是<终点、开销>
vector<vector<pair<int, int>>> edges(n);
for (int i = 0; i < n; i++)
{
for (int j = i + 1; j < n; j++)
{
int xi = points[i][0], yi = points[i][1];
int xj = points[j][0], yj = points[j][1];
edges[i].push_back(make_pair(j, abs(xi - xj) + abs(yi - yj)));
}
}
struct cmp
{
//[0]: from, [1]: to, [2]: weight
bool operator () (const vector<int>& a, const vector<int>& b)
{
return a[2] > b[2];
}
};
//构建基于权重的最小堆
priority_queue<vector<int>, vector<vector<int>>, cmp> minHeap;
vector<int> visit(n, 0);
//本次选择0为起点,如果0点没有邻接边,则0永远是孤岛。本次选择0为起点
if (edges[0].size() == 0)
{
return -1;
}
selected = 1;
visit[0] = true;
//将起点1的邻接边插入到基于权重的最小堆中
for (int i = 0; i < edges[0].size(); ++i)
{
//[0]: from, [1]: to, [2]: weight
minHeap.push(vector<int>({ 0, edges[0][i].first, edges[0][i].second }));
}
//遍历最小堆
while (!minHeap.empty())
{
auto curr = minHeap.top(); minHeap.pop();
if (!visit[curr[1]])
{
visit[curr[1]] = true;
ans += curr[2];
//将 curr 的邻接边插入到基于权重的最小堆中
for (auto& e : edges[curr[1]])
{
minHeap.push(vector<int>({ curr[1], e.first, e.second }));
}
selected++;
if (selected == n)
{ //如果n个节点都在时,则结束循环
return ans;
}
}
}
return ans;
}
};
4 单独实现prim算法
class Prim {
private:
struct cmp {
bool operator()(const pair<int, int>& a, const pair<int, int>& b) { return a.second > b.second; }
};
// 核心数据结构,存储「横切边」的优先级队列
priority_queue < pair<int, int>, vector<pair<int, int>>, cmp> minHeap;
// 类似 visited 数组的作用,记录哪些节点已经成为最小生成树的一部分
vector<bool> inMST;
bool allCover = false;
// 记录最小生成树的权重和
int weightSum = 0;
// graph 是用邻接表表示的一幅图,原来的邻接表保存的是邻接节点,这里保存的是邻接边,邻接边包含一个终点和一个权重
// graph[s] 记录节点 s 所有相邻的边,s就是这条边的起点,pair{to, weight}
vector<vector<pair<int, int>>> graph;
public:
//为了避免图中有孤立的节点,建议传入图中节点的数量
Prim(int n, vector<vector<pair<int, int>>>& graph)
{
this->graph = graph;
// 图中有 n 个节点
int selected = 0;
inMST.assign(n, false);
// 随便从一个点开始切分都可以,我们不妨从节点 0 开始
selected = 1;
inMST[0] = true;
Cut(0);
// 不断进行切分,向最小生成树中添加边
while (!minHeap.empty())
{
auto edge = minHeap.top();
minHeap.pop();
int to = edge.first;
int weight = edge.second;
if (inMST[to])
{
// 节点 to 已经在最小生成树中,跳过
// 否则这条边会产生环
continue;
}
// 将边 edge 加入最小生成树
weightSum += weight;
inMST[to] = true;
++selected;
if (selected == n)
{
allCover = true;
}
// 节点 to 加入后,进行新一轮切分,会产生更多横切边
Cut(to);
}
}
// 将 s 的横切边加入优先队列
void Cut(int s)
{
// 遍历 s 的邻边
for (auto& edge : graph[s])
{
int to = edge.first;
if (inMST[to])
{
// 相邻接点 to 已经在最小生成树中,跳过
// 否则这条边会产生环
continue;
}
// 加入横切边队列
minHeap.push(edge);
}
}
// 最小生成树的权重和
int WeightSum()
{
return weightSum;
}
// 判断最小生成树是否包含图中的所有节点
bool AllConnected()
{
return allCover;
}
};
4.1 最低成本连通所有城市
class Solution {
public:
int minimumCost(int n, vector<vector<int>>& connections) {
//图的邻接边实现,第一维是起点,二维是<终点、开销>
vector<vector<pair<int, int>>> edges(n);
//初始化边集合
for (auto& conn : connections)
{
//城市的编号从1开始,所以这里进行了减一处理
edges[conn[0]-1].push_back(make_pair(conn[1]-1, conn[2]));
edges[conn[1]-1].push_back(make_pair(conn[0]-1, conn[2]));
}
Prim prim(edges);
if (!prim.AllConnected())
{
// 最小生成树无法覆盖所有节点
return -1;
}
return prim.WeightSum();
}
};
4.2 连通所有点的最小费用
class Solution {
public:
int minCostConnectPoints(vector<vector<int>>& points) {
int n = points.size();
//图的邻接边实现,第一维是起点,二维是<终点、开销>
vector<vector<pair<int, int>>> edges(n);
for (int i = 0; i < n; i++)
{
for (int j = i + 1; j < n; j++)
{
int xi = points[i][0], yi = points[i][1];
int xj = points[j][0], yj = points[j][1];
edges[i].push_back(make_pair(j, abs(xi - xj) + abs(yi - yj)));
}
}
Prim prim(edges);
return prim.WeightSum();
}
};
4.3