[알고리즘개념]

[APS응용] 그래프최소비용3 : 벨만-포드, 플로이드워셜

whatezlife 2025. 3. 31. 15:39

1. 벨만 포드 알고리즘

단일 출발점 최단 경로 문제를 해결 가능 (음의 가중치를 포함하는 그래프에서 최단 경로 구할 수 있음)

  • 음수 사이클(negative cycle) 탐지 가능 -> 최단 경로가 '무한대로 줄어드는 경우' 방지
  • 모든 간선을 순회하면서 알려진 최단 경로보다 더 짧은 경로가 있다면 값을 갱신하는 과정을 진행 (최대 V-1번)
  • 시간 복잡도 O(VE) // V:정점의 수, E:간선의 수
  • 동작 과정
    • 초기화 -> 시작 정점 거리 0. 다른 모든 정점 거리 무한대
    • Relaxation 과정(V-1번 반복) -> 모든 간선 갱신 (업데이트)
    • 음수 사이클 검사
      • 최단 거리 갱신 O -> 음의 사이클 존재 O
      • 최단 거리 갱신 X -> 음의 사이클 존재 X;
[초기화]
모든 거리 = INF
시작 정점의 거리만 0으로 설정

     ↓

[V-1번 반복]
모든 간선(u → v, cost) 확인:
    if (dist[u] + cost < dist[v]):
        dist[v] = dist[u] + cost

     ↓

[음수 사이클 체크]
한 번 더 전체 간선 확인:
    if (dist[u] + cost < dist[v]):
        → 음수 사이클 존재!

 

📊 벨만포드 동작 예시 (표 형태)

예시 그래프

시작 정점: A

  • 간선 목록:
    A → B (4), A → C (2), C → B (-1), B → D (2), C → D (4)
단계거리 A B C D 비고
초기 0 시작 정점만 0
1회차 0 4 2 6 A→B, A→C, C→B, B→D, C→D 갱신
2회차 0 1 2 3 C→B(-1) 다시 갱신됨
3회차 0 1 2 3 더 이상 갱신 없음
  • 총 V-1 = 3번 반복 후 거리 고정
  • 4번째 반복 시 변화 없으면 → 음수 사이클 없음
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Scanner;

public class 그래프최소비용06_벨만포드 {
	static class Edge{
		int s, e, cost;
		public Edge() {
		}
		public Edge(int s, int e, int cost) {
			super();
			this.s = s;
			this.e = e;
			this.cost = cost;
		}
	}
	static String input1 = "6 7\r\n"
			+ "0 1 4\r\n"
			+ "0 2 5\r\n"
			+ "1 3 -2\r\n"
			+ "2 4 8\r\n"
			+ "3 5 7\r\n"
			+ "4 2 -3\r\n"
			+ "4 5 6";
	
	static String input2 = "4 4\r\n"
			+ "0 1 5\r\n"
			+ "1 2 -8\r\n"
			+ "2 1 3\r\n"
			+ "2 3 6";
	
	
	public static void main(String[] args) {
		Scanner sc = new Scanner(input1);
		int V = sc.nextInt();
		int E = sc.nextInt();
		List<Edge> edges = new ArrayList<>();
		for (int i = 0; i<E; i++) {
			int s= sc.nextInt();
			int e = sc.nextInt();
			int cost = sc.nextInt();
			edges.add(new Edge(s, e, cost));
		}//간선을 입력받자
		dist = new int[V]; // 시작 정점은 0번부터..
		bellmanFord(V, 0, edges); 

	}
	static int[] dist;
	static final int INF = Integer.MAX_VALUE;

	// V: 전체 정점의 개수
	// start : 시작 정점
	// edges : 간선의 배열
	private static void bellmanFord(int V, int start, List<Edge> edges) {
		Arrays.fill(dist, INF);
		dist[start] = 0; // 시작점만 0으로 바꾼다
		//1. 모든 간선을 (v-1)번 반복 => relazation 작업 수행
		for (int i = 0; i<V-1; i++) {
			boolean flag = false;
			for(Edge edge : edges) {
				//갱신 : 출발점이 무한대가 아닌 애들만!
				if (dist[edge.s]!=INF && dist[edge.s]+edge.cost < dist[edge.e]) {
					dist[edge.e] = dist[edge.s]+edge.cost; //갱신!
					flag = true;
				}
			}
			if(!flag) break; // 조기 종료 하겠다 . 갱신이 일어나지 않았어.
		}// 완화작업 끝
		//2. 음수 사이클 쳌
		boolean negativeCycle = false;
		for (Edge edge : edges) {
			if (dist[edge.s] != INF && dist[edge.s]+edge.cost < dist[edge.e]) {
				//해당 조건문이 만족한다.... -> 갱신이 일어났다! (음의 사이클이 존재한당)
				negativeCycle = true;
			}
		}
		//3. 결과 확인
		if (negativeCycle) System.out.println(" 음의 사이클");
		else System.out.println(Arrays.toString(dist));
	}

}

 

2. 플로이드 워셜 알고리즘

