Rubyでベルマン-フォード法

グラフの最短路問題を解くアルゴリズム。
アルゴリズム図鑑Wikipediaを参考にRubyで実装してみた。辺に重みのついた有向グラフに対して。

概要

  • 各頂点のコスト(重み)を無限(Float::INFINITY)で初期化
  • 各頂点から順番にそれぞれ隣接する頂点へのコストを計算(頂点のコスト+辺のコスト)し、既存のコストより小さくなった場合は隣接する頂点のコストを更新
  • コストの更新がなくなるまでコスト計算をループして繰り返す
  • 負の閉路がある場合はコストの更新が永遠に続くのでエラーとする(通常頂点数より多くループが続くことはないので、それを条件に負の閉路の有無を判定する)

コード

# 頂点
class Vertex
  attr_reader :key, :adjacents

  def initialize(key)
    @key = key
    @adjacents = {}
  end

  def connect(vertex, weight)
    @adjacents[vertex.key] = weight
  end
end

# シンプルな有方向グラフ
class Graph
  attr_reader :vertexes

  def initialize
    @vertexes = {}
  end

  def add_vertex(key)
    @vertexes[key] = Vertex.new(key)
  end

  def get_vertex(key)
    @vertexes[key]
  end

  def connect(from, to, weight = 0)
    if !@vertexes.has_key?(from)
      raise "Error: from vertex '#{from}' is not exist"
    end

    raise "Error: to vertex '#{to}' is not exist" if !@vertexes.has_key?(to)

    @vertexes[from].connect(@vertexes[to], weight)
  end
end

class ShortestRouteFinder
  def initialize(graph)
    @graph = graph
  end

  # ベルマン-フォード法
  # fromから各頂点までの最短距離(コスト)を求める
  def search(from)
    p "searching shortest routes from #{from}:"

    # 全頂点に対してコスト無限で初期化
    @vertex_weights = {}
    @routes = {}
    @graph.vertexes.keys.each do |k|
      @vertex_weights[k] = Float::INFINITY
      @routes[k] = [] # fromから各頂点までの最短の経路で通過する頂点を記録
    end

    # from自身のコストは0
    @vertex_weights[from] = 0
    @routes[from].push(from)

    # 全頂点までのコストを計算
    loop_count = 0
    while true
      before_vertex_weights = @vertex_weights.clone

      # 1巡ですべての頂点を1回ずつ処理
      @vertex_weights.keys.each do |key|
        update_vertex_weight(key)
      end

      # コストの変化がなくなったら終了
      break if before_vertex_weights == @vertex_weights

      # 頂点数よりループ回数が多くなった場合は負の閉路ありとみなす
      loop_count += 1
      if loop_count > @graph.vertexes.count
        p @vertex_weights
        raise "Error: 負の閉路があるのでいずれかの頂点のコストが永遠に小さくなり続けるよ"
      end
    end

    p @vertex_weights
    p @routes
  end

  # 頂点に隣接する頂点までのコストを求める
  def update_vertex_weight(vertex_key)
    vertex_weight = @vertex_weights[vertex_key] # 頂点の現在のコスト

    # 隣接する頂点のコストを更新 vertex_keyの頂点からのびる辺をすべて処理
    @graph.vertexes[vertex_key].adjacents.each do |k, adjacent_weight|
      # 新しく求めたコストが小さい場合のみ更新
      new_neighbor_weight = vertex_weight + adjacent_weight
      if @vertex_weights[k] > new_neighbor_weight
        @vertex_weights[k] = new_neighbor_weight 
        @routes[k] = @routes[vertex_key] + [k] # 経路を記録
        # p "vertex: #{k}, from_vertex: #{vertex_key}, weight: #{@vertex_weights[k]}, route: #{@routes[k]}"
      end
    end
  end
end


graph = Graph.new
%w[A B C D E F G].each { |key| graph.add_vertex(key) }

v1 = graph.get_vertex("A")

graph.connect("A", "B", 9)
graph.connect("A", "C", 2)
graph.connect("B", "C", 3)
graph.connect("B", "D", 3)
graph.connect("B", "E", 5)
graph.connect("C", "B", 3)
graph.connect("C", "D", 2)
graph.connect("D", "B", 3)
graph.connect("D", "E", 4)
graph.connect("D", "F", 6)
graph.connect("E", "F", -2)
# A -> C -> D -> B = 5
# A -> C = 2
# A -> C -> D = 4
# A -> C -> D -> E = 8
# A -> C -> D -> E -> F = 6

route_finder = ShortestRouteFinder.new(graph)

route_finder.search("A")
# {"A"=>0, "B"=>5, "C"=>2, "D"=>4, "E"=>8, "F"=>6, "G"=>Infinity}
# {"A"=>["A"], "B"=>["A", "C", "B"], "C"=>["A", "C"], "D"=>["A", "C", "D"], "E"=>["A", "C", "D", "E"], "F"=>["A", "C", "D", "E", "F"], "G"=>[]}

route_finder.search("B")
# {"A"=>Infinity, "B"=>0, "C"=>3, "D"=>3, "E"=>5, "F"=>3, "G"=>Infinity}
# {"A"=>[], "B"=>["B"], "C"=>["B", "C"], "D"=>["B", "D"], "E"=>["B", "E"], "F"=>["B", "E", "F"], "G"=>[]}

グラフの実装が違うとまた違った実装になるだろう。

負の閉路はどこか適当な辺の重みを-10とかにしてみると発生する。
頂点のコストと辺のコストの和がずっとマイナスになってしまうので、新しいコストが既存のコストよりも常に小さくなり続ける。

余談

最初は始点からすべての頂点をたどるのに、変に難しく考えて再帰で頂点から隣接する頂点へのコスト計算をしていたのだが、ベルマン-フォード法の説明をよく読むと、単純に1回のループですべての辺を処理し、ループしていくうちに頂点のコストの更新がなくなったら終了というものだったので、書き直したりしていた。

その実装でも最短路は求められていたけど、1回のループで同じ辺がコスト計算の対象になる回数が多くなるのでやや非効率だった。

2022-11-21コード修正

頂点の重みだけ求めて経路を記録していなかったので記録するようにした。

それと最短路を求めるロジックをGraphクラスからShortestRouteFinderに分離した。