Skip to content
Open
Show file tree
Hide file tree
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
348 changes: 348 additions & 0 deletions memo.md
Original file line number Diff line number Diff line change
@@ -1 +1,349 @@
# Step1

## アプローチ

* ノードとエッジが与えられて, 繋がっている塊の数を返せばいい
* グラフ理論で言うところの, 森?かな
* Union-Findを使って, エッジごとに更新をしていけば, 各ノードのグループを表す親ノードを得られる
* エッジの個数 E
* ノードの個数 N
* 1回のUnionにかかる最悪計算量
* O(logN)かな. 逆アッカーマンとかあったけど, あまり詳しく覚えていない.
* 最後に, すべてのノードをみて, 親番号の種類を確認する O(N)
* トータルで, O(ElogN + N)?
* step数としては, 5 * 10^3 * 10 + 2 * 10^3 = 10^4くらい
* 実行時間は, 10^7 step / secで見積もると, 0.001秒くらいか
* DFSやBFSを使って全部見ることもできそう
* 最初のノードを適当にとって辿る
* 辿り切ったら, まだ見ていないとこから一個適当にとって同様の手順を繰り返す
* 全てのノードを見るので, O(N)
* だけど, まだ見ていないところから一個取るのがO(1)でできる??
* visitedとかを使えば結局訪れるのはたかだか1回だからO(N)で良さそう



## Code1-1 (DFS)

* 繋がっているノードを確認するのにO(N)かかるから計算量の見積もりがアプローチで考えたものと違くなりそう
* 各ノードに対して,
* 繋がっているノードを確認 O(N)
* O(N^2)かな
* でも, 繋がっているノードをdictとして持つようにしたら, 繋がっているノードの確認はO(1)でよくなる
* ただ, 結局すべてのノードが辺で結ばれていたら, 繋がっているノードはN - 1個になるのでO(N^2)は免れないか??
* いや, 今回の場合は, 一度訪れられたノードに再び行くことはないから, O(N)
* エッジの前処理も含めたらO(N + E)

```python
from typing import List

class Solution:

def countComponents(self, n: int, edges: List[List[int]]) -> int:

def create_adjacent_matrix():
adjacent_matrix = [[False] * n for _ in range(n)]
for edge in edges:
node1 = edge[0]
node2 = edge[1]
adjacent_matrix[node1][node2] = True
adjacent_matrix[node2][node1] = True
return adjacent_matrix

def visit_all_connected_nodes(cur_node, adj_matrix, visited):
if visited[cur_node]:
return
visited[cur_node] = True
for next_node in range(n):
if cur_node == next_node:
continue
if not adj_matrix[cur_node][next_node]:
continue
visit_all_connected_nodes(next_node, adj_matrix, visited)
return

visited = [False] * n
adj_matrix = create_adjacent_matrix()
num_components = 0
for i in range(n):
if visited[i]:
continue
num_components += 1
visit_all_connected_nodes(i, adj_matrix, visited)
return num_components

```

## Code1-2 (Union Find)

```python
from typing import List


class UnionFind:
def __init__(self, size):
self.parents = [i for i in range(size)]
self.rank = [0] * size

def find(self, idx):
if self.parents[idx] != idx:
self.parents[idx] = self.find(self.parents[idx])
return self.parents[idx]

def union(self, idx1, idx2):
parent1 = self.find(idx1)
parent2 = self.find(idx2)
if parent1 == parent2:
return

if self.rank[parent1] < self.rank[parent2]:
self.parents[parent1] = parent2
return
elif self.rank[parent2] < self.rank[parent1]:
self.parents[parent2] = parent1
return
else:
self.parents[parent2] = parent1
self.rank[parent1] += 1
return


class Solution:
def countComponents(self, n: int, edges: List[List[int]]) -> int:
uf = UnionFind(n)
for edge in edges:
uf.union(edge[0], edge[1])

num_components = 0
seen_parents = set()
for i in range(n):
parent = uf.find(i)
if parent in seen_parents:
continue
seen_parents.add(parent)
num_components += 1

return num_components

```

# 他の人のコードを確認

