Interview/Algorithm

벨만 포드 Bellman-Ford

김 정출 2024. 10. 19. 00:21

벨만 포드 Bellman-Ford

  • 벨만 포드(Bellman-Ford) 알고리즘은 그래프에서 음의 가중치가 있는 간선을 포함한 최단 경로 문제를 해결할 수 있는 알고리즘입니다. 다익스트라(Dijkstra) 알고리즘과 달리 벨만 포드는 음의 가중치도 허용하며, 음의 사이클(negative cycle)이 존재하는지 여부도 확인할 수 있습니다.

벨만 포드 알고리즘의 주요 특징

  • 그래프: 방향 그래프(directed graph)와 음수 가중치를 허용하는 간선에서 동작.
  • 시간 복잡도: O(V * E), 여기서 V는 정점의 수, E는 간선의 수.
  • 출발점: 하나의 시작 노드로부터 모든 다른 노드까지의 최단 경로를 계산.
  • 음수 사이클 검출: 음의 가중치 사이클이 존재하는지 확인할 수 있음. 사이클이 존재하면 최단 경로를 정의할 수 없음.

알고리즘의 동작 원리

벨만 포드 알고리즘은 **반복적으로 간선을 완화(relax)**하면서 최단 경로를 찾아갑니다. 즉, 각 간선의 가중치와 두 노드 간의 거리를 비교하고, 더 짧은 경로를 발견할 경우 갱신하는 과정을 거칩니다.

  1. 초기화: 모든 정점까지의 거리를 무한대로 설정하고, 출발점의 거리는 0으로 설정합니다.
  2. 완화 과정: 그래프에 있는 모든 간선을 반복적으로 확인하며, 해당 간선을 통과했을 때 거리가 더 짧아지는지 확인하고, 짧아지면 그 경로로 거리를 갱신합니다. 이 과정을 정점의 수 V - 1번 반복합니다.
  3. 음수 사이클 검출: 마지막으로 모든 간선을 한 번 더 확인하여, 경로가 더 짧아질 수 있다면 음수 사이클이 존재함을 의미합니다.

벨만 포드 알고리즘의 단계

  1. 초기 상태 설정
    • 시작 정점에서 시작.
    • 모든 정점까지의 거리를 무한대로 설정 (단, 시작 정점의 거리는 0).
  2. 간선 완화(최단 경로 업데이트)
    • 모든 간선에 대해 정점 u에서 v로 이동하는 경로가 존재하는지 확인.
    • 현재 경로보다 더 짧은 경로를 찾으면, 그 경로로 거리를 갱신.
  3. 음수 사이클 검출
    • V - 1번 완화를 진행한 후에도 경로가 더 짧아질 수 있다면, 음수 사이클이 존재한다고 판단.

의사 코드

def bellman_ford(graph, V, E, src):
    # 거리 배열을 무한대로 초기화
    distance = [float('inf')] * V
    distance[src] = 0

    # 모든 간선을 V - 1번 완화
    for _ in range(V - 1):
        for u, v, weight in E:
            if distance[u] != float('inf') and distance[u] + weight < distance[v]:
                distance[v] = distance[u] + weight

    # 음수 사이클 검출
    for u, v, weight in E:
        if distance[u] != float('inf') and distance[u] + weight < distance[v]:
            print("음수 사이클이 존재합니다.")
            return

    return distance

장점

  • 음수 가중치를 가진 그래프에서도 최단 경로를 찾을 수 있음.
  • 음수 사이클이 존재하는지 확인 가능.

단점

  • 다익스트라 알고리즘보다 시간 복잡도가 높아, 큰 그래프에서는 비효율적일 수 있음.

벨만 포드 알고리즘은 음의 가중치가 있는 그래프에서 최단 경로를 찾는 문제에서 필수적인 알고리즘입니다. 특히 금융 모델링, 경로 최적화 문제 등에서 활용될 수 있습니다.


Python 구현

아래는 Python으로 벨만 포드 알고리즘을 구현한 코드입니다. 이 코드는 그래프의 노드와 간선을 입력받아, 음의 가중치가 있는 경우에도 최단 경로를 계산합니다. 또한 음수 사이클이 있는지 여부도 확인할 수 있습니다.

