Union-Find木

問題

頂点数 $N$ のグラフに対して、次の2種類のクエリーを処理してください。

 クエリー1:頂点 $u$ と頂点 $v$ を双方向に結ぶ辺を追加する。
 クエリー2:頂点 $u$ と頂点 $v$ は同じ連結成分に属するかを答える。

最初はグラフに辺が一本もなく、与えられるクエリ―の数は $Q$ 個であるとします。

[制約]
🔹$ 2 \leqq N \leqq 100000 $
🔹$ 1 \leqq Q \leqq 100000 $

解き方・ソースコード

この問題は、Union-Find木というデータ構造を使って解くことができます。

Union-Fin木dとは、グループ分けを効率的に管理することができるデータ構造です。

次のような2種類のクエリ―を高速に処理することができます。

🔹統合クエリー:要素 $u$ を含むグループと、要素 $v$ を含むグループを統合する。
🔹回答クエリー:要素 $u$ と 要素 $v$ が同じグループにあるかどうかを判定する。


以下のソースコードは、Union-Find木を使って与えられたグラフの連結成分を管理し、クエリーを処理します。

入力例1として、3つの頂点と4つのクエリーを設定しています。

クエリー1は頂点1と頂点2を結ぶ辺を追加し、クエリー2は頂点2と頂点3が同じ連結成分に属するかを判定するものです。


まず、Union-Find木のクラスを定義します。

コンストラクタでは、頂点数 n を受け取り、par と size の初期化を行っています。

par は親の頂点を保持する配列で、初期値は全て -1 です。

size はグループの頂点数を保持する配列で、初期値は全て 1 です。


次に、root関数では、引数で指定された頂点 x の根を探索して返します。

par[x] が -1 になるまで、頂点 x の親に移動していきます。


unite関数は、引数で指定された要素 u, v を同一のグループに統合します。

まず、それぞれの根を取得します。

もし根が異なる場合は、グループの頂点数が小さい方を大きい方に統合します。

par 配列には親の頂点を、size 配列にはグループの頂点数を記録しています。


same 関数は、引数で指定された要素 u, v が同一のグループに属するかどうかを判定します。

関数内ではroot 関数を呼び出して u, v の根を比較します。


最後に、クエリーの処理を行います。

クエリー1の場合は、union 関数を呼び出して頂点 u, v を統合します。

クエリー2の場合は、same 関数を呼び出して頂点 u, v が同一のグループに属するかどうかを判定し、結果に応じてメッセージを出力します。

[Google Colaboratory]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#-------- 入力例1 ---------
N = 3 # 頂点数
Q = 4 # クエリ―数
queries = [
(1, 1, 2),
(2, 1, 3),
(1, 2, 3),
(2, 2, 3)
]
#---------------------------
# Union-Find 木
class unionfind:
# n 頂点の Union-Find 木を作成
# 頂点番号が 1 - indexed になるように実装するので n + 1 としている。
def __init__(self, n):
self.n = n
self.par = [ -1 ] * (n + 1) # 最初は親が無い
self.size = [ 1 ] * (n + 1) # 最初はグループの頂点数が 1

# 頂点 x の根を返す関数
def root(self, x):
# 1 個先(親)がなくなるまで、1 個先(親)に進み続ける
while self.par[x] != -1:
x = self.par[x]
return x

# 要素 u, v を統合する関数
def unite(self, u, v):
rootu = self.root(u)
rootv = self.root(v)
if rootu != rootv:
# u と v が異なるグループのときに処理を行う
if self.size[rootu] < self.size[rootv]:
self.par[rootu] = rootv
self.size[rootv] += self.size[rootu]
else:
self.par[rootv] = rootu
self.size[rootu] += self.size[rootv]

# 要素 u と v が同一のグループかどうかを返す関数
def same(self, u, v):
return self.root(u) == self.root(v)

# クエリの処理
uf = unionfind(N)
for tp, u, v in queries:
if tp == 1: # クエリ―1の処理を行う
uf.unite(u, v)
if tp == 2: # クエリ―2の処理を行う
if uf.same(u, v):
print(f'頂点 {u} と頂点 {v} は同じ連結成分に属する。')
else:
print(f'頂点 {u} と頂点 {v} は同じ連結成分に属さない。')

[実行結果(入力例1)]

頂点 1 と頂点 3 は同じ連結成分に属しない。