* 1 - https://github.com/mamo3gr/arai60/pull/56
* 言語: Python
* DFSとUnionFindの両方を実装
* DFS: DFSでスタックを使用し, 辺は隣接行列ではなくて辞書として用意
* UnionFind: 実装はほぼ自分のものと一致. 違いとしては`rank`じゃなくて`size`を使用している
* 2 - https://github.com/Hiroto-Iizuka/coding_practice/pull/19
* 言語: Python
* 1とほぼ同様
* 3 - https://github.com/TakayaShirai/leetcode_practice/pull/19
* 言語: Swift
* 手作業の様子を考えているのがいいと思った
* 実行時間が図られているのも良い
* 1, 2と同様
* 4 - https://github.com/xbam326/leetcode/pull/21
* 言語: Python
* 1, 2, 3と同様
* 5 - https://github.com/dxxsxsxkx/leetcode/pull/19
* 言語: C++
* 辞書ではなく, 二次元配列で辺のつながりを表現. adjacency_list[i] = (nodeiにつながるノードのリスト)
* あとは1,2,3,4と同様

# 調べたこと

* UnionFindの計算量
* 最適化なしだったら, O(N)
* rankやsizeによって木の高さを制限したら, O(logN)
* Path Compression + rankにしたら, O(α(N))で, これはほぼ定数時間と見ていい


# Step2

## Code2-1 (DFS)

* 再帰をスタックで行うようにした
* 辞書として辺を登録した

```python
from typing import List
from collections import defaultdict

class Solution:

def countComponents(self, n: int, edges: List[List[int]]) -> int:

node_to_neighbors = defaultdict(set)
for edge in edges:
node_to_neighbors[edge[0]].add(edge[1])
node_to_neighbors[edge[1]].add(edge[0])

visited = [False] * n

def visit_connected_nodes(init_node):
frontier = [init_node]
while frontier:
node = frontier.pop()
if visited[node]:
continue
visited[node] = True
for neighbor_node in node_to_neighbors[node]:
if not visited[neighbor_node]:
frontier.append(neighbor_node)
return

num_components = 0
for i in range(n):
if visited[i]:
continue
num_components += 1
visit_connected_nodes(i)
return num_components

```

## Code2-2 (Union Find)

```python
from typing import List


class UnionFind:
def __init__(self, size):
self.parents = [i for i in range(size)]
self.rank = [0] * size

def find(self, idx):
if self.parents[idx] != idx:
self.parents[idx] = self.find(self.parents[idx])
return self.parents[idx]

def union(self, idx1, idx2):
parent1 = self.find(idx1)
parent2 = self.find(idx2)
if parent1 == parent2:
return

if self.rank[parent1] < self.rank[parent2]:
self.parents[parent1] = parent2
return
elif self.rank[parent2] < self.rank[parent1]:
self.parents[parent2] = parent1
return
else:
self.parents[parent2] = parent1
self.rank[parent1] += 1
return
Comment on lines +226 to +235
Copy link

Choose a reason for hiding this comment

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

  • returnが冗長に感じました
  • ランクが等しいときが一番特徴的な分岐なので、それを頭に持ってくると読みやすいと思いました
Suggested change
if self.rank[parent1] < self.rank[parent2]:
self.parents[parent1] = parent2
return
elif self.rank[parent2] < self.rank[parent1]:
self.parents[parent2] = parent1
return
else:
self.parents[parent2] = parent1
self.rank[parent1] += 1
return
if self.rank[parent1] == self.rank[parent2]:
self.parents[parent2] = parent1
self.rank[parent1] += 1
elif self.rank[parent1] < self.rank[parent2]:
self.parents[parent1] = parent2
else:
self.parents[parent2] = parent1

Copy link
Owner Author

Choose a reason for hiding this comment

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

アーリーリターンのほうがいいかなと思っていましたが、if文内の記述量が少ないので, 一目でわかるよう冗長なreturnはないほうが良さそうですね

Copy link

Choose a reason for hiding this comment

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

何らかの処理が続くのならearly returnは読み手を助けるのですが、このifブロックはその後で関数が終了することが見えているので、early returnするメリットが享受できないように感じました。

Copy link
Owner Author

Choose a reason for hiding this comment

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

おっしゃる通りだと思います!ありがとうございます!



class Solution:
def countComponents(self, n: int, edges: List[List[int]]) -> int:
uf = UnionFind(n)
for edge in edges:
uf.union(edge[0], edge[1])

num_components = 0
seen_parents = set()
for i in range(n):
parent = uf.find(i)
if parent in seen_parents:
continue
seen_parents.add(parent)
num_components += 1

return num_components

```