class Graph:
    def __init__(self, vertices):
        self.V = vertices  # 노드 개수
        self.edges = []    # 간선 리스트 (u, v, w)

    # 간선 추가
    def add_edge(self, u, v, w):
        self.edges.append((u, v, w))

    # 벨만 포드 알고리즘 구현
    def bellman_ford(self, src):
        # 모든 노드의 최단 거리를 무한대로 초기화
        distance = [float('inf')] * self.V
        distance[src] = 0  # 시작 노드의 거리는 0으로 설정

        # 각 간선을 V - 1번 완화
        for _ in range(self.V - 1):
            for u, v, w in self.edges:
                if distance[u] != float('inf') and distance[u] + w < distance[v]:
                    distance[v] = distance[u] + w

        # 음수 사이클 존재 여부 확인
        for u, v, w in self.edges:
            if distance[u] != float('inf') and distance[u] + w < distance[v]:
                print("음수 사이클이 존재합니다.")
                return None

        # 최종 최단 경로 결과 출력
        self.print_solution(distance)
        return distance

    # 결과 출력 함수
    def print_solution(self, dist):
        print("노드로부터의 최단 거리:")
        for i in range(self.V):
            print(f"노드 {i}의 최단 거리: {dist[i]}")

# 사용 예시
g = Graph(5)  # 5개의 노드가 있는 그래프 생성
g.add_edge(0, 1, -1)
g.add_edge(0, 2, 4)
g.add_edge(1, 2, 3)
g.add_edge(1, 3, 2)
g.add_edge(1, 4, 2)
g.add_edge(3, 2, 5)
g.add_edge(3, 1, 1)
g.add_edge(4, 3, -3)

# 노드 0에서 시작하여 최단 경로 찾기
g.bellman_ford(0)

코드 설명

  1. Graph 클래스:
    • __init__: 그래프의 노드 개수와 간선 리스트를 초기화합니다.
    • add_edge: 간선 (u, v, w)를 추가합니다. u는 출발 노드, v는 도착 노드, w는 간선의 가중치입니다.
    • bellman_ford: 벨만 포드 알고리즘을 수행하여 최단 경로를 계산하고, 음수 사이클이 있는지 확인합니다.
    • print_solution: 최단 경로 결과를 출력합니다.
  2. g.add_edge(u, v, w): 각 노드 간의 연결과 가중치를 설정합니다. 위 코드에서 노드 0을 출발점으로 설정했습니다.
  3. 음수 사이클 검출: 마지막에 한 번 더 모든 간선을 확인하여 음수 사이클이 존재하는지 검사합니다.

사용 예시

위 코드에서는 5개의 노드를 가진 그래프를 예시로 들었습니다. 출발 노드는 0이고, 노드 간의 가중치는 양수와 음수 모두를 포함합니다. 벨만 포드 알고리즘을 통해 최단 경로와 음수 사이클 여부를 확인할 수 있습니다.


네트워크 라우팅 알고리즘

벨만 포드 알고리즘은 라우팅 알고리즘에 적용할 수 있습니다. 특히, 네트워크에서 최단 경로 탐색네트워크 내의 음수 가중치 탐지에 유용하게 활용됩니다. 예를 들어, 벨만 포드는 **거리 벡터 라우팅(Distance Vector Routing)**에서 사용됩니다. 이 알고리즘은 라우팅 테이블을 업데이트하면서 네트워크 상의 최단 경로를 탐색하고, 장애나 연결 손실 등을 해결하는 데 중요한 역할을 합니다.

라우팅 알고리즘에서 벨만 포드의 활용 예시

  1. 거리 벡터 라우팅 프로토콜(RIP - Routing Information Protocol): 벨만 포드는 RIP와 같은 거리 벡터 라우팅 프로토콜에서 사용됩니다. 네트워크에서 라우터는 자신의 인접 라우터에게 자신이 아는 모든 경로와 해당 경로의 거리를 주기적으로 전송합니다. 라우터는 수신한 정보를 기반으로 자신의 라우팅 테이블을 업데이트하고, 최단 경로를 결정합니다.
  2. 링크 상태에 따른 경로 재계산: 네트워크에서 링크 상태가 변화할 수 있는데, 예를 들어, 네트워크의 링크가 끊기거나 새로운 링크가 추가되었을 때, 벨만 포드 알고리즘은 이러한 변화에 따라 모든 라우터가 최단 경로를 다시 계산하는 데 사용될 수 있습니다.

예시 시나리오: 네트워크에서 벨만 포드의 활용

상황: 다섯 개의 라우터(A, B, C, D, E)가 있는 네트워크가 있다고 가정합니다. 각 라우터는 서로 연결된 링크와 링크의 대기 시간(혹은 비용)을 가지고 있습니다. 이 네트워크에서 벨만 포드 알고리즘을 사용해 각 라우터가 다른 모든 라우터까지의 최단 경로를 계산한다고 가정합니다.