頂点 2 と頂点 3 は同じ連結成分に属する。

クエリ―1で頂点またはそのグループを結合し、クエリ―2の回答として各頂点が同じグループに属するかどうかを判定することができました。

深さ優先探索(社員の階級)

問題

ある会社には、$N(\leqq 100000)$ 人の社員がいて、$1$ から $N$ までの番号が付けれらています。

この会社について、次の $N-1$ 個の情報が分かっています。


$i$ 個目の情報:社員 $ A_i$ と社員 $B_i$ は直属の上司と部下の関係にあり、社員 $ A_i$ と社員 $B_i$ のどちらが上司なのかは分かっていません。


社員 $T$ が社長であり、それ以外の $N-1$ 人全員が誰か1人の直属の部下であるとき各社員の階級を求めて下さい。

ただし、部下がいない社員の階級は $0$ であり、部下がいる社員の階級は、直属の部下における階級の最大値に $1$ を足した値であるとします。

解き方・ソースコード

前回記事の問題とは違い、今回の問題は社員番号の制約がないので、深さ優先探索を使って解いていきます。


まず入力値として、変数 $N$ には社員数、変数 $T$ には社長の社員番号、変数 $R$ には上司または部下の関係を設定します。


次に、階層構造を持つデータを扱うため、グラフの形式で社員の関係を表現します。

変数 $g$ には社員の関係を格納するために、空のリストで初期化し、上司・部下関係の情報をもとに関係を追加していきます。


その後、階級を格納するために配列 $rank$ を作成します。初期値はすべて0です。

関数dfsは、深さ優先探索アルゴリズムを用いて、各社員の階級を計算します。

引数 parentには、ある社員の上司の社員番号を設定します。

引数 iには、チェックする社員の社員番号を設定します。

この関数内では $dfs(i, j) + 1$により、部下の階級から自分の階級を計算しています。

そして、現在保持している階級よりも大きければ更新します。

最初の関数dfsの呼び出しは、社長の階級を求めるために $dfs(-1, T - 1)$とします。


最後に、すべての $rank$ の値を出力することで、各社員の階級を表示します。

[Google Colaboratory]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#-------- 入力例1 ---------
N = 7 # 社員数
T = 3 # 社長の社員番号
R = [(3, 2), (3, 1), (2, 5), (1, 4), (4, 6), (4, 7)] # 上司または部下の関係
#-------- 入力例2 ---------
# N = 7 # 社員数
# T = 7 # 社長の社員番号
# R = [(3, 2), (3, 1), (2, 5), (1, 4), (4, 6), (4, 7)] # 上司または部下の関係
#---------------------------
g = [[] for i in range(N)]
for A, B in R:
A -= 1
B -= 1
g[A].append(B)
g[B].append(A)

rank = [0] * N

# 深さ優先探索
# parent:上司の社員番号
# i:チェックする社員番号
def dfs(parent, i):
for j in g[i]: # ある社員の部下全員分をループ
if j == parent: # 上司の社員番号は無視
continue
r = dfs(i, j) + 1 # チェックする社員番号を上司として、その部下のランクを取得する
if rank[i] < r: # 現在保持しているものよりランクが大きければ更新する
rank[i] = r
return rank[i]
dfs(-1, T - 1) # 上司はいないので-1、社長番号T(インデックス化するため-1)としてランクを取得する

for i in range(N):
print(f'社員 {i + 1} の階級は {rank[i]}')

[実行結果(入力例1)]

社員 1 の階級は 2

社員 2 の階級は 1

社員 3 の階級は 3

社員 4 の階級は 1

社員 5 の階級は 0

社員 6 の階級は 0

社員 7 の階級は 0

[実行結果(入力例2)]

社員 1 の階級は 3

社員 2 の階級は 1

社員 3 の階級は 2

社員 4 の階級は 4

社員 5 の階級は 0

社員 6 の階級は 0

社員 7 の階級は 5

各社員の階級を表示することができました。

動的計画法(部下の人数)

問題

ある会社には $ N $ 人の社員がいて、地位順に $1$ から $N$ までの番号が付けられています。

社長(社員番号 $1$)以外には直属の上司が1人いて、社員 $i$ の直属の上司は社員 $A_i$ です。

各社員について、部下が何人いるかを出力してください。

