0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

この記事は ひとりアドベントカレンダーRosettaCodeで楽しむプログラミング Advent Calendar 2025の18日めの記事です。

オイラー閉路を発見するための効率的な方法、とあるが、リンク先のpedia.deは他の言語に翻訳されていない、独自研究のような雰囲気。

示されているアルゴリズムを、翻訳と、内容の再調整をしながら引き写す。

事前条件preconditionを宣言しておきながら、アルゴリズムでまず VerifyEulerianCircuit() をしている部分は見なかったことにする。
割とimmutableに書きたいアトモスフィアを感じるが、命令的にかかないといけないという呪縛のせいでいびつになっている。
Circuit, Stack だけが状態(変数)のように見せて、実はグラフGも変更するのがその最たるところ。
出発する頂点の選択について degree(v) > 0 と条件を付けているが、前提条件からそうでないものなどない。(グラフが |V| = 1 でもない限り。)
currentをStackとは別物として扱っているが、多分Stackのtopがcurrentになっている。
実装見本らしいOCaml版でもそうしていて、この擬似コードとは違っている。
メインループ while の中の if の前後を変えた。先に動く方を前に置いた方がわかりやすいと思う。

入力 グラフG=(V,E)
事前条件
・グラフは強連結
・全ての頂点の入り次数と出次数が等しい
出力 頂点のリストとして、オイラー閉路

手続き
Circuit ← FindEulerianCircuit(G)
Circuitが全ての辺を含むなら成功、さもなくば存在しない

Function FindEulerianCircuit(Graph G):
    Circuit ← 空リスト  // 確定したオイラー閉路を後ろから積み上げるリスト
    Stack ← 空スタック  // 頂点のスタック

    // 出発点を任意に選ぶ
    current ← V中の任意の頂点v ただしvの次数 > 0 // 条件付けなくてもそんなのしかない。
//    current を Stack にpushする  // 多分これは嘘。一つ多くなってしまう

    While Stack が空でない:
// 未使用辺がまだあるなら、それを辿る
        if currentの出次数 > 0:
            current を Stack にpushする
            next ← currentの任意の隣接頂点
            currentからnextへの辺をGから削除する
            current ← next
        else:
// 未使用辺が無くなったら閉路に追加する
            currentをCircuitの先頭に追加する
            current ← Stackからpopした頂点

    Return Circuit

タスク指示に「OCaml版と同じ例で試せ」とあるように、それがお手本なのだろう。
と思って眺めると、while とか書いてあってほぼこの擬似コードに近くて、あれ?OCamlって命令型言語でしたっけ?という錯覚に陥る。

Haskell化

Stackは頂点のリストで表現できる。
currentは実はそのスタックトップであると解釈する。
Circuitはそのまま頂点のリストとする。

グラフは、頂点を連続する Int、隣接リストを Array Int [Int] で表現するとして、immutableに関数的に書くと、このアルゴリズムは擬似コードよりもコンパクトになる。
グラフの辺をもいでいくので Data.Array.Diff としているが、別に普通の Data.Array でも動く。

import Data.Array.Diff

findEulerianCircuit :: DiffArray Int [Int] -> Maybe [Int]
findEulerianCircuit graph0 = go graph0 [] [start]
  where
    start = fst $ bounds graph0                             -- 添え字の下限から開始
    go graph circuit []                                     -- スタックが空になったら終了
      | all null (elems graph) = Just circuit               -- 辺が尽きていたら成功
      | otherwise              = Nothing
    go graph circuit (current:stack)
      | null edges = go graph (current:circuit) stack       -- 辺が尽きているとき、currentをstackからcircuitへ移す
      | otherwise  = go graph1 circuit (next:current:stack) -- まだあるならnextへ進む
      where
        edges = graph ! current                       -- currentの出辺
        next = head edges                             -- 任意にnextを選ぶ
        graph1 = graph // [(current, tail edges)]     -- current→nextの辺を削除

graph1, graph2 :: DiffArray Int [Int]
graph1 = listArray (0,2) [[1],[2],[0]]
graph2 = listArray (0,6) [[1,6],[2],[0,3],[4],[2,5],[0],[4]]
ghci> findEulerianCircuit graph1
Just [0,1,2,0]
ghci> findEulerianCircuit graph2
Just [0,1,2,0,6,4,2,3,4,5,0]

さらに悪ノリすると、go 最後の7行を4行に圧縮もできる。

    go graph circuit (current:stack) =
      case graph ! current of
        next:rest -> go (graph // [(current, rest)]) circuit (next:current:stack)
        []        -> go graph (current:circuit) stack

おわりに

クワイン・マクラスキー法のときも思ったのだけど、リンクリストを繋ぎ直すだとか、in-placeな配列操作だとかが主眼なものでない多くのアルゴリズムは、命令型な擬似言語よりも関数型、データ不変な形で表現する方がよいのでは?という思いがますます強まりました。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?