Goでグラフの一筆書きをしてみる
したこと
グラフの一筆書きを求めるアルゴリズムをGolangで書きます。
一筆書きとは、グラフにおいて全ての辺を1度だけ通るような路のことです。一筆書きについては以下の記事を参照してください。
注意点
今回は、あまり計算量などは気にせず、実際に正しい出力が得られることを第一目標に実装しています。かなり無駄があるのでご了承ください。
高速なアルゴリズムが知りたい方は、別の記事をご参考ください。
実装
最終コード
先に完成版を載せておきます。
完成版
package main
import (
"fmt"
"io"
"os"
)
func main() {
edges := readFile()
G := newAdjacencyList(edges)
p := solve(G)
fmt.Println(p)
}
func solve(G [][]int) []int {
oddNodes := getOddNodes(G)
pathNum := countPath(G)
var p []int
if len(oddNodes) == 1 || len(oddNodes) > 2 {
return p
}
v := -1
if len(oddNodes) == 2 {
// 奇点から奇点へのパスを探して, pに格納する
from := oddNodes[0]
to := oddNodes[1]
p = findPath(G, from, to)
// pをGから削除する
removePath(G, p)
pathNum -= len(p) - 1
}
for pathNum > 0 {
// pから孤立点でない点を探す
for _, n := range p {
if len(G[n]) > 0 {
v = n
break
}
}
// pが空の場合は, Gから任意の孤立点でない点を探す
if len(p) == 0 {
for i := 0; i < len(G); i++ {
if len(G[i]) > 0 {
v = i
break
}
}
}
// vを含むサイクルを見つける
c := findPath(G, v, v)
if len(p) == 0 {
p = c
} else {
idx := 0
for i := 0; i < len(p); i++ {
if p[i] == v {
idx = i
break
}
}
// pのvの部分をcに置き換える
p = append(p[:idx], append(c, p[idx+1:]...)...)
}
// cをGから削除する
removePath(G, c)
pathNum -= len(c) - 1
}
return p
}
func readFile() [][]int {
f, err := os.Open("path/to/input.txt")
if err != nil {
panic(err)
}
defer f.Close()
var numLines [][]int
for {
var from, to int
_, err := fmt.Fscanf(f, "%d,%d\n", &from, &to)
if err == io.EOF {
break
}
numLines = append(numLines, []int{from, to})
}
return numLines
}
func newAdjacencyList(edges [][]int) [][]int {
G := make([][]int, 0)
maxNode := 0
for _, edge := range edges {
from := edge[0]
to := edge[1]
if from > maxNode {
maxNode = from
}
if to > maxNode {
maxNode = to
}
}
for i := 0; i <= maxNode; i++ {
G = append(G, []int{})
}
for _, edge := range edges {
from := edge[0]
to := edge[1]
G[from] = append(G[from], to)
G[to] = append(G[to], from)
}
return G
}
func getOddNodes(G [][]int) []int {
var oddNodes []int
for i, nodes := range G {
if len(nodes)%2 == 1 {
oddNodes = append(oddNodes, i)
}
}
return oddNodes
}
func findPath(G [][]int, from, to int) []int {
var p []int
// 辺を通ったかどうかを記録する
var passed [][]bool
for i := 0; i < len(G); i++ {
passed = append(passed, make([]bool, len(G)))
}
c := from
// fromからtoへのパスを一つ見つける
for {
p = append(p, c)
for _, n := range G[c] {
if !passed[c][n] {
passed[c][n] = true
passed[n][c] = true
c = n
break
}
}
if c == to {
p = append(p, c)
break
}
}
return p
}
func removeEdge(G [][]int, from, to int) {
for i, edge := range G[from] {
if edge == to {
G[from] = append(G[from][:i], G[from][i+1:]...)
break
}
}
for i, edge := range G[to] {
if edge == from {
G[to] = append(G[to][:i], G[to][i+1:]...)
break
}
}
}
func removePath(G [][]int, p []int) {
for i := 0; i < len(p)-1; i++ {
removeEdge(G, p[i], p[i+1])
}
}
func countPath(G [][]int) int {
count := 0
for i := 0; i < len(G); i++ {
count += len(G[i])
}
return count / 2
}
0. グラフを構築する
入力は、辺で結ばれたニ頂点をコンマ区切りで書いたもののリストです。
グラフは隣接リストとして持つことにします。
AtCoderなどでは、入力の1行目に頂点数などがあるのが一般的かと思いますが、少し訳があってこのような形になっています。
また、入力は連結な単純無向グラフであるとします。
入力
func readFile() [][]int {
f, err := os.Open("path/to/input.txt")
if err != nil {
panic(err)
}
defer f.Close()
var numLines [][]int
for {
var from, to int
_, err := fmt.Fscanf(f, "%d,%d\n", &from, &to)
if err == io.EOF {
break
}
numLines = append(numLines, []int{from, to})
}
return numLines
}
func newAdjacencyList(edges [][]int) [][]int {
g := make([][]int, 0)
maxNode := 0
for _, edge := range edges {
from := edge[0]
to := edge[1]
if from > maxNode {
maxNode = from
}
if to > maxNode {
maxNode = to
}
}
for i := 0; i <= maxNode; i++ {
g = append(g, []int{})
}
for _, edge := range edges {
from := edge[0]
to := edge[1]
g[from] = append(g[from], to)
g[to] = append(g[to], from)
}
return g
}
入力の例
0,4
0,1
0,2
0,4
1,3
1,4
2,3
2,4
3,4
1. Eulerグラフ・半Eulerグラフの判定
グラフが一筆書きできるための必要十分条件は、
- グラフがEulerグラフである
- グラフが半Eulerグラフである
のどちらか一方を満たすことです。
それぞれの定義は,
Def
-
はEulerグラフG 全ての頂点の次数が偶数:\Leftrightarrow -
は半EulerグラフG 次数が奇数であるものがちょうど2つ:\Leftrightarrow
です。(間違ってたらすみません...)
まず、 一筆書きを持つかどうかを調べるために、グラフが(半)Eulerグラフかどうかを調べます。
全ての頂点をチェックして、それぞれの次数を調べるだけです。
奇点の個数のチェック
func getOddNodes(g [][]int) []int {
var oddNodes []int
for i, nodes := range g {
if len(nodes)%2 == 1 {
oddNodes = append(oddNodes, i)
}
}
return oddNodes
}
getOddNodes
の戻り値は、奇点(次数が奇数)のリストです。返り値の長さが0ならばEulerグラフ、 2ならば半Eulerグラフ、それ以外ならどちらでもないです。
つまり、返り値の長さが0か2の場合は一筆書きが可能で、それ以外の場合は一筆書きが不可能ということがわかります。
2. 空配列を用意する
空配列p
を用意します。
p に入れ、G から除外する
3. (半Eulerグラフの場合)二つの奇点を結ぶパスを二つの奇点を結ぶ任意のパスを探して、グラフから削除します。このとき、どのようなパスを選んでも、グラフは新しいEulerグラフになります。
このアルゴリズムでは、隣接リストをsetで持っておけば、辺の削除を定数時間で行うことができます。しかし、Goはプリミティブ型としてsetを持っていないので、そのままsliceで行っています。
パスの探索
func findPath(G [][]int, from, to int) []int {
var p []int
// 辺を通ったかどうかを記録する
var passed [][]bool
for i := 0; i < len(G); i++ {
passed = append(passed, make([]bool, len(G)))
}
c := from
// fromからtoへのパスを一つ見つける
for {
p = append(p, c)
for _, n := range G[c] {
if !passed[c][n] {
passed[c][n] = true
passed[n][c] = true
c = n
break
}
}
if c == to {
p = append(p, c)
break
}
}
return p
}
パスの削除
func removePath(G [][]int, p []int) {
for i := 0; i < len(p)-1; i++ {
removeEdge(G, p[i], p[i+1])
}
}
func removeEdge(G [][]int, from, to int) {
for i, edge := range G[from] {
if edge == to {
G[from] = append(G[from][:i], G[from][i+1:]...)
break
}
}
for i, edge := range G[to] {
if edge == from {
G[to] = append(G[to][:i], G[to][i+1:]...)
break
}
}
}
p から孤立点でない点v を選ぶ
4. p
から孤立点でない点を一つ選んで、v
とします。
p
の長さが0のときは、G
から任意の点を一つ選んで、v
とします。
vの選択
func solve(G [][]int) []int {
oddNodes := getOddNodes(G)
var p []int
if len(oddNodes) == 1 || len(oddNodes) > 2 {
return p
}
v := -1
if len(oddNodes) == 2 {
// 奇点から奇点へのパスを探して, pに格納する
from := oddNodes[0]
to := oddNodes[1]
p = findPath(G, from, to)
// pをGから削除する
removePath(G, p)
// pから孤立点でない点を探す
for _, n := range p {
if len(G[n]) > 0 {
v = n
break
}
}
}
// 孤立点がない場合, Gから任意の孤立点でない点を選ぶ
if v == -1 {
for i := 0; i < len(G); i++ {
if len(G[i]) > 0 {
v = i
break
}
}
}
return p
}
v を含む閉路c を見つけ、G から削除
5. 3.で定義したfindPath
を使って求めることができます。
cの探索
c := findPath(G, v, v)
removePath(G, c)
p のv の場所にc を挿入
6. 可読性が低いので、あまり良くないかもしれませんが、これで挿入できます。
cの挿入
c := findPaht(G, v, v)
p = append(p[:idx], append(c, p[idx+1:]...)...)
G の辺が無くなるまで繰り返す
7. 3 ~ 5を繰り返し
func solve(G [][]int) {
numPath = countPath(G)
// ...
for numPath > 0 {
// pが空の場合は, Gから任意の孤立点でない点を探す
if len(p) == 0 {
for i := 0; i < len(G); i++ {
if len(G[i]) > 0 {
v = i
break
}
}
}
// vを含むサイクルを見つける
c := findPath(G, v, v)
if len(p) == 0 {
p = c
continue
}
idx := 0
for i := 0; i < len(p); i++ {
if p[i] == v {
idx = i
break
}
}
// pのvの部分をcに置き換える
p = append(p[:idx], append(c, p[idx+1:]...)...)
// cをGから削除する
removePath(G, c)
numPath -= len(c) - 1
}
// ...
}
func countPath(G [][]int) int {
count := 0
for i := 0; i < len(G); i++ {
count += len(G[i])
}
return count / 2
}
入力例
例1
0,1
0,3
0,4
1,4
2,3
2,4
3,4
[0 1 4 2 3 4 0 3]
例2
少し長いバージョンも試しました
入力例2
0,1
0,2
0,3
0,4
0,5
0,6
0,7
0,8
0,9
0,10
1,2
1,3
1,4
1,5
1,6
1,7
1,9
1,10
2,3
2,4
2,5
2,6
2,7
2,8
2,9
2,10
3,4
3,5
3,6
3,7
3,8
3,9
3,10
4,5
4,6
4,7
4,8
4,9
4,10
5,6
5,7
5,8
5,9
5,10
6,7
6,8
6,9
6,10
7,8
7,9
7,10
8,9
8,10
9,10
[1 9 8 10 9 6 7 8 6 10 7 9 4 5 6 4 7 5 8 4 10 5 9 2 3 4 2 5 3 6 2 7 3 8 2 10 3 9 0 10 1 0 2 1 3 0 4 1 5 0 6 1 7 0 8]
例3
出力からもわかる通り、例1と例2はともに半Eulerグラフです。最後に、Eulerグラフの場合も試してみます。
確かに、閉路となっていることがわかります。
0,1
0,2
0,3
0,4
1,2
1,3
1,4
2,3
2,4
3,4
[0 3 2 4 3 1 4 0 1 2 0 1 2 0]
まとめ
一応求めることはできましたが、完全には理解しきれていないです。いくつかの事実を認めたので、証明を与えられるようにします。
実装もかなり無駄が多いので、改善したいです。
Discussion