ただし、社員 $y$ が社員 $x$ の部下であることは、$ x \neq y$ であり、なおかつ社員 $y$ の直属の上司をたどって社員 $x$ に到達できることを意味します。

[制約]
🔹$ 2 \leqq N \leqq 100000 $
🔹$ 1 \leqq A_i \leqq i-1 (2 \leqq i \leqq N) $

解き方・ソースコード

動的計画法を使って、この問題を解いていきます。


まず、社員の数 $N$ と直属の上司のリスト $A$ を定義します。

次に、隣接リスト G を作成します。

隣接リストは、それぞれの社員が持つ部下の社員番号を管理します。

具体的には、社員 $i$ の持つ部下の社員番号が $j_1, j_2, \ldots, j_k$ である場合、$G[i]$ には $[j_1, j_2, \ldots, j_k]$ というリストが格納されます。


次に、動的計画法を用いて各社員が持つ部下の数を求めます。

具体的には、各社員の持つ部下の数を $dp$ というリストに保存します。

最初は全員が部下を持たないものとして、全ての要素を $0$ に初期化しています。

そして、社員 $i$ の持つ部下の数を計算するために、G[i] の要素(つまり社員 $i$ の部下の社員番号)を順番に取り出して、その社員が持つ部下の数を dp から参照して加算します。

最後に、$dp[i]$ に $1$ を加えることで、社員 $i$ の直属の部下の数を $dp$ に追加しています。

この処理を、社員 $i$ が社長(社員番号1)である場合まで繰り返します。


最後に、$dp$ の各要素を順番に出力することで、各社員が何人の部下を持つかを表示します。

[Google Colaboratory]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#-------- 入力例1 ---------
N = 7 # 社員数
A = [1, 1, 3, 2, 4, 5] # 社員番号2から社員番号7の直属の上司の社員番号
#---------------------------
# 隣接リストの作成
G = [list() for i in range(N + 1)]
for i in range(2, N + 1):
G[A[i - 2]].append(i) # 上司 → 部下の方向に方向に辺を追加

# 動的計画法(dp[x] は社員 x の部下の数)
dp = [0] * (N + 1)
for i in range(N, 0, -1):
for j in G[i]:
dp[i] += (dp[j] + 1)

for i in range(1, N + 1):
print(f'社員 {i} の部下の数は {dp[i]} 人')

[実行結果(入力例1)]

社員 1 の部下の数は 6 人

社員 2 の部下の数は 2 人

社員 3 の部下の数は 2 人

社員 4 の部下の数は 1 人

社員 5 の部下の数は 1 人

社員 6 の部下の数は 0 人

社員 7 の部下の数は 0 人

各社員について、部下が何人いるかを出力することができました。

ダイクストラ法②(最短経路)

問題

次のような重み付き無向グラフが与えられます。

🔹頂点数は $N$、辺の数は $M$ です。
🔹$i$ 番目の辺は頂点 $ A_i $ と頂点 $ B_i $ を結び、長さは $ C_i $ です。

頂点 $1$ から頂点 $N$ までの具体的な最短経路を1つ出力して下さい。

解き方・ソースコード

前回記事では、グラフにおける最短経路長を求めましたが、今回は最短経路(道筋)を求めます。


まず、与えられた辺の情報を使ってグラフを表現するために、隣接リスト g を作成します。このリストは、各頂点につながる辺の情報を保持するために使用されます。

次に、最短経路を求めるための初期化を行います。

ここでは、距離を表す cost リストと、最短経路の復元に使用する back リストを作成し、それぞれ無限大-1 で初期化します。

また、探索を行うためのキュー q を作成し、スタート地点となる頂点の情報をキューに追加します。


その後、以下の処理をキューが空になるまで繰り返します。

1. キューから距離が最小の頂点を取り出す。
2. 取り出した頂点に隣接する頂点について、距離を更新する必要がある場合は、push 関数を呼び出して距離を更新し、キューに頂点の情報を追加する。

この push 関数は、最短経路探索中に頂点に訪れる部分を関数化したものです。

この関数では、新たな頂点に到達した場合に距離を更新し、その頂点を探索するためにキューに情報を追加します。


最短経路探索が終了したら、back リストを使って、スタート地点から終点までの最短経路を復元します。

最後に、求めた最短経路を順番に出力します。

