Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
188 changes: 188 additions & 0 deletions 323/memo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
## step1:
それぞれの集合の数を求めていくということで、Union Findを使うんだろなと思ったがUnion Findの実装方法をすっかり忘れてしまった。そこでDFSを使って各ノードを探索していくことにする。双方向グラフなのでお互いのノードに対してベクトルを辞書型で置いていき、深さ優先探索で辿っていく。edgesをfor文で回してもし辿ったことのないノードだったらそこで探索をし、そのグラフを探索済みのものとしてanswerをインクリメントしていく。
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

双方向グラフなのでお互いのノードに対してベクトルを辞書型で置いていき

これがどういう意味なのか、文からは分かりませんでした。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

edgesをfor文で回してもし辿ったことのないノードだったらそこで探索

コードと一致していないように見えますが、どこに相当しますか?

Copy link
Copy Markdown
Owner Author

@atmaxstar atmaxstar May 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

すいません、途中で方針を変えて言ってることが食い違ってしまうことが多々ありますね...
今回の場合は
「0からn-1までのノードを巡り辿ったことのないノードだったらそこで探索」
ですね。

### code
```python
from collections import defaultdict
class Solution:
def countComponents(self, n: int, edges: List[List[int]]) -> int:
x_to_neighbors = defaultdict(list)
for edge in edges:
i, j = edge
x_to_neighbors[i].append(j)
x_to_neighbors[j].append(i)
visited = set()

def explore(x):
visited.add(neighbor)
for neighbor in x_to_neighbors[x]:
if not neighbor in visited:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

neighbor not in visitedと書くのが一般的かと思います。

explore(neighbor)

answer = 0
for i in range(n):
if not i in visited:
explore(i)
answer += 1

return answer
```
空間計算量はO(2*E + N), 時間計算量はO(2*E + N)である。(Eはedgesのサイズ、Nはノードの数)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Big O記法で定数倍は省略するのが一般的だと思います。

これでひとまずacceptされたが、一応UnionFindのやり方を調べて書いてみる。

```python
class UnionFind:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [1]*n

def find(self, node):
if node == self.parent[node]:
return node
self.parent[node] = self.find(self.parent[node])
return self.parent[node]

def union(self, u, v):
pu = self.find(u)
pv = self.find(v)
if pu == pv:
return False
if self.rank[pu] > self.rank[pv]:
self.parent[pv] = pu
self.rank[pu] += self.rank[pv]
else:
self.parent[pu] = pv
self.rank[pv] += self.rank[pu]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

これだとunion by rankではなく、union by sizeではないですか?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

本当ですね。NeetCodeの回答を参考にしたのですがあちらもrankを使っているけどもunion by sizeをしているみたいです。self.sizeに変名した方がいいですね。

return True


class Solution:
def countComponents(self, n: int, edges: List[List[int]]) -> int:
unionFind = UnionFind(n)
for u, v in edges:
unionFind.union(u, v)
roots = set()
for i in range(n):
roots.add(unionFind.find(i))
return len(roots)
```
UnionFindを何も見なくても実装できるように言語化してみる。まずUnionFindの初期化ではサイズnと0からn-1までの各ノードの属する集合のルートをフィールドとしてもつ。各ノードの親は最初は各ノード自身を指定させる。findメソッドでは引数に入れたノードの集合のルートを求めて返すのだがノードAを引数にもらった際に単にself.parents[A]を求めた場合に親がノードBとして、ノードBがさらに親を持つ場合、再帰的に親の親を求めてノードが自分自身を親とする所まで遡る必要がある。またノードAの親を求める時に再びその再帰のプロセスを行うのは非効率なので、self.parent[node]を最終的な親に書き換えておく。
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

まずUnionFindの初期化ではサイズnと0からn-1までの各ノードの属する集合のルートをフィールドとしてもつ。

コードと一致していないように見えますが、この説明で合っていますか?

    def __init__(self, n):
        self.parent = list(range(n))
        self.rank = [1]*n

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

最初self.n = nと書いていたのですが途中で書き換えてしまったので一致しなくなってしまいました...

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self.parent[node]を最終的な親に書き換えておく

最終的な親=ルートですかね?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

そうですね、最終的な親に名付けるならルートかなと思いました。

