题目
547. 省份数量
有 n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。
省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。
给你一个 n x n 的矩阵 isConnected ,其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。
返回矩阵中 省份 的数量。
示例 1:
输入:isConnected = [[1,1,0],[1,1,0],[0,0,1]]
输出:2
示例 2:
输入:isConnected = [[1,0,0],[0,1,0],[0,0,1]]
输出:3
思路1:
图表的深度优先搜索
- isConnected的长度代表的是城市的总数
- 申明一个Set对象visited,用于存放遍历过的城市和于当前城市相连的城市,这类被记录下来的城市后序遍历会跳过。
- 申明一个计数器count,用于记录省份的数量
- 遍历isConnected中每一个城市,
- 如果当前城市i没有遍历过,则递归深度查找,
- 如果发现有城市j和i相连,并且这个城市j没有被visted记录过,则将城市j放入visted中,
- 再递归的去查找城市j,是否有其他城市和它相连,直到没有为止,计数器count+1
- 最终返回这个count即可
代码如下:
/**
* @param {number[][]} isConnected
* @return {number}
*/
var findCircleNum = function(isConnected) {
let cityNum = isConnected.length;
let visited = new Set();
let count = 0;
for(let i=0;i<cityNum;i++){
if(!visited.has(i)){
dfs(isConnected,visited,cityNum,i);
count++
}
}
return count;
};
function dfs(isConnected,visited,cityNum,i){
for(let j=0;j<cityNum;j++){
if(isConnected[i][j] == 1 && !visited.has(j)){
visited.add(j);
dfs(isConnected,visited,cityNum,j)
}
}
}
复杂度分析:
时间复杂度:O(n^2),需要遍历每一个城市的同时,再去遍历所有城市,可以等同于循环套循环。
空间复杂度:O(n),这里vistedCity存放的城市的长度为n,另外递归调用栈的深度也不会超过n。
思路2:
图表的广度优先搜索
- isConnected的长度代表的是城市的总数
- 申明一个Set对象visited,用于存放遍历过的城市和于当前城市相连的城市,这类被记录下来的城市后序遍历会跳过。
- 申明一个计数器count,用于记录省份的数量
- 申明一个队列,用于存储待处理的城市
- 遍历isConnected
- 如果当前城市i没有处理过,那么将它加入队列
- 从队列取出头元素,并将当前城市放入visited,
- 再去遍历isConnected,如果有城市和当前城市相连,则放入队列
- 只要队列不为空,则一直处理下去,直到队列为空,则count+1,代表与城市i相连的所有城市都已经被放入了visited里了
- 最终返回count即位省份数
代码如下:
/**
* @param {number[][]} isConnected
* @return {number}
*/
var findCircleNum = function(isConnected) {
let cityNum = isConnected.length;
let visited = new Set();
let count = 0;
let queue = [];
for(let i=0;i<cityNum;i++){
if(!visited.has(i)){
queue.push(i);
while(queue.length>0){
let j = queue.shift();
visited.add(j);
for(let k=0;k<isConnected.length;k++){
if(isConnected[j][k] == 1 && !visited.has(k)){
queue.push(k)
}
}
}
count++;
}
}
return count;
};
复杂度分析
时间复杂度:O(n^2),本质上每一个城市都去和所有城市比较一遍,就是n*n;
空间复杂度:O(n), 需要用visited记录所有的城市,即为n,另外队列的长度最多为n。
思路3:
并查集
我们可以利用并查集的数据结构,初始的连通量为isConnected的长度,如果两个城市相连,则将两个城市连接,最终计算连通量即可。
这里先要去了解并查集是什么,推荐看下这两篇文章,讲的很好。
- 创建parent数组,用于记录城市之间的连接关系,初始化parent,元素都指向自己
- 创建连通量count,用于记录有多少个集合
- 创建连接方法union,将一个节点的根节点指向另一个节点的根节点
- 创建查找根节点方法find,这里用上路径压缩手段,最后扁平化处理,每个集合里,所有元素除了根节点,大家都在一层。
- 遍历isConnected数组,判断城市两两是否相邻,如果相邻,则将这两个节点在parent数组中连接
- 遍历parent,如果节点是跟节点,则count++
- 返回连通量count
代码如下:
/**
* @param {number[][]} isConnected
* @return {number}
*/
var findCircleNum = function(isConnected) {
let count = 0;
let len = isConnected.length;
// 处理parent数组,初始化时,每个元素都指向自己
let parent = new Array(len).fill(0).map((item,index)=>index)
for(let i=0;i<len;i++){
for(let j=i+1;j<len;j++){
// 判断i和j,如果相连,则将其连接
if(isConnected[i][j] == 1){
union(parent,i,j);
}
}
}
for(let i=0;i<parent.length;i++){
if(parent[i] == i){
count++;
}
}
return count;
};
function union(parent,p,q){
// 连接p、q,将p的根节点指向q的根节点即可,反过来q的指向p也行
let rootP = find(parent,p);
let rootQ = find(parent,q);
parent[rootP] = rootQ;
}
function find(parent,x){
// 找x的根节点,并且压缩路径,将找到的根节点直接当作x的父节点
if(parent[x] != x){
parent[x] = find(parent,parent[x])
}
return parent[x];
}
复杂度分析
时间复杂度:O(n^2logn),遍历isConnected为n^2,遍历的同时要合并与查找,合并为O(1),查找我们这里使用了路径优化,在find查找父节点时时间复杂度为logn,所以总共为O(n^2logn)
推荐看一下大佬这篇并查集时间复杂度的分析
空间复杂度:O(n),parent要存储所有节点的父节点情况。