地址:
力扣https://leetcode-cn.com/problems/number-of-provinces/
题目:
有 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 <= n <= 200
n == isConnected.length
n == isConnected[i].length
isConnected[i][j] 为 1 或 0
isConnected[i][i] == 1
isConnected[i][j] == isConnected[j][i]
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/number-of-provinces
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路:
题目有些多字,我们举个例子:
城市个数:6
城市关系值:城市之间如果直接连接,值=1,否则=0
a | b | c | d | e | f | |
a | 1 | 1 | 1 | 0 | 0 | 0 |
b | 1 | 1 | 0 | 1 | 0 | 0 |
c | 1 | 0 | 1 | 0 | 0 | 0 |
d | 0 | 1 | 0 | 1 | 0 | 0 |
e | 0 | 0 | 0 | 0 | 1 | 1 |
f | 0 | 0 | 0 | 0 | 1 | 1 |
直接连接或者间接连接都算作连接,这些城市就划定为一个省,依据二维数组来求得省份数量
这类题目可以变换为朋友圈数量,亲戚数量,换个名字而已,只要有关系就是一个单位
方法一、深度优先 DFS
以上图为例,起点就选用第一个城市 a,依据 a 的城市关系 [1,1,1,0,0,0] ,相连的是城市 b
继续以 b 为起点,依据 b 的城市关系 [1,1,0,1,0,0],相连的是城市 d
继续以 d 为起点,依据 d 的城市关系,没有新的城市相连,那么这条线
a->b->d 就截止,开始继续找 a 城市关系相连的下一个新城市 c
所有走过的城市我们会标注,这样寻找时不会走回头路,整个以 a 为起点的连接我们就找完了,标注也打完了,剩下没有打标注的表明不属于 a 代表的省会
继续以第二个城市 b 为起点,如此大循环,因为前序有些已结打了标注,所以,这样的遍历可能不会有标注
遍历完所有的城市,我们的标注也就全部打完了,每一次以某点开始的遍历,完成后表明一个省会的确认
void dfsFindCircle(int** isConnected, int cityNum, int *markArr, int start)
{
int i;
for(i=0; i<cityNum; i++)
{
//printf("in....start=%d, i=%d, markArr[%d]=%d, isConnected[%d][%d]=%d\n", start, i, i, markArr[i], start, i, isConnected[start][i]);
if(markArr[i] != 1)
{
if(isConnected[start][i] == 1)
{
markArr[i] = 1;
dfsFindCircle(isConnected, cityNum, markArr, i);
}
}
}
}
int findCircleNum(int** isConnected, int isConnectedSize, int* isConnectedColSize){
int i,j;
int cityNum = *isConnectedColSize;
int findNum = 0;
int markLen = cityNum;
int *markArr = (int *)malloc(sizeof(int) * markLen);
for(i=0; i<markLen; i++)
markArr[i] = 0;
for(i=0; i<cityNum; i++)
{
//printf("out....%d, findNum=%d\n", i, findNum);
if(markArr[i] != 1)
{
// DFS
dfsFindCircle(isConnected, cityNum, markArr, i);
findNum++;
}
}
free(markArr);
return findNum;
}
方法二、广度优先 BFS
仔细观察城市图,其实与之前的二叉树很像,不过这里可能是多叉树
我们在二叉树的层序遍历时(二叉树遍历之层序遍历,111. 二叉树的最小深度),用队列来进行。
这里找城市的连接也是一样
以城市 a 为起点,依据城市关系,与之直接连接的是城市 b,c,这是第一圈城市
分别找以 b,c 为起点的直接连接城市,找到了 d,这是第二圈城市
没有第三圈城市,后续e,f一样的逻辑
入队顺序一样:
1. a 入队
2. 队列不为空时循环处理
2.1 出队 a
2.2 直接连接城市 b,c,入队b,c
2.3 出队 b,直接连接城市 d
2.4 出队 c,没有直接连接城市
2.5 出队 d,没有直接连接城市
一次循环完成,这个就是找到的省份,开始下一个城市为起点依据上面的逻辑
int enqueue(int *queue, int len, int *rear, int cityIdx)
{
//printf("enqueue..rear=%d, len=%d, cityIdx=%d\n", *rear, len, cityIdx);
if(*rear == len)
return -1;
queue[*rear] = cityIdx;
(*rear)++;
return 0;
}
int dequeue(int *queue, int len, int *front, int *rear)
{
if(*front == *rear)
return -1;
int idx = *front;
if(idx == len - 1)
{
*front = 0;
*rear = 0;
}
else
(*front)++;
//printf("dequeue..cityIdx=%d\n", queue[idx]);
return queue[idx];
}
int isEmpty(int front, int rear)
{
return front == rear ? 1:0;
}
void checkMarkArr(int *markArr, int markLen)
{
int i;
printf("---checkMarkArr---\n");
for(i=0; i<markLen; i++)
printf("markArr[%d]=%d\n", i, markArr[i]);
}
int findCircleNum(int** isConnected, int isConnectedSize, int* isConnectedColSize){
int i,j;
int cityNum = *isConnectedColSize;
int findNum = 0;
/* mark array */
int markLen = cityNum;
int *markArr = (int *)malloc(sizeof(int) * markLen);
for(i=0; i<markLen; i++)
markArr[i] = 0;
/* queue */
int front = 0;
int rear = 0;
int queueLen = cityNum*5;
int *queueArr = (int *)malloc(sizeof(int) * queueLen);
for(i=0; i<queueLen; i++)
queueArr[i] = 0;
for(i=0; i<cityNum; i++)
{
if(markArr[i] != 1)
{
enqueue(queueArr, queueLen, &rear, i);
while(!isEmpty(front, rear))
{
int cityIdx = dequeue(queueArr, queueLen, &front, &rear);
markArr[cityIdx] = 1;
for(j=0; j<cityNum; j++)
{
if(isConnected[cityIdx][j] == 1 && markArr[j] != 1)
enqueue(queueArr, queueLen, &rear, j);
}
}
// printf("checkMarkArr----%d\n", i);
// checkMarkArr(markArr, markLen);
findNum++;
}
}
free(markArr);
free(queueArr);
return findNum;
}
注意:C语言自己造轮子就没有其他高级语言那么爽,这里队列的长度不一定是城市的数量
可能存在闭环的省会样式,这样某个城市对其直接连接城市入队,其他城市可能对同样的城市入队,队列长度就不够了。
以力扣的批量测试,2倍队列长度都不够,干脆直接上5倍,过了也没有细研了
方法三、并查集
城市的关联如同一棵树,多叉树
如果一个城市和一个城市是直接相连的,那么可以认定其中一个为树的根节点
再来一个城市
如果直接相连根节点,那么也算作根节点的子节点
如果与根节点的某一个子节点相连,那么与根节点就是间接相连,自然把其归入该树
或者这个城市本来就是另一棵树的节点,那么就是两棵树的合并
这样的逻辑关系遍历所有城市,构建出来的树的集合,就是省份的个数
我们需要构建一个表,用于表明每个节点的直接根节点:
Idx 表示城市节点
Val 表示城市节点的直接根节点
例如:城市 4 的直接根节点是 3,3 的直接跟节点是 2, 2的根节点是自己
Idx | 0 | 1 | 2 | 3 | 4 |
Val | 0 | 1 | 2 | 2 | 3 |
我们还需要构建一个表,用于表明层级,因为存在合并树的情况,为了加快寻找最终根节点的速度,我们需要把 小一点的树 合并到 大一点的树
初始值都是 1,当合并同level时,选定为根节点的值++
有新节点(树),比较level值,小值需要并入大树
示例:
有 0 ~ 14 共 15 个城市,关系图如下
int isConnected[][]=
{
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
{1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0}, // 0
{1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // 1
{0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // 2
{0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0}, // 3
{0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0}, // 4
{0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0}, // 5
{0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0}, // 6
{1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0}, // 7
{0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0}, // 8
{0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1}, // 9
{0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0}, // 10
{0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0}, // 11
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0}, // 12
{0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0}, // 13
{0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1} // 14
};
手动画出关系图:
如果 head 相同不做动作,图示也不进行说明
节点 0:连接 1,7
节点 2:孤立
节点 3:连接 5,6
节点 4:连接 9,10
节点 5:连接 10
5 这颗树与 9这棵树产生了联系,因为树大小一样,合并后level值是要增加的
节点 6:连接 8,13
6 属于 9 这棵树,所以 8,13须合入 9 这棵树
节点 7:连接 8
7 属于 1 这棵树,小于 9 这棵树,所以须合入 1 到 9
节点 9:连接 11, 14
合入 11,14
节点 12:孤立
最后生成的就是 2,9,12 三棵树,对应的构建关系:
Head:
初始值:
Idx | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
Val | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
最终值:
Idx | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
Val | 1 | 9 | 2 | 5 | 9 | 9 | 9 | 1 | 9 | 9 | 9 | 9 | 12 | 9 | 9 |
level:
初始值:
Idx | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
Val | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
最终值:
Idx | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
Val | 2 | 2 | 1 | 2 | 2 | 3 | 1 | 1 | 1 | 3 | 1 | 1 | 1 | 1 | 1 |
通过遍历 head数组,如果 Idx = Val,那么就表明这是一个树的根节点,否则就是某个树的子节点
所以只需要找到根节点个个数即可
代码实现:
int findHead(int cityIdx, int *headArr)
{
int idx;
if(headArr[cityIdx] == cityIdx)
return cityIdx;
idx = findHead(headArr[cityIdx], headArr);
return idx;
}
void updateHead(int cityOne, int cityTwo, int *headArr, int *levelArr)
{
int i = findHead(cityOne, headArr);
int j = findHead(cityTwo, headArr);
if(i == j)
return;
if(levelArr[i] >= levelArr[j])
headArr[j] = i;
else
headArr[i] = j;
if(levelArr[i] == levelArr[j])
{
levelArr[i]++;
levelArr[j]++;
}
}
int findCircleNum(int** isConnected, int isConnectedSize, int* isConnectedColSize){
int i,j;
int cityNum = *isConnectedColSize;
int findNum = 0;
int headArrLen = cityNum;
int *headArr = (int *)malloc(sizeof(int) * headArrLen);
int levelArrLen = cityNum;
int *levelArr = (int *)malloc(sizeof(int) * levelArrLen);
for(i=0; i<cityNum; i++)
{
headArr[i] = i;
levelArr[i] = 1;
}
for(i=0; i<cityNum; i++)
{
for(j=i+1; j<cityNum; j++)
{
if(isConnected[i][j] == 1)
updateHead(i, j, headArr, levelArr);
}
}
for(i=0; i<cityNum; i++)
{
if(headArr[i] == i)
findNum++;
}
free(headArr);
free(levelArr);
return findNum;
}