[Python] ベルマン–フォード法

Pythonで、ベルマン–フォード法を使って、重み付きの有向グラフの単一始点最短経路問題を解きます。

以下の続きです。

Python で、ダイクストラ法を使って、重み付きの有向グラフの単一始点最短経路問題を解きます。 以下の続きです。 ダイクストラ法 ダイクストラ法(だいくすとらほう、英: Dijkstra's algorithm)はグラフ理論における辺の重みが...

ダイクストラ法は辺の重みがゼロ以上の場合でしたが、ベルマン–フォード法は辺の重みが負の場合に使われます。

負の閉路がある場合は最短経路を出すことはできませんが、負の閉路の存在を答えることができます。

ベルマン–フォード法

ベルマン–フォード法 (Bellman–Ford algorithm) は、重み付き有向グラフにおける単一始点の最短経路問題を解くラベル修正アルゴリズム[1]の一種である。各辺の重みは負数でもよい。辺の重みが非負数ならば優先度付きキューを併用したダイクストラ法の方が速いので、ベルマン–フォード法は辺の重みに負数が存在する場合に主に使われる

出典: フリー百科事典『ウィキペディア(Wikipedia)』

以下のようなグラフの探索を行います。

最初のグラフは、ダイクストラ法で用いたのと同じグラフです。

a を出発地点、e を目的地とした場合、最短経路は「a -> c -> b -> e」、移動距離は7になるはずです。

負の辺の重み無し

2つ目のグラフは、負の重みの辺を持つグラフです。

a を出発点、d を目的地とすると、 ベルマン–フォード法 により、a から d への最短移動経路: [‘a’, ‘b’, ‘e’, ‘d’] 、移動距離は -2 になるはずです。

負の辺の重みあり・負の閉路無し

3つ目は負の閉路のあるグラフです。

b -> e -> d の移動の総和が -1 と負の閉路を形成しています。

ベルマン–フォード法で最短経路を出すことはできませんが、 負の重みの閉路を検出することができるはずです。

import heapq
import sys

class Vertex(object):
    def __init__(self, id):
        self.id = id
        self.adjacent = {}
        self.distance = sys.maxsize
        self.visited = False
        self.previous = None
    
    def __lt__(self, other):
        return self.distance < other.distance

    def add_neighbor(self, neighbor, weight=0):
        self.adjacent[neighbor] = weight
    
    def get_neighbors(self):
        return self.adjacent

    def get_connections(self):
        return self.adjacent.keys()  

    def get_vertex_id(self):
        return self.id

    def get_weight(self, neighbor):
        return self.adjacent[neighbor]
    
    def set_distance(self, distance):
        self.distance = distance

    def get_distance(self):
        return self.distance

    def set_previous(self, previous_vertex):
        self.previous = previous_vertex 
    
    def get_previous(self):
        return self.previous
    
    def set_visited(self):
        self.visited= True


class Graph(object):
    def __init__(self):
        self.vertex_dict = {}
        self.num_vertex = 0
    
    def __iter__(self):
        return iter(self.vertex_dict.values())

    def __len__(self):
        return self.num_vertex

    def add_vertex(self, id):
        self.num_vertex = self.num_vertex + 1
        new_vertex = Vertex(id)
        self.vertex_dict[id] = new_vertex
        return new_vertex

    def get_vertex(self, id):
        if id in self.vertex_dict:
            return self.vertex_dict[id]
        else:
            return None

    def add_edge(self, frm, to, weight=0):
        if frm not in self.vertex_dict:
            self.add_vertex(frm)
        if to not in self.vertex_dict:
            self.add_vertex(to)
        self.vertex_dict[frm].add_neighbor(self.vertex_dict[to], weight)

    def get_vertices(self):
        return self.vertex_dict.keys()

    def get_edges(self):
        edges = []
        for v in self.vertex_dict.values():
            for w in v.get_connections():
                vid = v.get_vertex_id()
                wid = w.get_vertex_id()
                edges.append((vid, wid, v.get_weight(w)))
        return edges


def dijkstra(graph, source, destination):
    source.set_distance(0)

    priority_queue = [(vertex.get_distance(), vertex) for vertex in graph]
    heapq.heapify(priority_queue)

    while len(priority_queue):
        nearest_vrtex = heapq.heappop(priority_queue)
        current = nearest_vrtex[1]
        current.set_visited()

        for next in current.adjacent:
            if next.visited:
                continue
            new_distance = current.get_distance() + current.get_weight(next)

            if next.get_distance() > new_distance:
                next.set_distance(new_distance)
                next.set_previous(current)
                heapq.heappush(priority_queue, (next.get_distance(), next))

    print('dijkstra:')        
    for vertex in graph.vertex_dict.values():
        print(f'{source.get_vertex_id()} から {vertex.get_vertex_id()} への移動距離: {vertex.get_distance()}')

    shortest_path = [destination.get_vertex_id()]
    while destination.previous:
        shortest_path.append(destination.previous.get_vertex_id())
        destination = destination.previous
    print(f'{source.get_vertex_id()} から {vertex.get_vertex_id()} への最短移動経路:', shortest_path[::-1])


def bellman_ford(graph, source, destination):
    # 初期化
    source.set_distance(0)

    # 辺の緩和を反復
    for _ in range(len(graph) - 1):
        for u in graph:
            for v in u.adjacent:
                if v.get_distance() > u.distance + u.get_weight(v):
                    v.set_distance(u.distance + u.get_weight(v))
                    v.set_previous(u)
    
    print('bellmanford:')
    # 負の重みの閉路がないかチェック
    for u in graph:
        for v in u.adjacent:
            if v.get_distance() > u.distance + u.get_weight(v):
                print('負の重みの閉路があります。')
                return
        
    for vertex in graph.vertex_dict.values():
        print(f'{source.get_vertex_id()} から {vertex.get_vertex_id()} への移動距離: {vertex.get_distance()}')

    shortest_path = [destination.get_vertex_id()]
    while destination.previous:
        shortest_path.append(destination.previous.get_vertex_id())
        destination = destination.previous
    print(f'{source.get_vertex_id()} から {vertex.get_vertex_id()} への最短移動経路:', shortest_path[::-1])



if __name__ == '__main__':

    graph_no_neg_edges = Graph()
    graph_no_neg_edges.add_vertex('a')
    graph_no_neg_edges.add_vertex('b')
    graph_no_neg_edges.add_vertex('c')
    graph_no_neg_edges.add_vertex('d')
    graph_no_neg_edges.add_vertex('e')
    graph_no_neg_edges.add_edge('a', 'b', 4)  
    graph_no_neg_edges.add_edge('a', 'c', 1)
    graph_no_neg_edges.add_edge('b', 'e', 4)
    graph_no_neg_edges.add_edge('c', 'b', 2)
    graph_no_neg_edges.add_edge('c', 'd', 4)
    graph_no_neg_edges.add_edge('d', 'e', 4)

    source = graph_no_neg_edges.get_vertex('a')
    destination = graph_no_neg_edges.get_vertex('e')
    dijkstra(graph_no_neg_edges, source, destination)
    bellman_ford(graph_no_neg_edges, source, destination)
    # dijkstra、bellmanford ともに同じ結果
    # a から a への移動距離: 0
    # a から b への移動距離: 3
    # a から c への移動距離: 1
    # a から d への移動距離: 5
    # a から e への移動距離: 7
    # a から e への最短移動経路: ['a', 'c', 'b', 'e']

    graph_neg_edges = Graph()
    graph_neg_edges.add_vertex('a')
    graph_neg_edges.add_vertex('b')
    graph_neg_edges.add_vertex('c')
    graph_neg_edges.add_vertex('d')
    graph_neg_edges.add_vertex('e')
    graph_neg_edges.add_edge('a', 'b', -1)  
    graph_neg_edges.add_edge('a', 'c', 4)
    graph_neg_edges.add_edge('b', 'c', 3)
    graph_neg_edges.add_edge('b', 'd', 2)
    graph_neg_edges.add_edge('b', 'e', 2)
    graph_neg_edges.add_edge('d', 'b', 1)
    graph_neg_edges.add_edge('d', 'c', 5)
    graph_neg_edges.add_edge('e', 'd', -3)

    source = graph_neg_edges.get_vertex('a')
    destination = graph_neg_edges.get_vertex('d')
    bellman_ford(graph_neg_edges, source, destination)
    # a から a への移動距離: 0
    # a から b への移動距離: -1
    # a から c への移動距離: 2
    # a から d への移動距離: -2
    # a から e への移動距離: 1
    # a から d への最短移動経路: ['a', 'b', 'e', 'd']

    graph_neg_edges_neg_cycle = Graph()
    graph_neg_edges_neg_cycle.add_vertex('a')
    graph_neg_edges_neg_cycle.add_vertex('b')
    graph_neg_edges_neg_cycle.add_vertex('c')
    graph_neg_edges_neg_cycle.add_vertex('d')
    graph_neg_edges_neg_cycle.add_vertex('e')
    graph_neg_edges_neg_cycle.add_edge('a', 'b', -1)  
    graph_neg_edges_neg_cycle.add_edge('a', 'c', 4)
    graph_neg_edges_neg_cycle.add_edge('b', 'c', 3)
    graph_neg_edges_neg_cycle.add_edge('b', 'd', 2)
    graph_neg_edges_neg_cycle.add_edge('b', 'e', 2)
    graph_neg_edges_neg_cycle.add_edge('d', 'b', 0)
    graph_neg_edges_neg_cycle.add_edge('d', 'c', 5)
    graph_neg_edges_neg_cycle.add_edge('e', 'd', -3)

    source = graph_neg_edges_neg_cycle.get_vertex('a')
    destination = graph_neg_edges_neg_cycle.get_vertex('d')
    bellman_ford(graph_neg_edges_neg_cycle, source, destination)
    # 負の重みの閉路があります。

想定通りの結果を得ることができました。