1. 네트워크 초기 상태

  • 라우터 A는 B와 연결되어 있으며 비용은 2입니다.
  • 라우터 B는 C와 연결되어 있으며 비용은 3입니다.
  • 라우터 C는 D와 연결되어 있으며 비용은 1입니다.
  • 라우터 D는 E와 연결되어 있으며 비용은 4입니다.
  • A는 E로 바로 가는 경로가 없으며, A에서 E로 가는 경로는 여러 단계를 거쳐야 합니다.

2. 벨만 포드를 활용한 경로 계산

  1. 각 라우터는 자신의 라우팅 테이블을 초기화합니다. 각 라우터는 다른 라우터까지의 경로를 무한대(∞)로 설정하고, 자신에게 가는 경로의 비용은 0으로 설정합니다.
  2. 벨만 포드 알고리즘이 작동하면, 각 라우터는 인접 라우터로부터 비용 정보를 수신하고, 자신의 테이블을 업데이트합니다.
  3. 이 과정을 V-1번(즉, 네트워크의 라우터 개수-1번) 반복하여 모든 라우터가 자신을 제외한 다른 라우터까지의 최단 경로를 알아내게 됩니다.

3. 라우터 장애 발생 시의 경로 재계산

네트워크에서 장애가 발생하여 라우터 C와 D 사이의 링크가 끊겼다고 가정합니다. 이때 벨만 포드 알고리즘은 다음과 같이 동작할 수 있습니다:

  1. 라우터 C는 더 이상 D로 가는 경로를 찾을 수 없으며, 이 정보를 인접 라우터에 전파합니다.
  2. 라우터들은 새로운 정보를 바탕으로 라우팅 테이블을 업데이트하며, 다른 경로가 있는지 확인합니다.
  3. 만약 다른 경로가 존재한다면, 해당 경로로 라우팅 테이블을 수정합니다. 예를 들어, A에서 E로 가는 경로가 A → B → E와 같은 새로운 경로로 변경될 수 있습니다.

라우팅 테이블 업데이트의 예시

라우팅 테이블은 라우터가 네트워크 내 다른 라우터로 패킷을 전송할 때 사용됩니다. 벨만 포드 알고리즘을 사용하면 각 라우터는 다음과 같은 테이블을 유지합니다:

목적지 라우터 다음 라우터 비용

B B 2
C B 5
D C 6
E D 10

이 테이블은 라우터 A에서 다른 라우터까지의 경로와 비용을 나타내며, 라우터 A는 각 패킷을 해당 경로에 따라 전달합니다.

class Router:
    def __init__(self, name, num_routers):
        self.name = name  # 라우터의 이름
        self.num_routers = num_routers  # 네트워크 내의 라우터 수
        self.edges = []  # 연결된 간선 (다른 라우터로의 연결, 비용)
        self.distance = [float('inf')] * num_routers  # 다른 라우터까지의 최단 거리
        self.prev_router = [-1] * num_routers  # 최단 경로 상의 이전 라우터

    def add_edge(self, to_router, cost):
        self.edges.append((to_router, cost))

    def bellman_ford(self, start_idx):
        # 시작 라우터까지의 거리는 0으로 설정
        self.distance[start_idx] = 0

        # 간선 완화: V - 1번 반복
        for _ in range(self.num_routers - 1):
            for router_idx, cost in self.edges:
                if self.distance[start_idx] + cost < self.distance[router_idx]:
                    self.distance[router_idx] = self.distance[start_idx] + cost
                    self.prev_router[router_idx] = start_idx

        # 음수 사이클 검출
        for router_idx, cost in self.edges:
            if self.distance[start_idx] + cost < self.distance[router_idx]:
                print(f"{self.name}에서 음수 사이클이 검출되었습니다.")
                return False

        return True

    def print_routing_table(self):
        print(f"라우터 {self.name}의 라우팅 테이블:")
        for i in range(self.num_routers):
            if self.distance[i] == float('inf'):
                print(f"  -> 라우터 {i}: 도달 불가")
            else:
                print(f"  -> 라우터 {i}: 거리 = {self.distance[i]}")
        print()