unionメソッドは2つのノードを引数とし、それぞれが属している集合を合体させるのだが、もし合体させようとしてる2つのノードのルートが同じノードだった場合、それはその集合はすでに同一であることの証なので早期リターンする。もしparentが違ったら集合を合体させる。単に片方のルートのparentをもう片方のルートに設定すればいいのだが、集合のノード数が少ない方のルートの親を集合のノード数が多い方のルートに指定した方がself.parentの更新数が少なくなるのでself.rankを比較してノード数の多い方のルートを中心に結合する。この際に、結合された方のランクを結合する方へ加算するのを忘れない。self.findとself.unionの時間計算量はどちらもO(α(n))でほぼO(1)と同一視できるらしいが、もしself.findでの
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

集合のノード数が少ない方のルートの親を集合のノード数が多い方のルートに指定した方がself.parentの更新数が少なくなる

「self.parentの更新数が少なくなる」とは後のfind()を呼び出した時の話ですか?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

そうですね、findで再帰的に辿ったノードのparentをルート指定するように更新する時のことを言ってますね。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ノード数の多い方のルートを中心に結合する

「中心に結合する」ってどういう意味ですかね?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ここちょっと表現に迷いました。単純に、「ノード数の多い方のルートをノード数の少ない方のルートの親に指定する」と書けばよかったですね。

`self.parent[node] = self.find(self.parent[node])`
これとunionのrank比較がないとどちらもlogNになるらしい。証明が難しそうなので軽く流しておく。

## step2:
### code
```python
class UnionFind:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [1]*n

def find(self, node):
if node == self.parent[node]:
return node
self.parent[node] = self.find(self.parent[node])
return self.parent[node]

def union(self, u, v):
pu = self.find(u)
pv = self.find(v)
if pu == pv:
return False
if self.rank[pu] > self.rank[pv]:
self.parent[pv] = pu
self.rank[pu] += self.rank[pv]
else:
self.parent[pu] = pv
self.rank[pv] += self.rank[pu]
return True


class Solution:
def countComponents(self, n: int, edges: List[List[int]]) -> int:
unionFind = UnionFind(n)
for u, v in edges:
unionFind.union(u, v)
roots = set()
for i in range(n):
roots.add(unionFind.find(i))
return len(roots)
```

## step3:
### code
```python
class UnionFind:
def __init__(self, n: int) -> None:
self.parent = [i for i in range(n)]
self.rank = [1]*n

def find(self, x: int) -> int:
if x == self.parent[x]:
return x
self.parent[x] = self.find(self.parent[x])
return self.parent[x]

def union(self, u: int, v: int) -> bool:
pu, pv = self.find(u), self.find(v)
if pu == pv:
return False
if self.rank[pu] >= self.rank[pv]:
self.parent[pv] = pu
self.rank[pu] += self.rank[pv]
else:
self.parent[pu] = pv
self.rank[pv] += pu
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+= self.rank[pu]ですか?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

間違ってますね...
puがintだったのでエラーが出ず見逃してました。

return True

class Solution:
def countComponents(self, n: int, edges: List[List[int]]) -> int:
unionFind = UnionFind(n)
for edge in edges:
u, v = edge
unionFind.union(u, v)
roots = set()
for i in range(n):
roots.add(unionFind.find(i))
return len(roots)
```

## 他の人のコードを読んでみる
https://github.com/h1rosaka/arai60/pull/60/changes
ブルートフォースで実装されていて、コメントにあったこの書き方がとても単純で分かりやすいと感じた。だが時間計算量でO(E*N)かかってしまう。(Eはエッジの数、Nはノードの数)
```python
class Solution:
def countComponents(self, n: int, edges: List[List[int]]) -> int:
connected_sets = []
unseen_nums = set([num for num in range(n)])
def serach_in_connected_sets(target:int) -> int:
for i in range(len(connected_sets)):
if target in connected_sets[i]:
return i
return -1

for x, y in edges:
unseen_nums.discard(x)
unseen_nums.discard(y)

set_num_x = serach_in_connected_sets(x)
set_num_y = serach_in_connected_sets(y)

if set_num_x == -1 and set_num_y == -1:
connected_sets.append({x, y})
continue

if set_num_x != -1 and set_num_y == -1:
connected_sets[set_num_x].add(y)
continue

if set_num_x == -1 and set_num_y != -1:
connected_sets[set_num_y].add(x)
continue

if set_num_x != set_num_y:
connected_sets[set_num_x] |= connected_sets[set_num_y]
connected_sets.pop(set_num_y)

return len(connected_sets) + len(unseen_nums)
```