PS:算法并非原创,仅作个人学习记录使用,侵删。
题目描述
算法思路分析
首先看到这道题目的时候,个人的想法就是使用图论里面的“连通分量”这一个概念,这在最小生成树的prim算法中也有使用到。所以就用一个二维矩阵存储邻接矩阵,然后再用一个联通数组指明哪些结点属于一个连通分量,之后再统一计数即可。
而通过浏览前辈们的算法思想之后,这道题目的解题算法主要有三类:
深度优先遍历(dfs),广度优先遍历(bfs),并查集,
其中深度优先和广度优先有思想上的相似性,先建立好图的连接,然后遍历顶点集,当遇到一个新的结点时,以它为组长找到剩余的组内结点,并做好标记,不同的只是找到剩余组内结点的过程。
而并查集思想则是先将它们全部分散为单独的组,之后如果有联系,则合并两个结点。在这个合并的过程中,组长永远是小组的最后一个成员,而之前的成员会一个指向一个,最终指向组长的标号。
代码实现:
【C】
/*
算法简介:
深度优先遍历方法
通过visitd数组,从头遍历城市数组,每个城市都通过深度遍历查找所有连接城市,并将这些城市做上“记号”。
这样每个连通分量只统计一次,且每个连通分量都不会有遗漏
*/
void dfs(int** isConnected, int* visited, int provinces, int i) {//递归算法
//邻接矩阵——isConnected
//访问数组——visited
//城市数量——provinces
//i当前城市编号
for (int j = 0; j < provinces; j++) {//遍历所有城市
if (isConnected[i][j] == 1 && !visited[j]) {//如果城市i和遍历到的城市j之间有连接,并且j没有访问过
visited[j] = 1;//j和i合并,此刻把j置为“已访问”,之后就不会在涉及它的连通分量查找。
//j与i直接连接
dfs(isConnected, visited, provinces, j);//再寻找与城市i间接连接的城市,可以想见,这些城市也会设置为“已访问”,之后不会再加入统计
}
}
}
int findCircleNum(int** isConnected, int isConnectedSize, int* isConnectedColSize) {
//isConnected表示二维邻接矩阵;
//isConnetedSize为矩阵大小,即n;
//isConnectedColSize为每行的长度
int provinces = isConnectedSize;//省数量
int visited[provinces];//表示某节点有没有是否访问过
memset(visited, 0, sizeof(visited));//初始化visited访问数组,全部置0
int circles = 0;//连通分量的数量
for (int i = 0; i < provinces; i++) {//遍历所有的结点,
if (!visited[i]) {//当某个结点已经访问过,跳过,当某个结点还没有访问过,进行dfs深度遍历
dfs(isConnected, visited, provinces, i);//寻找i的所有连接城市(直接+间接)
circles++;
}
}
return circles;
}
【C++】
/*
并查集思想:
初始时,每个城市都属于不同的连通分量。
遍历矩阵isConnected,如果两个城市之间有相连关系,则它们属于同一个连通分量,对它们进行合并。
*/
class Solution {
public:
int Find(vector<int>& parent, int index) {//查找某城市的“队长”
if (parent[index] != index) {//如果是组员的话
parent[index] = Find(parent, parent[index]);//组员的组长不再是自己,而是组长的编号。
}
return parent[index];
}
void Union(vector<int>& parent, int index1, int index2) {//合并函数,
//parent数组,index1和index2表示两个需要合并的连通分量。
parent[Find(parent, index1)] = Find(parent, index2);//parent[城市1的父节点编号] = 城市2的父节点编号
/*
城市5和城市1,城市3都相同,城市7和城市5相通,那么
parent[1]=3,parent[3]=5, parent[5[]=7,parent[7]=7(队长);
*/
}
int findCircleNum(vector<vector<int>>& isConnected) {//主要算法
int cities = isConnected.size();//城市数量是矩阵的大小(行数)
vector<int> parent(cities);//vector(int nSize):创建一个vector结构,元素个数为nSize
//最开始每一个都是单独的集合,parent表示他们小组的“队长”,一开始队长都是自己
for (int i = 0; i < cities; i++) {
parent[i] = i;
}
//进行并查集的合并
for (int i = 0; i < cities; i++) {//遍历城市
for (int j = i + 1; j < cities; j++) {//遍历城市i之后的后续城市
if (isConnected[i][j] == 1) {//i,j两城市间有联通则合并
Union(parent, i, j);
}
}
}
int provinces = 0;//最终的省份数量
for (int i = 0; i < cities; i++) {//遍历所有城市
if (parent[i] == i) {//找到“队长”的话,组数++
provinces++;
}
//如果找到的不是组长,而是组员,那么直接跳过。
}
return provinces;//返回省份数量
}
};
【JAVA】
/*
广度优先遍历
通过广度优先搜索的方法得到省份的总数。
对于每个城市,如果该城市尚未被访问过,则从该城市开始广度优先搜索,直到同一个连通分量中的所有城市都被访问到,即可得到一个省份。
*/
class Solution {
public int findCircleNum(int[][] isConnected) {
int n=isConnected.length;//城市数量
boolean[] visited=new boolean[n];//访问数组
LinkedList<Integer> list=new LinkedList<>();//链表,此处用作栈
/*
ArrayList 采用的是数组bai形式来保存对象的,这du种方式将对象放在连zhi续的位dao置中,所以最zhuan大的缺点就是插入删除时shu非常麻烦
LinkedList 采用的将对象存放在独立的空间中,而且在每个空间中还保存下一个链接的索引 但是缺点就是查找非常麻烦 要从第一个索引开始.
所以 LinkedList 在逻辑上是有序的,会按照你的插入顺序存储的。但物理上是不连续的。,也就是链表
*/
int count=0;//总的省份数量
//开始遍历
for(int i=0;i<n;i++){//遍历所有城市
//如果之间没有访问过
if(visited[i]==false){
list.addLast(i);//添加在末尾的位置
while(!list.isEmpty()){//当栈中还有没处理的顶点
int a=list.removeFirst();//取出栈顶的顶点
visited[a]=true;//该顶点设为访问过
for(int j=0;j<n;j++){//遍历成熟
if(isConnected[a][j]==1&&visited[j]==false){//所有和顶点相连并且没有访问过的城市,都加入栈中
list.addLast(j);
}
}
}//处理栈结束,这时栈为空,i的所有连接城市都已经设为已访问,进入下一轮循环,在此之前,统计省份数量
count++;//
}
//如果之前已经访问过了城市i,那么跳过该城市
}
return count;
}
}
Java参考网址
【python】
##图的深度优先遍历(dfs)
class Solution:
def findCircleNum(self, isConnected):
n,ans = len(isConnected),0#n——城市数量,ans——最终结果
visited = set()#访问数字
def dfs(i):#深度优先遍历函数定义
visited.add(i)#i标记为已经访问
for j in range(n):#遍历城市,找到和i相连的未访问城市,继续深度遍历
if isConnected[i][j]==1 and j not in visited:
dfs(j)
for i in range(n):#遍历城市
if i not in visited:#尚未访问的城市表示一个新的连通分量
ans += 1#省份+1
dfs(i)#找到i的所有连接城市,做好标记
return ans