[Google Colaboratory]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#-------- 入力例1 ---------
N = 6 # 頂点数
M = 7 # 辺の数
# つながっている頂点とその距離
edges = [
(1, 2, 14),
(1, 4, 22),
(2, 3, 62),
(2, 5, 5),
(3, 6, 52),
(4, 5, 38),
(5, 6, 18)
]
#---------------------------
from heapq import heappush, heappop

g = [[] for _ in range(N)]
for A, B, C in edges:
A -= 1
B -= 1
g[A].append((B, C))
g[B].append((A, C))

INF = 1 << 61
cost = [INF] * N
back = [-1] * N
q = []

# 頂点に訪れる部分を関数化
def push(prev, i, c):
if cost[i] <= c:
return
cost[i] = c
back[i] = prev
heappush(q, (c, i))

# 復元の都合上後ろから ダイクストラ法を行う
push(-1, N - 1, 0)
while q:
c, x = heappop(q)
if cost[x] != c:
continue # 同じ頂点から複数回探索しない
for j, d in g[x]:
push(x, j, c + d)

# 最短経路を復元
ans = [0]
while ans[-1] != N - 1:
ans.append(back[ans[-1]])

for x in ans:
print(x + 1)

[実行結果(入力例1)]

1

2

5

6

最短経路(道筋)は【頂点1➡頂点2➡頂点5➡頂点6】であることが確認できました。

ダイクストラ法①(最短経路長)

問題

次のような重み付き無向グラフが与えられます。

🔹頂点数は $N$、辺の数は $M$ です。
🔹$i$ 番目の辺は頂点 $ A_i $ と頂点 $ B_i $ を結び、長さは $ C_i $ です。

このグラフに対して最短経路長を求めて下さい。

出力形式として、各頂点への最短距離を頂点 $1$ から頂点 $N$ まで順番に出力して下さい。

移動できない頂点の場合は、$-1$ を出力してください

[制約]
🔹$ 2 \leqq N \leqq 100000 $
🔹$ 1 \leqq M \leqq min(100000,N(N-1))/2 $
🔹$ 1 \leqq A_i < B_i \leqq N $
🔹$ 1 \leqq C_i \leqq 10000 $

解き方・ソースコード

ダイクストラ法を使ってこの問題を解いていきます。


ソースコードの冒頭で、グラフの情報を設定しています。

ここでは、頂点数、辺の数、および各辺の情報をタプルとしてリストに格納しています。


次に、隣接リストを作成しています。

このようにしておくことで、後続の処理で各頂点からつながっている辺を効率的に参照できるようになります。


また、このコードではPythonの組み込みモジュールheapqを使って優先度付きキューを実装しています。

これにより、次に探索すべき頂点を常に距離の短い順に処理することができます。

ダイクストラ法の実装部分では、優先度付きキューから取り出した要素が確定済みの頂点(kakutei変数)である場合はスキップしています。

また、確定していない隣接頂点について、現在の距離よりも短い経路が存在する場合は、その距離を更新し、優先度付きキューに加えます。

この処理を、優先度付きキューが空になるまで繰り返します。


最後に、各頂点までの最短距離を出力しています。

頂点までの最短距離が存在しない場合は-1を出力するようにしています。

[Google Colaboratory]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#-------- 入力例1 ---------
N = 6 # 頂点数
M = 7 # 辺の数
# つながっている頂点とその距離
edges = [
(1, 2, 14),
(1, 4, 22),
(2, 3, 62),
(2, 5, 5),
(3, 6, 52),
(4, 5, 38),
(5, 6, 18)
]
#---------------------------
import heapq

# 隣接リストの作成
# 重み付きグラフなので、各辺について (隣接頂点, 重み) のタプルを記録
G = [list() for i in range(N + 1)]
for a, b, c in edges:
G[a].append((b, c))
G[b].append((a, c))

# 配列・キューの初期化
# キューには (距離, 頂点番号) のタプルを記録
kakutei = [ False ] * (N + 1)
cur = [float('inf')] * (N + 1) # 距離の暫定値
cur[1] = 0
Q = []
heapq.heappush(Q, (cur[1], 1))

# ダイクストラ法
while len(Q) > 0:
# 次に確定させるべき頂点を求める
# 優先度付きキュー Q の最小要素を取り除き、その要素の 2 番目の値(頂点番号)を pos に代入
pos = heapq.heappop(Q)[1]

# Q の最小要素が「既に確定した頂点」の場合
if kakutei[pos] == True:
continue

