JavaScript并查集

459 阅读5分钟

「这是我参与2022首次更文挑战的第9天,活动详情查看:2022首次更文挑战

小明的亲戚圈

小明家是一个大家族,大到很多亲戚都互不相识;但是小明是程序员,想做一个自己的“亲戚圈”,输入一个人可以立即知道此人是否是自己的亲戚。

小明的亲戚圈是这样定义的:小明与A是亲戚,A与C是亲戚,B与C是亲戚;小明与B也是亲戚;给以小明身边的的亲戚关系,如何判断一个人跟小明是亲戚;

小明与A是亲戚,A与C是亲戚,B与C是亲戚;小明与B也是亲戚;

初始时,小明只与自己时亲戚,A、B、C也是毫无关系的。

graph LR
小明 --> 小明

A --> A

B --> B

C --> C

示例中小明与A是亲戚,此时在图中将A指向小明,为什么是A指向小明而不是小明指向A;这个不影响,后面讨论。跟着思路来

graph LR
小明 --> 小明

A --> 小明

B --> B

C --> C

此时A指向小明,表示A的亲戚链有小明,从小明出发也能找到亲戚A;

示例中又说A与C是亲戚

此时A已经与小明有亲戚关系了,如何让C添加进这个亲戚关系呢?

此时就是并查集中的核心,并的部分了

如合并,通过图可以看到,C指向C说明暂时只与自己有亲戚关系,A确不是指向自己,A指向小明;这是把C直接指向小明是不是可以将C添加到A-小明的‘亲戚圈’了。暂时先这么处理,看看后续有没有什么问题

graph LR
小明 --> 小明

A --> 小明

B --> B

C --> 小明

在看示例说明B与C是亲戚;

B如何加入A-小明-C这个“亲戚圈”呢?老套路

  • B指向B说明暂时只与自己有亲戚关系
  • C指向的是小明,将B指向小明即可
graph LR
小明 --> 小明

A --> 小明

B --> 小明

C --> 小明

此时输入A与B,A递归向上找自己的亲戚关系,B递归向上找自己的亲戚关系;只到找到某个只指向自己的人,就是自己亲戚关系的根节点。这个图中A与B共同指向小明,说明A与B是亲戚关系;

上述解决问题的思想就叫并查集。

朋友圈问题

有一批人,已知他们的关系,求朋友圈的数量。 刚开始谁也不认识谁,每个人都是相互独立的,自己是自己朋友圈的代表,这点可以理解吧

这时有朋友圈9个朋友圈

graph LR
0 --> 0
1 --> 1
2 --> 2
3 --> 3
4 --> 4
5 --> 5
6 --> 6
7 --> 7
8 --> 8

首先,0与1成为好朋友,5与6成为好朋友;2与3成为好朋友 将他们的关系修改为下图所示

现在0是0、1这个朋友圈的代表人物;5是5、6这个朋友圈的代表人物,2是2、3这个朋友圈的代表人物

这时有6个朋友圈

graph LR
0 --> 0
1 --> 0
2 --> 2
3 --> 2
4 --> 4
5 --> 5
6 --> 5
7 --> 7
8 --> 8

然后3与6成为好朋友了。

  • 3对6说:我的代表是2,咱们成为了好朋友,你要不要也认2作为代表?
  • 6对3说:我看我是不是代表,然后6弱弱的说,我的代表是5,我自己做不了主;
  • 3对6说:咱咋弄?
  • 3对6说:让2跟5去处理
  • 然后2与5一番交谈,2当代表 然后2就成了5,6,3朋友圈的代表

疑问1

6是如何找自己的代表的?

// 
function find(n) {
    //自己是代表吗?
    if (list[n] !== n) {
        //不是,向上找自己的代表
      list[n] = find(list[n])
    }
    // 找到了,返回这个代表
    return list[n]
  }

疑问2

3和6是如何2和5去处理的

 function marge(i, j) {
    const x = find(i)
    const y = find(j)
    list[x] = y
  }

这时有5个朋友圈

graph LR
0 --> 0
1 --> 0
2 --> 2
3 --> 2
4 --> 4
5 --> 2
6 --> 5
7 --> 7
8 --> 8

然后1与7成为朋友

  • 1对7说:我的代表是0,咱们成为了好朋友,你要不要也认0作为代表?
  • 7对1说:我看我是不是代表,然后7说我是代表我可以认0作为代表;

这时有4个朋友圈

graph LR
0 --> 0
1 --> 0
2 --> 2
3 --> 2
4 --> 4
5 --> 2
6 --> 5
7 --> 0
8 --> 8

现在看一下leetcode省份问题

省份数量

547. 省份数量

掘金-省份数量题解

城市之间的关系数组 isConnected = [[1,1,0],[1,1,0],[0,0,1]]

初始时,每个城市都是相互独立的,每个城市都表示一个省

通过枚举城市之间的关系数组isConnected,当i === j 时跳过

graph LR
0 --> 0
1 --> 1
2 --> 2

i=0,j=0;跳过,i=0,j = 1;此时 isConnected[i][j] === 1 ,说明城市0与城市1是彼此相连

图中可以看到,城市0与城市1都是指向自己的城市,所以可以任选一个作为代表本省的城市。假设城市1作为本省城市,将城市0指向1

graph LR
0 --> 1
1 --> 1
2 --> 2

i=1,j = 0;此时 isConnected[i][j] === 1 ,说明城市1与城市0是彼此相连;

途中看到城市1已经表示一个省了,城市0指向城市1,说明城市0与城市1已经在一个省;

最后如何统计省份的数量呢?

只要不是指向自己的,统计省份的时候忽略,只统计指向自己的城市就表示省份数量。

为什么?在这个示例中,因为城市0与城市1彼此相连;城市0让城市1代表自己了

graph LR
0 --> 1
1 --> 1
2 --> 2

完成并查集代码

class UnionFind {
  constructor(n) {
    this.node = new Array(n).fill().map((item, idx) => idx)
  }
  // 查找当前元素所在的根节点
  find(x) {
    if (x === this.node[x]) {
      return x
    }
    return this.find(this.node[x])
  }
  // 合并x,y所处集合
  merge(x, y) {
    x = this.find(x)
    y = this.find(y)
    if (x === y) return
    this.node[x] = y
  }
}