プログラミングコンテストでは、ある(無向)グラフが与えられ、
その中の2つの頂点(ノード)を結んだ場合に、そのグラフが閉路(ループ)を持つことになるか?
を判定しなければならない問題が出題されることがあります(たとえば AtCoder Beginner Contest 231 D問題 など)。
その閉路の検出は、UnionFind を用いることで簡単に行うことができます。
例として、以下の2つのグラフでは、
青色のグラフでは、頂点2と頂点4を結んでも、1 – 2 – 4 – 3 という並びになるだけで閉路は持ちません。
一方で、緑色のグラフは頂点2と頂点4を結ぶと、1 – 2 – 4 の中で閉路(ループ)が発生します。
そのように、ある2つの頂点を結んだ場合に閉路ができるかどうかを判定する方法を、
UnionFind を使って、python3 で実装してみます。
UnionFind
今回使う UnionFind のコードは以下の通りです。
#今回の例で使用する頂点の数
N = 4
#各頂点(要素)の根(親)を管理する配列:初期値は各頂点自身, 頂点数は上述の N
root = [i for i in range(N+1)]
#各頂点(要素)の高さを管理する配列:初期値は最下位の0
rank = [0]*(N+1)
#頂点xの根(親)を見つける関数:戻り値は根(親)の頂点番号
def find(x):
#print(x, root)
if root[x] == x:
return x
else:
root[x] = find(root[x])
return root[x]
#頂点xと頂点yが同じ根(親)を持つかを判定する関数:戻り値はBoolean(True or False)
def same(x,y):
return find(x) == find(y)
#頂点xと頂点yが所属するグループを併合する関数
def unite(x,y):
x = find(x)
y = find(y)
if x == y:
return 0
#浅い(木の高さとして低い)方を深い(高い)方に合併
if rank[x] < rank[y]:
root[x] = y
else:
root[y] = x
if rank[x] == rank[y]:
rank[x] += 1
上記コード中の関数、unite(x, y) を使って、与えられたグラフを表現するために、頂点を結合していきます。
今回の「閉路を持たないグラフ」の例では、
unite(1,2)
unite(3,4)
と記述することになります。
一方で、「閉路をもつグラフ」の例では、
unite(1,2)
unite(1,3)
unite(1,4)
と記述します。
閉路の判定
今回は、頂点2と頂点4を結んだ場合に閉路が発生するかを調べたいのですが、
これは言い換えれば「頂点2と頂点4が同じ根(ルート)を持っているか?」を調べることになります。
同じルートを持つ2頂点を結べば、閉路が発生することになり、
異なるルートを持つ2頂点を結んでも、閉路は発生しない訳です。
よって実際のプログラムは、
same(2, 4)
と記述すれば、
「閉路が発生する = 同じルートを持つ」場合は「True」が返され、
「閉路が発生しない = 同じルートを持たない」場合は「False」が返されます。
割と簡単ですね。
それでは最後に、それぞれのケースのコード全体を載せて終わりたいと思います。
お役に立てれば嬉しいです。
(例)頂点2と頂点4を結んでも平路を持たないグラフ
#今回の例で使用する頂点の数
N = 4
#各頂点(要素)の根(親)を管理する配列:初期値は各頂点自身, 頂点数は上述の N
root = [i for i in range(N+1)]
#各頂点(要素)の高さを管理する配列:初期値は最下位の0
rank = [0]*(N+1)
#頂点xの根(親)を見つける関数:戻り値は根(親)の頂点番号
def find(x):
#print(x, root)
if root[x] == x:
return x
else:
root[x] = find(root[x])
return root[x]
#頂点xと頂点yが同じ根(親)を持つかを判定する関数:戻り値はBoolean(True or False)
def same(x,y):
return find(x) == find(y)
#頂点xと頂点yが所属するグループを併合する関数
def unite(x,y):
x = find(x)
y = find(y)
if x == y:
return 0
#浅い(木の高さとして低い)方を深い(高い)方に合併
if rank[x] < rank[y]:
root[x] = y
else:
root[y] = x
if rank[x] == rank[y]:
rank[x] += 1
unite(1,2)
unite(3,4)
print(same(2,4))
# --> False
(例)頂点2と頂点4を結ぶと平路を持つグラフ
#今回の例で使用する頂点の数
N = 4
#各頂点(要素)の根(親)を管理する配列:初期値は各頂点自身, 頂点数は上述の N
root = [i for i in range(N+1)]
#各頂点(要素)の高さを管理する配列:初期値は最下位の0
rank = [0]*(N+1)
#頂点xの根(親)を見つける関数:戻り値は根(親)の頂点番号
def find(x):
#print(x, root)
if root[x] == x:
return x
else:
root[x] = find(root[x])
return root[x]
#頂点xと頂点yが同じ根(親)を持つかを判定する関数:戻り値はBoolean(True or False)
def same(x,y):
return find(x) == find(y)
#頂点xと頂点yが所属するグループを併合する関数
def unite(x,y):
x = find(x)
y = find(y)
if x == y:
return 0
#浅い(木の高さとして低い)方を深い(高い)方に合併
if rank[x] < rank[y]:
root[x] = y
else:
root[y] = x
if rank[x] == rank[y]:
rank[x] += 1
unite(1,2)
unite(1,3)
unite(1,4)
print(same(2,4))
# --> True