본 포스팅에서는 벨만포드 알고리즘을 이용한 최소비용 경로탐색 알고리즘에 관한 개인적인 정리에 대한 내용을 담고있습니다.
이 글은 어디까지나 저의 개인적인 노트, 정리같은내용이기 때문에 이론들을 깊게 설명하는것은 하지 않겠습니다.
프로그래밍언어는 c++ , c# 사용하겠습니다.
2021-08-04기준 Windows10 Home 사용했으며, 컴파일러는 MSVC, GCC를 사용했습니다.
c++버전은 17사용했습니다.
c#버전은 .Net6.0이랑 .Net4.72사용했습니다.
벨만-포드 알고리즘은 어떠한 그래프가 있을때, 시작지점에서 도착지점까지 이르는데 필요한 최소비용을 구하는 알고리즘입니다.
*V: 간선의 개수
우선 이번에 만들어볼 알고리즘의 전체적인 시간복잡도는 O(M + N) + O(VE)입니다.
minV함수와 출력부분을 제외하고, 순수 알고리즘 부분은 O(VE)입니다.
아래사진은 이번내용의 알고리즘을 적용해볼 예시용 그래프입니다.
참고로 단방향그래프입니다, 화살표를 그리는것을 까먹었습니다.
하지만 다 작은정점에서 큰정점으로 가는 방향입니다.
C++17 (소스코드가 길어보이는것은 착시입니다. 주석때문입니다.)
#include <bits/stdc++.h>
// INF: 무한대수정의
constexpr int INF = 987654321;
// a: 현재지점, b: 다음지점, cost: 거리에 대한 비용
struct edge
{
int a, b, cost;
};
// e: 연결된 간선과 비용을 저장하는 배열
std::vector<edge> e;
// n: 정점의 개수, m: 간선의 개수, v: 출발점, t: 도착점
int n, m, v, t;
// minV: 만약 출발점이 0이 아닐경우에 대비하여 만든 함수
// *만약 출발점이 1이고, 도착점이 6인데 n이 6이면 오버플로우 발생하기때문이다.
// *위의 예시에 대한 경우를 안정화시키기 위한 함수
int minV() {
int result = INF;
for (std::size_t i = 0; i < e.size(); ++i) {
result = std::min(result, std::min(e[i].a, e[i].b));
}
result = std::max(v, result);
return result;
}
void BellmanFord()
{
// 이 부분은 굳이 안넣어주셔도됩니다.
// 넣어준이유는 출발점이 0이아닐경우에 대비해서 넣은겁니다.
n += minV();
// visit: 방문여부확인 & 해당 정점에대한 비용저장용 변수
std::vector<int> visit(n, INF);
// 시작지점은 이동비용이 0입니다.
visit[v] = 0;
// p: 경로탐색에 필요한 경로저장용 배열
std::vector<int> p(n, -1);
// 이 부분은 평범한 벨만-포드 알고리즘 구현부분입니다.
// 모르시는 분들은 그래프이론공부하고 오시는것을 추천드립니다.
while (true)
{
bool flag = false;
for (int j = 0; j < m; ++j)
if (visit[e[j].a] < INF)
if (visit[e[j].b] > visit[e[j].a] + e[j].cost)
{
visit[e[j].b] = visit[e[j].a] + e[j].cost;
p[e[j].b] = e[j].a;
flag = true;
}
if (!flag) break;
}
// 조건분기[TRUE]: 도착지점까지 갈수없는 그래프
if (visit[t] == INF)
std::cout << "이 그래프에서 최소비용경로는 존재하지 않습니다.";
// 조건분기[FALSE]: 도착지점까지 갈수있는 그래프
else
{
// path: 최소비용에 대한 탐색된 경로가 저장된 배열
std::vector<int> path;
// -1이 나올때까지 거꾸로 순회하면서 그때의 정점을 push한다.
for (int cur = t; cur != -1; cur = p[cur])
path.push_back(cur);
std::reverse(path.begin(), path.end());
std::cout << "최소비용은 " << visit[t] << "입니다." << '\n';
std::cout << v << "에서 " << t << "까지의 최소비용경로: ";
for (std::size_t i = 0; i < path.size(); ++i)
std::cout << path[i] << ' ';
}
}
// makeGraph: 간선만드는 함수
void makeGraph(int start, int end, int cost) {
e.push_back({ start, end, cost });
}
int main() {
n = 6; //정점의 개수
m = 8; //간선의 개수
v = 1; //출발점
t = 6; //도착점
makeGraph(1, 2, 2);
makeGraph(1, 4, 3);
makeGraph(1, 3, 1);
makeGraph(2, 3, 4);
makeGraph(3, 4, 1);
makeGraph(3, 5, 3);
makeGraph(4, 6, 1);
makeGraph(5, 6, 2);
BellmanFord();
}
C#은 Winform .netframework로 했습니다.
뭔가 시각적으로 보이니까 더 재밌어요!!ㅠㅠㅋㅋ
C# (.Net 6.0)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
namespace ShortestPath
{
public partial class Form1 : Form
{
struct Edge
{
public int a, b, cost;
public Edge(int a, int b, int cost)
{
this.a = a;
this.b = b;
this.cost = cost;
}
}
struct ReturnType
{
public List<int> Path;
public int Cost;
}
private static List<Edge> edges = new List<Edge>();
private static int N, M;
private static int Min(int a, int b)
{
return (a < b) ? a : b;
}
private static int Max(int a, int b)
{
return (a > b) ? a : b;
}
private static int MaxVertax()
{
int result = 0;
for(int i = 0; i < edges.Count; ++i)
{
result = Max(result, Max(edges[i].a, edges[i].b));
}
return result;
}
private static int MinVertax(int s)
{
int result = 987654321;
for (int i = 0; i < edges.Count; ++i)
{
result = Min(result, Min(edges[i].a, edges[i].b));
}
result = Max(result, s);
return result;
}
private static ReturnType Bellman_ford(int start, int end)
{
M = edges.Count;
N = MaxVertax();
N += MinVertax(start);
ReturnType rType = new ReturnType();
List<int> visit = new List<int>();
List<int> p = new List<int>();
for (int i = 0; i < N; ++i)
{
visit.Add(987654321);
p.Add(-1);
}
visit[start] = 0;
while (true)
{
bool flag = false;
for(int i = 0; i < M; ++i)
{
if (visit[edges[i].a] < 987654321)
{
if (visit[edges[i].b] > visit[edges[i].a] + edges[i].cost)
{
visit[edges[i].b] = visit[edges[i].a] + edges[i].cost;
p[edges[i].b] = edges[i].a;
flag = true;
}
}
}
if (!flag) break;
}
List<int> Path = new List<int>();
if (visit[end] != 987654321) {
for (int i = end; i != -1; i = p[i])
{
Path.Add(i);
}
Path.Reverse();
}
rType.Cost = visit[end];
rType.Path = Path;
return rType;
}
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
textBox4.ReadOnly = true;
textBox5.ReadOnly = true;
}
private void button1_Click(object sender, EventArgs e)
{
edges.Add(new Edge(
Convert.ToInt32(textBox1.Text),
Convert.ToInt32(textBox2.Text),
Convert.ToInt32(textBox3.Text)));
textBox1.Text = "";
textBox2.Text = "";
textBox3.Text = "";
}
private void button2_Click(object sender, EventArgs e)
{
var result = Bellman_ford(Convert.ToInt32(textBox6.Text), Convert.ToInt32(textBox7.Text));
if (result.Path.Count != 0)
{
string str = "";
var arr = result.Path.ToArray<int>();
for (int i=0;i<arr.Length;++i)
{
str = str + arr[i].ToString();
if (i != arr.Length - 1) str += " -> ";
}
textBox5.Text = str;
textBox4.Text = result.Cost.ToString();
}
}
private void button3_Click(object sender, EventArgs e)
{
edges.Clear();
textBox1.Text = "";
textBox2.Text = "";
textBox3.Text = "";
textBox4.Text = "";
textBox5.Text = "";
textBox6.Text = "";
textBox7.Text = "";
}
private void textBox1_TextChanged(object sender, EventArgs e) { } // A
private void textBox2_TextChanged(object sender, EventArgs e) { } // B
private void textBox3_TextChanged(object sender, EventArgs e) { } // Cost
private void textBox4_TextChanged(object sender, EventArgs e) { } // 최소비용
private void textBox5_TextChanged(object sender, EventArgs e) { } // 최소비용 경로
private void textBox6_TextChanged(object sender, EventArgs e) { } // Start
private void textBox7_TextChanged(object sender, EventArgs e) { } // End
}
}
C++17
사실 이것말고 개인적으로 다익스트라 알고리즘에도 아래 코드와같이 똑같은 테크닉을 사용했는데 정상적으로 작동했습니다. 신기!!방기!!하네요~
똑같은 테크닉을 다익스트라에다가도 적용시켜본 C++17소스코드
#include <bits/stdc++.h>
constexpr int MAX = 101;
constexpr int INF = 987654321;
std::vector<std::pair<int, int>> adj[MAX];
int cost[MAX];
void dijkstra(int start){
std::vector<int> p(MAX, -1);
std::priority_queue<std::pair<int, int>, std::vector<std::pair<int, int>>, std::greater<std::pair<int, int>>> pq;
cost[start] = 0;
pq.push({ start, 0 });
while(!pq.empty()){
int current = pq.top().first;
int dist = pq.top().second;
pq.pop();
if(cost[current] < dist) continue;
for(std::size_t i=0;i<adj[current].size();++i){
int next = adj[current][i].first;
int ndist = dist + adj[current][i].second;
if(ndist < cost[next]){
cost[next] = ndist;
p[next] = current;
pq.push({ next, ndist });
}
}
}
std::vector<int> path;
for(int i=5;i!=-1;i=p[i]){
path.push_back(i);
}
std::reverse(path.begin(), path.end());
for(auto const& i : path){
std::cout << i << ' ';
}
std::cout << "\n\n";
}
void makeGraph1(int s, int e, int c){
adj[s].push_back({ e, c });
}
void makeGraph2(int s, int e, int c, int rc){
adj[s].push_back({ e, c });
adj[e].push_back({ s, rc });
}
int main(){
int n = 5;
makeGraph1(0, 1, 2);
makeGraph1(0, 2, 4);
makeGraph1(1, 2, 5);
makeGraph1(1, 4, 3);
makeGraph1(0, 3, 6);
makeGraph2(3, 5, 3, 3);
makeGraph2(2, 4, 2, 3);
makeGraph2(2, 3, 2, 1);
makeGraph2(4, 5, 5, 1);
for(int i=0;i<=n;++i) cost[i] = INF;
dijkstra(0);
for(int i=0;i<=n;++i){
std::cout << cost[i] << ' ';
}
}
첫번째줄이 0에서 5까지가는데 드는 최소비용 경로이고,
그다음줄은 0부터 5까지의 정점에 대한 각각의 최소비용입니다.
C# (.Net 6.0)
C# winform은 만들기 귀찮으실것같아서 저가 만들던거 공유하겠습니다.
C# winform 샘플 다운로드
비번은 "naw6"입니다.
벨만-포드 알고리즘은 최소비용을 구하는데 사용하는 알고리즘으로써,
비슷한 최소비용 알고리즘으로는 다익스트라, 플로이드-와샬 알고리즘이 존재합니다.
개인적으로 최소비용을 구하는데에만 연습했었는데, 이번 계기로 최소비용에 대한 경로탐색을 하는 테크닉에 대해서도 공부하게되었습니다.
개인적으로 되게 도움이 많이 되었던 공부였습니다.