모든 정점 쌍에 대한 최단 경로 -> 단일 출발점 X, 모든 정점 쌍에 대한 최단 경로 계산

  • 음의 가중치를 포함하는 그래프에서 최단 경로 구할 수 있음 (단 음의 사이클 X)
  • 경유지를 하나씩 추가하면서, 각 정점 쌍의 경로를 점진적으로 개선함
  • 경로는 모름, 오로지 arr에는 거리만 존재함
  • 시간복잡도 O(v^3) // V : 정점의 수
  • 동작 과정 (경 출과 도둑) -> 경유지 출발 도착
    • 초기화 : 직접 연결된 간선이 있으면 해당 가중치로 갱신, 간선이 없는 경우 무한대, 자기 자신은 0
    • 중간 정점 (경유지) 추가 (3중 for문) : 중간 정점 K를 경유지로 사용했을 때 i에서 j로 가는 경로 거리 갱신
    • 음수 사이클 검사 : 어떤 정점 i에 대해 d[i][i] <0인 경우가 발생하면 음수 사이클 존재

 

📊 플로이드 워셜 동작 예시 (표 형태)

정점: A, B, C
간선 목록:

  • A → B (4)
  • A → C (2)
  • C → B (1)

💡 시작 정점 개념 없이 모든 정점 쌍에 대한 최단 거리를 구함


⏱ 초기 거리 행렬 (D₀)

ABC
A 0 4 2
B 0
C 1 0

간선 없는 곳은 ∞ (무한), 자기 자신은 0


🔁 1단계: 경유지 A 사용 (k = A)

ABC비고
A 0 4 2 그대로
B 0 변화 없음
C 1 0 변화 없음

🔁 2단계: 경유지 B 사용 (k = B)

ABC비고
A 0 4 2 그대로
B 0 그대로
C 1 0 그대로

변화 없음


🔁 3단계: 경유지 C 사용 (k = C)

ABC비고
A 0 3 2 A → C → B = 2 + 1 = 3 으로 갱신됨
B 0 변화 없음
C 1 0 그대로

✅ 최종 거리 행렬

ABC
A 0 3 2
B 0
C 1 0

🔍 음수 사이클 검사

  • d[i][i] < 0인 정점이 없음 → 음수 사이클 없음

 

import java.util.Scanner;

public class 그래프최소비용07_플로이드워셜 {
	static String input1 = "5 9\r\n"
			+ "0 1 3\r\n"
			+ "0 2 8\r\n"
			+ "0 4 -4\r\n"
			+ "1 3 1\r\n"
			+ "1 4 7\r\n"
			+ "2 1 4\r\n"
			+ "3 0 2\r\n"
			+ "3 2 -5\r\n"
			+ "4 3 6\r\n"
			+ "\r\n";
	static String input2 = "4 5\r\n"
			+ "0 1 1\r\n"
			+ "0 3 4\r\n"
			+ "1 2 1\r\n"
			+ "2 0 -3\r\n"
			+ "3 2 2";
	static final int INF = Integer.MAX_VALUE;
	public static void main(String[] args) {
		Scanner sc = new Scanner(input1);
		int V = sc.nextInt(); //정점의 개수 (시작 정점의 번호 확인)
		int E = sc.nextInt(); //간선의 개수
		int[][] dist = new int[V][V]; //0번부터 하더라..
		//i==j 어차피 0이야
		for (int i = 0; i<V; i++) {
			for (int j = 0; j<V; j++) {
				if(i!=j) dist[i][j] = INF; // dist 다시 호출 안 할꺼면 이렇게 해두 돼
			}
		}//dist 초기값 설정
		for (int i = 0; i<E; i++) {
			int s = sc.nextInt();
			int e = sc.nextInt();
			int cost = sc.nextInt();
			dist[s][e] = cost; //무향이면 반대로도 해야하지만 지금은 유향이니깐
		}
		//플로이드 워셜
		for (int k = 0; k<V; k++) {
			for (int i = 0; i<V; i++) {
				if (dist[i][k]==INF) continue; //i의 출발지에서 경유지 k까지 거리가 무한대!
				for (int j = 0; j<V; j++) {
					if (dist[k][j]==INF) continue; //경유지 k로부터 도착지 j까지 무한대라면 할 필요 없음
					dist[i][j] = Math.min(dist[i][j], dist[i][k]+dist[k][j]);
				}//도착점
			}//출발점
		}//경유지
		
		//옵션)음수사이클 검사
		boolean negativeCycle = false;
		for (int i = 0; i<V; i++) {
			if (dist[i][i]<0) {
				negativeCycle = true;
				break;
			}
		}
		if (negativeCycle) System.out.println("음의 사이클 존재");
		else {
			for (int i = 0; i<V; i++) {
				for (int j = 0; j<V; j++) {
					System.out.printf("%4d",dist[i][j]);
				}
				System.out.println();
			}
		}
		
	}

}

 

 

 

 

 


📊 3. 최단 경로 알고리즘 비교 분석

구분 다익스트라(Dijkstra) 벨만-포드 (Bellman-Ford) 플로이드-워셜 (Floyd-Warshall)
문제 유형 단일 출발점 최단 경로 단일 출발점 최단 경로 모든 정점 쌍 최단 경로
음수 가중치 허용 ❌ 허용 (음수 사이클은 X) 허용 (음수 사이클은 X)
음수 사이클 검출 ❌ 검출 가능 검출 가능
시간 복잡도 O(E log V) O(VE) O(V³)
장점 음수가 없을 경우 빠름 음수 가중치 처리 가능 모든 정점 쌍의 최단 경로 계산 가능
단점 음수 가중치 처리 불가 다익스트라보다 느림 정점 수가 많을 경우 비효율적