班上有 N 名学生。其中有些人是朋友,有些则不是。他们的友谊具有是传递性。如果已知 A 是 B 的朋友,B 是 C 的朋友,那么我们可以认为 A 也是 C 的朋友。所谓的朋友圈,是指所有朋友的集合。
给定一个 N * N 的矩阵 M,表示班级中学生之间的朋友关系。如果M[i][j] = 1,表示已知第 i 个和 j 个学生互为朋友关系,否则为不知道。你必须输出所有学生中的已知的朋友圈总数。
并查集的重要思想在于,用集合中的一个元素代表集合。
一个有趣的比喻,把集合比喻成公司,而代表元素则是老总。
[[1,1,0], [1,1,0], [0,0,1]]
一、定义数组:
p[i] = parent;
也就是 第 [i] 个人的领导是谁(假设每个人的直属领导只有一个人),显然当公司只有一个人的时候,自己就是老总,所以初始化的时候 p[i] = i;
二、查找自己公司的老总
然后递归查找,就可以找到这个公司的老总;
function find(i) { if (i == p[i]){ return i }else{ return find(p[i]); } }
三、并
当确认i,j两个人是一个公司的,就可以开始合并了,方式有两种
p[i] = j; 意思是: j 当上了 i 的领导
p[j] = i; 意思是: i 当上了 j 的领导
不管怎么样,反正两个人算是一伙的了
则合并算法如下:
function union(i,j){ let rootI = find(i); // 寻找 i 公司的领导 let rootJ = find(j); // 寻找 j 公司的领导 // 本意是将 i,j 合并,如果领导人不一样,就需要融合 if(rootI != rootJ){ p[i] = rootJ; // 或者 p[j] = rootI; } }
四、统计公司的个数
方法1、只需要确定有多少个老总就行。 老总上面是没有领导的,所以当 p[i] = i 的时候,就是老总
function countSetNumber(){ let count = 0; for(let i = 0; i < p.length; i++){ if(p[i] === i){ ++count; } } return count; }
方法2、理论初始化的时候,公司个数为 p.length , 那么每次 union 合并的时候,把个数减一,最后就可以得到结果了
let count = p.length; // 最开始大家都是自己的领导 function union(i,j){ let rootI = find(i); // 寻找 i 公司的领导 let rootJ = find(j); // 寻找 j 公司的领导 // 本意是将 i,j 合并,如果领导人不一样,就需要融合 if(rootI != rootJ){ p[i] = rootJ; // 或者 p[j] = rootI; count--;// 合并了,公司数量减少 } }
五、完整代码
/** * @param {number[][]} M * @return {number} */ var findCircleNum = function(M) { let len = M.length; let count = len; let p = new Array(len); for(let i = 0; i < len; i++){ p[i] = i; } for(let i = 0; i < len; i++){ for(let j = i+1; j < len; j++){ // 只需要确定 i,j的关系, i -> j 跟 j -> i 是一样的 ,所以 j 不需要从0开始 if(M[i][j] === 1){ // 说明他两是一个公司的,就进行合并 union(i,j); } } } function find(i) { if (i == p[i]){ return i }else{ return find(p[i]); } } function union(i,j){ let rootI = find(i); // 寻找 i 公司的领导 let rootJ = find(j); // 寻找 j 公司的领导 // 本意是将 i,j 合并,如果领导人不一样,就需要融合 if(rootI != rootJ){ p[i] = rootJ; // 或者 p[j] = rootI; count--;// 合并了,公司数量减少 } } return count; }; console.log(findCircleNum( [[1,1,0], [1,1,0], [0,0,1]] ) )
六、优化
方式1、现在公司是一级管一级,每个人都有自己唯一的直属领导,每次查找公司的老总,都要递归的查找,费时费力。
开始实行扁平化管理,每个员工的直属领导,都是公司的老总。这样可以减少查找次数。
方式就是,在查找的时候,顺便把每个员工的直属领导,设置为老总:
function find(i) { if (i == p[i]){ return i }else{ p[i] = find(p[i]);// 这一步是查找老总,并给 i 的只接领导 return p[i]; } }
方式2、每次合并,i ,j 有两种状态, 小公司合并到大公司,显然会减少下次查找的复杂度
则可以记录每个公司的阶级等级。然后选择合并方式。这里不做叙述。
七、题目来源
https://leetcode-cn.com/problems/friend-circles/