# 暫定値 cur[x] の値を更新する
kakutei[pos] = True
for e in G[pos]:
# pos = e[0], cost = e[1] で対応
if cur[e[0]] > cur[pos] + e[1]:
cur[e[0]] = cur[pos] + e[1]
heapq.heappush(Q, (cur[e[0]], e[0]))

# 答えを出力
for i in range(1, N + 1):
if cur[i] == float('inf'):
print('頂点 {} までの最短距離 {}'.format(i, -1))
else:
print('頂点 {} までの最短距離 {}'.format(i, cur[i]))

[実行結果(入力例1)]

頂点 1 までの最短距離 0

頂点 2 までの最短距離 14

頂点 3 までの最短距離 76

頂点 4 までの最短距離 22

頂点 5 までの最短距離 19

頂点 6 までの最短距離 37

各頂点への最短距離を求めることができました。

幅優先探索(Depth First Search)②

問題

$ H \times W $ のマス目で表される迷路があります。

迷路の情報は文字列のリストとして表され、各文字は壁(‘#’) または 通過できる場所(‘.’)となります。

スタート座標からゴール座標までの最短移動回数を求めて下さい。

[制約]
🔹$ 1 \leqq H \leqq 100000 $
🔹$ 1 \leqq W \leqq 100000 $

解き方・ソースコード

前回記事と同様に幅優先探索を使ってこの問題を解いていきます。


前回記事ではqueueを使いましたが、今回はリストで代用します。

また、キューに格納したマス情報は削除せずに、追加した順番にキューの最後までチェックします。


上下左右にマスに移動する処理は関数化(push関数)し、壁の場合はパスし、通過できるマスの場合はコストを1つ増やして、そのマスをキューに追加しています。


cost配列(移動回数を管理)のインデックスは、0からではなく1から始まっています。

このため、push関数で引数に渡す座標値を $sx - 1, sy - 1, gx - 1, gy - 1$ にする必要があります。

[Google Colaboratory]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#-------- 入力例1 ---------
H, W = 7, 8 # 高さ、横幅
sx, sy = 2, 2 # スタート地点の x,y 座標
gx, gy = 4, 5 # ゴール地点の x,y 座標
# 迷路
S = [
'########',
'#......#',
'#.######',
'#..#...#',
'#..##..#',
'##.....#',
'########'
]
#-------- 入力例2 ---------
H, W = 5, 8 # 高さ、横幅
sx, sy = 2, 2 # スタート地点の x,y 座標
gx, gy = 2, 4 # ゴール地点の x,y 座標
# 迷路
S = [
'########',
'#.#....#',
'#.###..#',
'#......#',
'########'
]

#---------------------------
# マスごとの移動回数
cost = [[float('inf')] * W for _ in range(H)]

q = [] # queue は list で代用

# マスに移動する部分を関数化
def push(x, y, c):
if S[x][y] == '#': # 壁なので移動できない
return
if cost[x][y] <= c: # より少ない移動回数で移動済み(設定済み)
return
cost[x][y] = c
q.append((x, y))

# 幅優先探索
push(sx - 1, sy - 1, 0)
for x, y in q:
# コストを1つ増やす
c2 = cost[x][y] + 1
# 上下左右をチェック
push(x - 1, y, c2)
push(x, y - 1, c2)
push(x + 1, y, c2)
push(x, y + 1, c2)

print('解:', cost[gx - 1][gy - 1])

[実行結果(入力例1)]

解: 11

[実行結果(入力例2)]

解: 10

迷路に対する最短移動回数を求めることができました。

幅優先探索(Breadth First Search)

問題

次のような重みなし無向グラフに対して、頂点1から各頂点までの最短経路長を求めて下さい。

🔹頂点数は $N$、辺の数は $M$
🔹結びつく2つの頂点はリスト型に格納(edges変数)

[制約]
🔹$ 1 \leqq N \leqq 100000 $
🔹$ 0 \leqq M \leqq min(100000,N(N-1)/2) $
🔹$ 1 \leqq A_i < B_i \leqq N $

解き方・ソースコード

この問題を幅優先探索で解いてきます。

幅優先探索は、スタートに近い頂点から順番に探索していくアルゴリズムです。


今回は、キューを使った方法で幅優先探索を行います。

手順は以下の通りです。

手順1:頂点 $1$ から頂点 $x$ までの最短経路長の確定値を、$dist[x]=-1$ に初期化する。
手順2:キューに頂点 $1$ を追加し、$dist[1]=0$ にする。
手順3:キューが空になるまで、以下の手続きを繰り返す。
 🔹キューの先頭要素 $pos$ を取得し、それをキューから削除する。
 🔹$pos$ と隣接するすべての未確定頂点 $to$ に対し、$ dist[to] = dist[pos] + 1 $ を設定し、キューに $to$ を追加する。
  (未確定頂点とは $ dist[x]=-1 $ となっている頂点のことです。)

[Google Colaboratory]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#-------- 入力例1 ---------
N = 6 # 頂点数
M = 5 # 辺の数
# 頂点同士のつながり情報
edges = [(1, 2), (1, 3), (2, 4), (3, 4), (4, 6)]
#---------------------------
from collections import deque

# 隣接リストの作成
G = [ list() for i in range(N + 1) ]
for a, b in edges:
G[a].append(b)
G[b].append(a)

# 幅優先探索の初期化(dist[i] = -1 で初期化)
dist = [-1] * (N + 1)
dist[1] = 0
Q = deque()
Q.append(1)

# 幅優先探索
while len(Q) > 0:
pos = Q.popleft() # キュー Q の先頭要素を取り除き、その値を pos に代入する
for nex in G[pos]:
if dist[nex] == -1: # まだチェックしてない頂点
dist[nex] = dist[pos] + 1
Q.append(nex)

# 頂点 1 から各頂点までの最短距離を出力
for i in range(1, N + 1):
print('頂点 1 から頂点 {} までの最短経路は {}'.format(i, dist[i]))

[実行結果(入力例1)]

頂点 1 から頂点 1 までの最短経路は 0

頂点 1 から頂点 2 までの最短経路は 1

頂点 1 から頂点 3 までの最短経路は 1

頂点 1 から頂点 4 までの最短経路は 2

頂点 1 から頂点 5 までの最短経路は -1

頂点 1 から頂点 6 までの最短経路は 3

頂点1から各頂点への最短経路長を求めることができました。

深さ優先探索②(Depth First Search)

問題

頂点数 $ N $、辺数 $ M $ の連結グラフが与えられます。

このグラフについて、頂点 $1$ から 頂点 $N$ までの単純パスを1つ出力してください。

解き方・ソースコード

この問題は、移動経路の軌跡をどうやって記録するかがポイントになります。


まず前回記事で実施したように隣接リストを作成してから、深さ優先探索を行います。

その過程でどの頂点をたどっているのかを $path$ に記録していき、ゴール(目的となる頂点)にたどり着いた時点で、記録してきた $path$ を出力します。

[Google Colaboratory]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#-------- 入力例1 ---------
N = 5
M = 4
edges = [(1, 3), (2, 4), (3, 4), (3, 5)]
#---------------------------
# 隣接リストを作成
g = [[] for _ in range(N)]
for edge in edges:
a = edge[0] - 1
b = edge[1] - 1
g[a].append(b) # 頂点 a に隣接する頂点として b を追加
g[b].append(a) # 頂点 b に隣接する頂点として a を追加

# 深さ優先探索
visited = [False] * N # チェック済の頂点かどうかを管理
path = [] # 辿ってきたパスを管理

def dfs(i):
path.append(i) # 確認する頂点をパスに記録
# ゴール地点にたどり着いた場合
if i == N - 1:
# これまでたどってきた頂点を順番に出力
for x in path:
print(x + 1)
exit(0)

# ゴールに辿りついていない場合
visited[i] = True
for j in g[i]: # その頂点からつながっている頂点をチェック
if not visited[j]: # まだ確認していない頂点の場合
dfs(j) # 深さ優先探索でチェック
path.pop() # 一旦戻るのでチェックした頂点をパスから消去

dfs(0)

[実行結果(入力例1)]

1

3

5

頂点1⇒頂点3⇒頂点5の経路で目的の頂点に辿りつけることが確認できました。

深さ優先探索(Depth First Search)

問題

頂点数 $N$、辺数 $M$ のグラフが与えられます。

頂点には $1$ から $N$ までの番号が付けられており、$i$ 番目の辺は頂点 $A_i$ と $B_i$ を双方向につないでいます。

グラフ全体が連結であるかどうかを判定して下さい。

連結とは、グラフがどの頂点間も行き来可能であることを意味します。

[制約]
🔹$ 1 \leqq N \leqq 100000 $
🔹$ 0 \leqq M \leqq min(100000, N(N-1)/2 $
🔹$ 1 \leqq A_i < B_i \leqq N $

解き方・ソースコード

深さ優先探索とは、『進めるだけ進み、行き詰ったら一歩戻る』という考えに基づいてグラフを探索していくアルゴリズムです。

深さ優先探索を使うと、次のようにしてグラフの連結判定を行うことができます。

訪問した頂点には青色の印をつけていくことにします。

🔹【手順1】最初に、全ての頂点の色を白色に塗っておく。
🔹【手順2】頂点1を訪問して青色で塗る。その後、以下の行動を繰り返す。
 ・隣接する白色頂点がある場合⇒現在位置に隣接する白色の頂点を1つ選び、その頂点に移動し青色で塗る。
 ・隣接する白色頂点がない場合⇒一歩戻る。ただし頂点1にいる場合は戻れないので行動終了。
🔹【手順3】行動が終わった時点で全頂点が青色で塗られていたら、グラフは連結となる。


深さ優先探索は、再帰関数を使って次のように実装します。

🔹ある頂点に移動するときはdfs関数を再帰呼び出しする。
🔹一歩戻るときはreturnする。


プロブラムの計算量は $ O(N+M) $ となります。

[Google Colaboratory]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#-------- 入力例1 ---------
N = 6 # 頂点の数
M = 6 # 辺の数
# 各辺が結ぶ頂点の情報
edges = [
(1, 2),
(1, 3),
(2, 4),
(3, 4),
(4, 5),
(4, 6)
]
#-------- 入力例2 ---------
# N = 6 # 頂点の数
# M = 6 # 辺の数
# # 各辺が結ぶ頂点の情報
# edges = [
# (1, 2),
# (1, 3),
# (2, 4),
# (3, 4),
# (4, 5)
# ]
#---------------------------
# 深さ優先探索を行う関数(pos は現在位置、visited[x] は頂点 x が青色かどうかを表す真偽値)
def dfs(pos, G, visited):
visited[pos] = True
for i in G[pos]:
if visited[i] == False:
dfs(i, G, visited)

# 隣接リストの作成
G = [list() for i in range(N + 1)] # G[i] は頂点 i に隣接する頂点のリスト
for a, b in edges:
G[a].append(b) # 頂点 a に隣接する頂点として b を追加
G[b].append(a) # 頂点 b に隣接する頂点として a を追加

# 深さ優先探索
visited = [ False ] * (N + 1)
dfs(1, G, visited)
print('各頂点の判定結果', visited[1:], '=> 全ての結果をまとめた判定', all(visited[1:]))

# 答えの出力
print("このグラフは連結です。" if all(visited[1:]) else "このグラフは連結ではありません。")

[実行結果(入力例1)]

各頂点の判定結果 [True, True, True, True, True, True] => 全ての結果をまとめた判定 True

このグラフは連結です。

[実行結果(入力例2)]

各頂点の判定結果 [True, True, True, True, True, False] => 全ての結果をまとめた判定 False

このグラフは連結ではありません。

グラフが連結となっているかどうかを判定することができました。

グラフ(友達関係)

問題

ある学校のクラスには $N$ 人の生徒が在籍しており、$1$ から $N$ までの番号が付けられています。

このクラスには $M$ 個の友達関係があり、各 $i(1 \leqq i \leqq M)$ について、生徒 $A_i$ と生徒 $B_i$ が互いに友達です。

最も友達の多い生徒の番号を出力してください。

該当者が複数いる場合は、該当者をすべて出力してください。

解き方・ソースコード

この問題は、生徒を頂点、友達関係をに置き換えたグラフと見なして解いていきます。

[Google Colaboratory]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#-------- 入力例1 ---------
N = 5 # 生徒数
edges = [(1, 3), (2, 4), (3, 4), (3, 5)] # 友達関係
#---------------------------
# 友達の数を数える
friend = [0] * N
for a, b in edges:
friend[a - 1] += 1
friend[b - 1] += 1

mx = max(friend)
for f in friend:
if f == mx:
print(f'最も友達が多い生徒の番号は {ans + 1} です。')

[実行結果(入力例1)]

最も友達が多い生徒の番号は 3 です。

最も友達が多い生徒の番号を表示することができました。