携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第16天,点击查看活动详情
题目描述
给你一个整数 n ,表示一张 无向图 中有 n 个节点,编号为 0 到 n - 1 。同时给你一个二维整数数组 edges ,其中 表示节点 和 之间有一条 无向 边。
请你返回 无法互相到达 的不同 点对数目 。
提示:
- 不会有重复边。
示例 1:
输入:n = 3, edges = [[0,1],[0,2],[1,2]]
输出:0
解释:所有点都能互相到达,意味着没有点对无法互相到达,所以我们返回 0 。
示例 2:
输入:n = 7, edges = [[0,2],[0,5],[2,4],[1,6],[5,4]]
输出:14
解释:总共有 14 个点对互相无法到达:
[[0,1],[0,3],[0,6],[1,2],[1,3],[1,4],[1,5],[2,3],[2,6],[3,4],[3,5],[3,6],[4,6],[5,6]]
所以我们返回 14 。
整理题意
题目给定 n 个节点,以及 m 条边,表示一张 无向图,让我们返回 不能互通 的节点对数。
题目给定的
m条边是以一个二维数组edges的形式给出,其中 表示节点 和 之间有一条 无向 边。
解题思路分析
观察题目数据范围,节点数在 以内,需要注意节点对数溢出问题。
可以知道在同一个联通分块中的节点可以相互到达,且当前联通分块中的节点所能组成的节点对数目为:sum * (sum - 1) / 2(sum 为联通分块中的节点个数)。
DFS 搜索每个连通块中的节点数
那么我们可以通过 DFS 进行搜索,统计每个联通分块中节点的个数,从而得知所有能够互通的节点对数目。由于题目求的是不能够互通的节点对数目,所以我们用 n 个节点能够组成的最大节点对数减去搜索得到的互通节点对数目即可得到不能够互通的节点对数。 n 个节点能够组成的最大节点对数为:n * (n - 1) / 2
并查集(最优解)
由于该题涉及到 连通块,我们可以使用并查集进行解决,因为并查集是用来解决图的连通性问题的。
我们遍历题目给定的边,如果当前边的两个节点节点不在同一个连通块中时,我们在合并两个连通块时利用乘法原则计算增加的互通节点对数目:sum[fy] * sum[fx];当遍历完所有边时,也就得出所有能够互通的节点对数目,从而能够计算得到不能够互通的节点对数。
具体实现
DFS 搜索每个连通块中的节点数
- 通过给定的边进行建图;
- 标记遍历到的每个节点,统计每个连通块中的节点数量;
- 通过连通块中的节点数量计算每个连通块的互通节点对数目;
- 最后用总的节点对数目减去互通节点对数目,得到不能够互通的节点对数。
并查集
- 遍历题目给定的边,使用并查集将给定边的两个节点进行连接合并;
- 在连接合并时判断两个节点是否在同一个连通块中,如果不在同一个连通块中,计算合并连通块后增加的互通节点数对的数目。
- 遍历完题目给定的边,也就得到了所有互通节点对数目,从而得到不能够互通的节点对数。
复杂度分析
- 时间复杂度:,其中
n是点的数量,m是边的数量,遍历图需要 的时间,遍历所有点需要 时间。 - 空间复杂度:,其中
n是点的数量,m是边的数量,建图需要 的空间,标记点需要 的空间。
代码实现
DFS
class Solution {
private:
vector<vector<int>> G;
long long now;
//标记数组
vector<bool> vis;
//遍历连通块
void dfs(int x){
now++;
vis[x] = true;
int sz = G[x].size();
for(int i = 0; i < sz; i++) if(!vis[G[x][i]]) dfs(G[x][i]);
}
public:
long long countPairs(int n, vector<vector<int>>& edges) {
//最多能够形成的数对数量
long long ans = (long long)n * (n - 1) / 2;
//建图
G.resize(n);
for(int i = 0; i < n; i++) G[i].clear();
int m = edges.size();
for(int i = 0; i < m; i++){
int x = edges[i][0], y = edges[i][1];
G[x].push_back(y);
G[y].push_back(x);
}
//dfs 求连通块中点的数量
vis.resize(n, false);
for(int i = 0; i < n; i++){
//now记录数量
now = 0;
if(!vis[i]) dfs(i);
//减掉当前已经形成的数对
ans -= now * (now - 1) / 2;
}
return ans;
}
};
并查集
class Solution {
private:
long long ans;
vector<int> father, sum;
//并查集模板
int find(int x){
//使用路径压缩优化
return x == father[x] ? x : father[x] = find(father[x]);
}
void unite(int x, int y){
int fx = find(x);
int fy = find(y);
//如果不是一个连通块,合并
if(fx != fy){
/* ****以下为在并查集模板上添加部分**** */
//当合并两个连通块时利用乘法原则计算互通数对数目为:sum[fy] * sum[fx]
ans -= (long long)sum[fy] * sum[fx];
//sum[i] 记录以i为根节点的每个连通包含多少个节点
sum[fy] += sum[fx];
/* ****以上为在并查集模板上添加部分**** */
father[fx] = fy;
}
}
public:
long long countPairs(int n, vector<vector<int>>& edges) {
father.resize(n);
sum.resize(n);
//初始化连通块
for(int i = 0; i < n; i++) father[i] = i, sum[i] = 1;
//ans 初始化为n个节点最大互通数对数目
ans = (long long)n * (n - 1) / 2;
//遍历边,并将两个节点连接为一个连通块
int m = edges.size();
for(int i = 0; i < m; i++){
int x = edges[i][0], y = edges[i][1];
unite(x, y);
}
return ans;
}
};
总结
- 根据题目数据范围,需要注数据大小意溢出问题。
- 遇到有图的题目,通常需要对图进行遍历和搜索,该题很容易想到通过
DFS对图进行搜索统计连通块中点的数量,但是我们由连通块可以想到使用并查集进行优化,无需遍历图即可统计连通块中节点个数。 - 在使用并查集时需要对查找路径进行「路径压缩优化」或者「平衡性优化」,一般「路径压缩优化」更为常用,当使用了「路径压缩优化」后,「平衡性优化」可以不使用,二者都是为了降低并查集中
find()函数的时间复杂度。 - 在「路径压缩优化」中用递归实现的「路径压缩」效率更高。
- 测试结果:
通过测试结果进行对比可知并查集的解法在时间复杂度和空间复杂度上都更优于
DFS 解法。
结束语
人生的意义,如果仅在于追求成功,得到的快乐或许并不会多。真正带给你成就感的是成长。克服自身的弱点,探寻未知的领域,在这种不断打磨塑造自己的过程中,更能体会到由衷的喜悦和人生的价值。新的一天,加油!