# Step3

## Code3-1 (DFS)

```python
from typing import List
from collections import defaultdict


class Solution:
def countComponents(self, n: int, edges: List[List[int]]) -> int:

node_to_neighbors = defaultdict(set)
for edge in edges:
node_to_neighbors[edge[0]].add(edge[1])
node_to_neighbors[edge[1]].add(edge[0])

visited = [False] * n

def visit_connected(init_node):
if visited[init_node]:
return
frontier = [init_node]
while frontier:
node = frontier.pop()
if visited[node]:
continue
visited[node] = True
for neighbor_node in node_to_neighbors[node]:
if visited[neighbor_node]:
continue
frontier.append(neighbor_node)
return

num_components = 0
for node in range(n):
if visited[node]:
continue
num_components += 1
visit_connected(node)
return num_components

```

## Code3-2 (Union Find)

```python
from typing import List

class UnionFind:
def __init__(self, size):
self.parents = [i for i in range(size)]
self.rank = [0] * size

def find(self, idx):
if self.parents[idx] != idx:
self.parents[idx] = self.find(self.parents[idx])
return self.parents[idx]

def union(self, idx1, idx2):
parent1 = self.find(idx1)
parent2 = self.find(idx2)
if parent1 == parent2:
return
if self.rank[parent1] < self.rank[parent2]:
self.parents[parent1] = parent2
return
if self.rank[parent2] < self.rank[parent2]:
self.parents[parent2] = parent1
return
self.parents[parent2] = parent1
self.rank[parent1] += 1
return

class Solution:
def countComponents(self, n: int, edges: List[List[int]]) -> int:
uf = UnionFind(n)
for edge in edges:
uf.union(edge[0], edge[1])

num_components = 0
seen_parent = set()
for node in range(n):
parent = uf.find(node)
if parent in seen_parent:
continue
seen_parent.add(parent)
num_components += 1

return num_components


```
36 changes: 36 additions & 0 deletions step1-1_dfs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from typing import List

class Solution:

def countComponents(self, n: int, edges: List[List[int]]) -> int:

def create_adjacent_matrix():
adjacent_matrix = [[False] * n for _ in range(n)]
for edge in edges:
node1 = edge[0]
node2 = edge[1]
adjacent_matrix[node1][node2] = True
adjacent_matrix[node2][node1] = True
return adjacent_matrix

def visit_all_connected_nodes(cur_node, adj_matrix, visited):
Copy link

Choose a reason for hiding this comment

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

  • 変数名はよほど長くない限り、省略しないほうが分かりやすいです
  • _all_ は何か特別な強調をしたいニュアンスを受けました(が、そうではないですよね)
Suggested change
def visit_all_connected_nodes(cur_node, adj_matrix, visited):
def visit_connected_nodes(current_node, adjacent_matrix, visited):

Copy link

Choose a reason for hiding this comment

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

cur_nodestart_index あるいは start_node などどうでしょうか。current=現在の、というのが読み手には何なのか分かりにくそうです。

Copy link
Owner Author

Choose a reason for hiding this comment

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

startの方が良さそうですね。ありがとうございます!

if visited[cur_node]:
return
visited[cur_node] = True
for next_node in range(n):
if cur_node == next_node:
continue
if not adj_matrix[cur_node][next_node]:
continue
visit_all_connected_nodes(next_node, adj_matrix, visited)
return

visited = [False] * n
adj_matrix = create_adjacent_matrix()
Copy link

Choose a reason for hiding this comment

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

動作的には問題ないのですが、何に依存しているのかを明にするために edges を引数に取るようにしたいです。

Suggested change
adj_matrix = create_adjacent_matrix()
adj_matrix = create_adjacent_matrix(edges)

num_components = 0
for i in range(n):
if visited[i]:
continue
num_components += 1
visit_all_connected_nodes(i, adj_matrix, visited)
return num_components
Loading