目录
一、并查集基础概念
1、并查集的作用
- 并:将一个元素添加到一个集合中
- 查:查找元素所在的集合
2、并查集模板
int n; // 节点数量
vector<int> father(n, 0);
void init() // 初始化
{
// 最开始时,每个节点的根都是自己
for (int i = 0; i < n; i++)
{
father[i] = i;
}
}
int find(int u) // 寻找一个节点的根
{
if (u == father[u])
{
return u; // 自己就是根,直接返回
}
else
{
return father[u] = find(father[u]); // 自己不是根,层层递归并把寻找路径压缩
}
}
bool isSame(int u, int v) // 判断两个节点是否在同一根下
{
u = find(u);
v = find(v);
return u == v;
}
void join(int u, int v) // 合并操作
{
u = find(u);
v = find(v);
if (u == v)
{
return; // 已经是同一根下,无需合并
}
father[v] = u; // 把 v 的根置为 u
}
二、寻找图中是否存在路径(并查集)
原题力扣1971 . - 力扣(LeetCode)
等价于求起点和终点是否在同一个图中,即同一个根下
int find(vector<int>& father, int u)
{
if (u == father[u])
{
return u;
}
else
{
return father[u] = find(father, father[u]);
}
}
bool isSame(vector<int>& father, int u, int v)
{
u = find(father, u);
v = find(father, v);
return u == v;
}
void join(vector<int>& father, int u, int v)
{
u = find(father, u);
v = find(father, v);
if (u == v)
{
return;
}
father[v] = u;
}
bool validPath(int n, vector<vector<int>>& edges, int source, int destination)
{
vector<int> father(n, 0);
for (int i = 0; i < n; i++)
{
father[i] = i;
}
for (const auto& edge : edges)
{
join(father, edge[0], edge[1]);
}
return isSame(father, source, destination);
}
三、冗余连接(并查集)
原题力扣684 . - 力扣(LeetCode)
模板代码冗长且是一摸一样的,略过
vector<int> findRedundantConnection(vector<vector<int>>& edges)
{
int n = edges.size();
vector<int> father(n + 1, 0);
for (int i = 0; i <= n; i++)
{
father[i] = i;
}
for (const auto& edge : edges)
{
if (isSame(father, edge[0], edge[1]))
{
return edge; // 两个节点已经在同一根下,说明是冗余的,可以删除
}
else
{
join(father, edge[0], edge[1]);
}
}
return {};
}
四、冗余连接II(并查集)
原题力扣685 力扣. - 力扣(LeetCode)
参考网站 代码随想录
// 找到构成环的边,即要删除的边
vector<int> removeEdge(vector<int>& father, vector<vector<int>>& edges)
{
for (const auto& edge : edges)
{
if (isSame(father, edge[0], edge[1])) // 构成环了,此时就是要删除的边
{
return edge;
}
else
{
join(father, edge[0], edge[1]);
}
}
return {};
}
// 判断删除边后是否为一颗树
bool isTree(vector<int>& father, vector<vector<int>>& edges, int toDelete)
{
for (int i = 0; i < edges.size(); i++)
{
if (i == toDelete) // 跳过要删除的边
{
continue;
}
if (isSame(father, edges[i][0], edges[i][1])) // 构成环了,一定不是树
{
return false;
}
join(father, edges[i][0], edges[i][1]);
}
return true;
}
vector<int> findRedundantDirectedConnection(vector<vector<int>>& edges)
{
int n = edges.size();
vector<int> father(n + 1, 0);
for (int i = 0; i <= n; i++)
{
father[i] = i;
}
vector<int> inDegree(n + 1, 0); // 记录所有节点的入度
for (const auto& edge : edges)
{
inDegree[edge[1]]++;
}
vector<int> inEqualTwo; // 记录使节点入度为 2 的边
for (int i = n - 1; i >= 0; i--) // 注意倒序,因为要输出最后的边
{
if (inDegree[edges[i][1]] == 2)
{
inEqualTwo.push_back(i);
}
}
// 存在使节点入度为 2 的边,要删除的边必定在此中
if (inEqualTwo.size() > 0)
{
// 如果删除后仍然是树,则是答案
if (isTree(father, edges, inEqualTwo[0]))
{
return edges[inEqualTwo[0]];
}
else
{
return edges[inEqualTwo[1]];
}
}
// 不存在入度为 2 的边,说明情况同冗余连接I
return removeEdge(father, edges);
}
五、最小生成树
原题卡码53 53. 寻宝(第七期模拟笔试)
1、prim算法(维护点集)
prim算法步骤:
- 选择距离生成树最近的非树中节点
- 把节点加入树中
- 更新非树中节点到生成树的距离(即更新minDist数组)
时间复杂度:O(n^2),其中 n 为节点个数,适用稠密图(边数量较多)
int minimumSpanningTreePrim()
{
int v, e;
cin >> v >> e;
vector<vector<int>> graph(v + 1, vector<int>(v + 1, 100001));
int v1, v2, val;
for (int i = 0; i < e; i++)
{
cin >> v1 >> v2 >> val;
graph[v1][v2] = val;
graph[v2][v1] = val;
}
vector<int> minDist(v + 1, 100001); // 记录非树中节点到生成树的距离
vector<bool> visited(v + 1, false); // 记录是否遍历过
for (int i = 1; i < v; i++)
{
int cur = -1; // 重置当前选择的节点
int minVal = INT_MAX; // 重置当前选择的节点的最小距离
for (int j = 1; j <= v; j++)
{
// 找到了更近的节点,选择此节点,并更新参数
if (!visited[j] && minDist[j] < minVal)
{
minVal = minDist[j];
cur = j;
}
}
// 将节点标记为已遍历
visited[cur] = true;
// 更新 minDist 数组
for (int j = 1; j <= v; j++)
{
if (!visited[j] && graph[cur][j] < minDist[j])
{
minDist[j] = graph[cur][j];
}
}
}
// 求最小生成树的路径总和
int ans = 0;
for (int i = 2; i <= v; i++)
{
ans += minDist[i];
}
cout << ans << endl;
return 0;
}
2、kruskal算法(维护边集)
kruskal算法步骤:
- 边的权值排序,因为要优先选最小的边加入到生成树里
- 遍历排序后的边
- 如果边首尾的两个节点在同一个集合,说明如果连上这条边图中会出现环
- 否则,加入到最小生成树,并把两个节点加入同一个集合
时间复杂度:O(nlogn),其中 n 为节点个数,适用稀疏图(边数量较少)
// 表示一条边:左端点,右端点,权值
struct Edge
{
int l, r, val;
};
int find(vector<int>& father, int u)
{
if (u == father[u])
{
return u;
}
else
{
return father[u] = find(father, father[u]);
}
}
void join(vector<int>& father, int u, int v)
{
u = find(father, u);
v = find(father, v);
if (u == v)
{
return;
}
father[v] = u;
}
int minimumSpanningTreeKruscal()
{
int v, e;
cin >> v >> e;
vector<Edge> edges;
int v1, v2, val;
for (int i = 0; i < e; i++)
{
cin >> v1 >> v2 >> val;
edges.push_back({v1,v2,val});
}
// 按边权值排序
sort(edges.begin(), edges.end(), [&](const auto& a, const auto& b) {
return a.val < b.val;
});
int ans = 0;
vector<int> father(v + 1, -1);
for (int i = 0; i <= v; i++)
{
father[i] = i;
}
for (const auto& edge : edges)
{
int x = find(father, edge.l);
int y = find(father, edge.r);
// 两个节点不是同一根,则加入生成树
if (x != y)
{
ans += edge.val;
join(father, x, y);
}
}
cout << ans << endl;
return 0;
}
六、课程表II(拓扑排序)
原题力扣207 . - 力扣(LeetCode)
步骤:
- 找到入度为0的节点,加入结果集
- 从图中删除该点
- 循环直至完成
判断图中有环的方法:最终如果结果集的节点数不等于总节点数,说明有环
vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites)
{
vector<int> inDegree(numCourses, 0); // 记录各节点入度
unordered_map<int, vector<int>> mp; // 记录课程依赖关系,<前置课程,<后置课程>>
// [ai,bi] 的关系:bi->ai,bi是前置课程,ai是后置课程
for (const auto& prerequisite : prerequisites)
{
inDegree[prerequisite[0]]++; // 后置课程入度增加
mp[prerequisite[1]].push_back(prerequisite[0]); // 记录依赖关系
}
queue<int> que; // 记录入度为 0 的节点
for (int i = 0; i < numCourses; i++)
{
if (inDegree[i] == 0)
{
que.push(i); // 入度为 0 的节点说明无前置,可入队
}
}
vector<int> ans;
while (!que.empty())
{
int cur = que.front();
que.pop();
ans.push_back(cur); // 当前节点放入结果集中
vector<int> temp = mp[cur]; // 当前节点的后置节点
if (temp.size() > 0)
{
for (int i = 0; i < temp.size(); i++)
{
inDegree[temp[i]]--; // 当前节点的后置节点入度减 1
if (inDegree[temp[i]] == 0)
{
que.push(temp[i]); // 入度为 0 的节点说明无前置,可入队
}
}
}
}
// 结果集的大小等于课程总数,说明无环,所有课程都能学
if (ans.size() == numCourses)
{
return ans;
}
return {};
}