# 라우터 간의 연결을 설정
def setup_network():
    routers = []

    # 라우터 A
    router_a = Router('A', 5)
    router_a.add_edge(1, 2)  # A -> B 비용 2
    router_a.add_edge(2, 4)  # A -> C 비용 4
    routers.append(router_a)

    # 라우터 B
    router_b = Router('B', 5)
    router_b.add_edge(2, 3)  # B -> C 비용 3
    router_b.add_edge(3, 2)  # B -> D 비용 2
    router_b.add_edge(4, 2)  # B -> E 비용 2
    routers.append(router_b)

    # 라우터 C
    router_c = Router('C', 5)
    router_c.add_edge(3, 1)  # C -> D 비용 1
    routers.append(router_c)

    # 라우터 D
    router_d = Router('D', 5)
    router_d.add_edge(1, 1)  # D -> B 비용 1
    routers.append(router_d)

    # 라우터 E
    router_e = Router('E', 5)
    router_e.add_edge(3, -3)  # E -> D 비용 -3
    routers.append(router_e)

    return routers

# 네트워크 라우팅 테이블 계산
def calculate_routing_tables(routers):
    for i, router in enumerate(routers):
        router.bellman_ford(i)
        router.print_routing_table()

# 네트워크 장애 발생 후 경로 재계산
def simulate_network_failure(routers):
    print("\\n네트워크 장애 발생: C -> D 링크가 단절되었습니다.\\n")
    
    # 라우터 C와 D 사이의 연결을 제거
    routers[2].edges = [edge for edge in routers[2].edges if edge[0] != 3]

    # 장애 후 경로 재계산
    calculate_routing_tables(routers)

# 메인 함수
def main():
    # 네트워크 설정
    routers = setup_network()

    # 라우팅 테이블 초기 계산
    print("초기 라우팅 테이블 계산:")
    calculate_routing_tables(routers)

    # 네트워크 장애 시뮬레이션 및 경로 재계산
    simulate_network_failure(routers)

if __name__ == "__main__":
    main()

코드 설명

  1. Router 클래스:
    • 라우터의 이름, 라우팅 테이블을 유지하며, 다른 라우터와의 연결을 설정합니다.
    • bellman_ford: 각 라우터에서 벨만 포드 알고리즘을 사용하여 다른 라우터까지의 최단 경로를 계산합니다.
    • add_edge: 라우터 간의 연결(간선)과 비용을 설정합니다.
  2. 라우팅 테이블 출력: 각 라우터의 최단 경로와 비용을 출력하는 함수입니다.
  3. 네트워크 설정: 5개의 라우터(A, B, C, D, E)가 연결된 네트워크를 구성합니다. 각 라우터는 다른 라우터와의 간선과 비용을 설정합니다.
  4. 라우팅 테이블 계산: 각 라우터에서 벨만 포드를 실행하여 다른 모든 라우터까지의 최단 경로를 계산하고, 이를 출력합니다.
  5. 네트워크 장애 시뮬레이션: 네트워크 장애가 발생하는 상황을 시뮬레이션합니다. 예를 들어, C 라우터와 D 라우터 사이의 링크가 끊기는 경우를 시뮬레이션하고, 장애 발생 후 경로를 재계산합니다.

실행 예시

  1. 초기 라우팅 테이블 계산: 각 라우터는 네트워크 내 다른 모든 라우터까지의 최단 경로를 계산합니다.
  2. 네트워크 장애 발생 후 재계산: C → D의 링크가 끊기고, 새로운 경로가 반영된 라우팅 테이블이 출력됩니다.

음수 가중치와 음수 사이클

네트워크에서 음수 가중치가 의미하는 것은, 어떤 경로가 시간이 지남에 따라 더 짧아질 수 있다는 의미입니다. 이는 물리적인 네트워크에서 자주 발생하는 상황은 아니지만, 가상 네트워크나 비용 기반 시스템에서 중요할 수 있습니다. 벨만 포드는 음수 사이클을 감지할 수 있기 때문에, 경로 비용이 지속적으로 감소하는 네트워크가 있으면 이를 감지하고 네트워크 관리자에게 경고할 수 있습니다.


결론

벨만 포드 알고리즘은 라우팅 알고리즘에서 다음과 같은 방식으로 활용됩니다:

  • 각 라우터가 네트워크 내 다른 모든 라우터까지의 최단 경로를 계산하는 데 사용됩니다.
  • 네트워크 상태 변화(링크 단절, 새로운 링크 연결)에도 유연하게 대응할 수 있습니다.
  • 음수 사이클과 같은 특수 상황을 감지하여 네트워크에서 발생할 수 있는 문제를 미리 예방할 수 있습니다.

이러한 특성 덕분에 벨만 포드는 네트워크 라우팅 프로토콜, 특히 RIP와 같은 거리 벡터 라우팅에서 중요한 역할을 하고 있습니다.