공부/Algorithm

Dijkstra algorithm(다익스트라 알고리즘) 구현

sudo 2021. 8. 11. 19:37
// Dijkstra.h
#pragma once

#include <queue>
#include "List.h"
#include "Heap.h"
#include <assert.h>

template <typename T>
class CEdge
{
	template <typename T>
	friend class CDijkstraNode;

	template <typename T>
	friend class CDijkstra;

private:
	CEdge()
	{
		m_Node = nullptr;
		m_Cost = 0;
	}

	~CEdge()
	{

	}

private:
	CDijkstraNode<T>* m_Node;
	int m_Cost;
};

template <typename T>
class CDijkstraNode
{
	template <typename T>
	friend class CDijkstra;

private:
	CDijkstraNode()
	{
		m_Size = 0;
		m_Capacity = 3;
		m_Visit = false;

		m_Cost = INT_MAX;
		m_Parent = nullptr;

		m_EdgeArray = new CEdge<T>*[m_Capacity];
	}

	~CDijkstraNode()
	{
		for (int i = 0; i < m_Size; ++i)
		{
			delete	m_EdgeArray[i];
		}

		delete[]	m_EdgeArray;
	}

private:
	T m_Data;
	// 이 노드에 연결된 Edge 포인터 배열
	CEdge<T>** m_EdgeArray;
	// 나중에 도착노드에서 출발해서 각 노드의 m_Parent를 타고
	// 출발지점까지 가는게 최단 경로가 되도록 하기 위해 선언한 멤버
	CDijkstraNode<T>* m_Parent;
	// 나와 연결된 엣지 개수( = 연결된 노드 개수)
	int m_Size;
	int m_Capacity;
	bool m_Visit;
	int m_Cost;

private:
	// 나와 인자로 들어온 Node를 이어주는 edge를 만들고
	// 두개를 서로 이어준다
	void ConnectNode(CDijkstraNode<T>* Node, int Cost)
	{
		if (!Node)
		{
			assert(Node != nullptr);
		}

		if (m_Size == m_Capacity)
		{
			m_Capacity *= 2;

			CEdge<T>** tmp = new CEdge<T>*[m_Capacity];
			memcpy(tmp, m_EdgeArray, sizeof(CEdge<T>*) * m_Size);

			delete[] m_EdgeArray;

			m_EdgeArray = tmp;
		}

		CEdge<T>* NewEdge = new CEdge<T>;
		
		m_EdgeArray[m_Size] = NewEdge;

		NewEdge->m_Cost = Cost;
		NewEdge->m_Node = Node;

		++m_Size;
	}

};

template <typename T>
class CDijkstra
{

public:
	CDijkstra()
	{
		m_Size = 0;
		m_Capacity = 10;

		m_NodeArray = new CDijkstraNode<T>*[m_Capacity];
	}

	~CDijkstra()
	{
		for (int i = 0; i < m_Size; ++i)
		{
			delete m_NodeArray[i];
		}

		delete[] m_NodeArray;
	}

private:
	int m_Size;
	int m_Capacity;
	CDijkstraNode<T>** m_NodeArray;

public:
	void insert(const T& Data)
	{
		if (m_Size == m_Capacity)
		{
			m_Capacity *= 2;

			CDijkstraNode<T>** Array = new CDijkstraNode<T>*[m_Capacity];

			memcpy(Array, m_NodeArray, sizeof(CDijkstraNode<T>*) * m_Size);

			delete[]	m_NodeArray;

			m_NodeArray = Array;
		}

		CDijkstraNode<T>* NewNode = new CDijkstraNode<T>;

		NewNode->m_Data = Data;

		m_NodeArray[m_Size] = NewNode;

		++m_Size;
	}

	void AddEdge(const T& Src, const T& Dest, int Cost)
	{
		CDijkstraNode<T>* SrcNode = FindNode(Src);
		CDijkstraNode<T>* DestNode = FindNode(Dest);

		SrcNode->ConnectNode(DestNode, Cost);
		DestNode->ConnectNode(SrcNode, Cost);
	}

	bool FindRoute(const T& Src, const T& Dest, CList<T>* Result)
	{
		CDijkstraNode<T>* SrcNode = FindNode(Src);

		if (!SrcNode)
			return false;

		CDijkstraNode<T>* DestNode = FindNode(Dest);

		if (!DestNode)
			return false;

		SrcNode->m_Visit = true;
		SrcNode->m_Cost = 0;

		CHeapsort<CDijkstraNode<T>*> heap;
		heap.SetSortFunction(SortNode);

		heap.PushData(SrcNode);

		while (!heap.empty())
		{
			int Size = heap.GetSize();
			heap.Heapsort();
			// Heapsort 호출하고 나면 m_Size = 0이 돼서 다시 원래 size로 복귀시켜주는 코드
			heap.SetSize(Size);

			CDijkstraNode<T>* SearchNode = heap.GetTop();
			std::cout << SearchNode->m_Data << std::endl;
			heap.Pop();
			
			for (int i = 0; i < SearchNode->m_Size; ++i)
			{
				if (SearchNode->m_EdgeArray[i]->m_Node->m_Cost > SearchNode->m_Cost + SearchNode->m_EdgeArray[i]->m_Cost)
				{
					SearchNode->m_EdgeArray[i]->m_Node->m_Cost = SearchNode->m_Cost + SearchNode->m_EdgeArray[i]->m_Cost;
					SearchNode->m_EdgeArray[i]->m_Node->m_Parent = SearchNode;
				}

				if (SearchNode->m_EdgeArray[i]->m_Node->m_Visit == false)
				{
					heap.PushData(SearchNode->m_EdgeArray[i]->m_Node);

					heap.MaxHeapify(1);

					SearchNode->m_EdgeArray[i]->m_Node->m_Visit = true;
				}
			}
		}

		CDijkstraNode<T>* Node = DestNode;

		while (Node)
		{
			Result->push_front(Node->m_Data);
			Node = Node->m_Parent;
		}

		return true;
	}


private:
	CDijkstraNode<T>* FindNode(const T& data)
	{
		for (int i = 0; i < m_Size; ++i)
		{
			if (m_NodeArray[i]->m_Data == data)
				return m_NodeArray[i];
		}

		// 여기에 도달한다는 것은 못찾았다는 의미
		assert(false);
	}

	static bool SortNode(CDijkstraNode<T>* const& Left, CDijkstraNode<T>* const& Right)
	{
		return Left->m_Cost > Right->m_Cost;
	}
};

 

// Heap.h


#pragma once

#include <cmath>
#include <assert.h>


template <typename T>
class CHeapsort
{
public:
	CHeapsort()
	{
		m_Capacity = 5;
		m_Size = 0;
		//std::cout << typeid(m_Data).name() << std::endl;
		m_Data = new T[m_Capacity];

	}
	~CHeapsort()
	{
		delete[] m_Data;
	}

private:
	T* m_Data;
	int m_Size;
	int m_Capacity;
	bool (*m_Func)(const T&, const T&);

public:
	void MaxHeapify(int Index)
	{
		int Left = Index * 2;
		int Right = Index * 2 + 1;
		int LargeIndex = Index;


		if (Left <= m_Size && m_Func(m_Data[Left], m_Data[LargeIndex]))
		{
			LargeIndex = Left;
		}

		if (Right <= m_Size && m_Func(m_Data[Right], m_Data[LargeIndex]))
		{
			LargeIndex = Right;
		}

		if (LargeIndex != Index)
		{
			T tmp = m_Data[LargeIndex];
			m_Data[LargeIndex] = m_Data[Index];
			m_Data[Index] = tmp;

			MaxHeapify(LargeIndex);
		}
	}
private:
	void BuildMaxHeap()
	{
		for (int i = floor(m_Size / 2); i > 0; --i)
		{
			MaxHeapify(i);
		}
	}

public:
	// heap에 데이터를 넣기만 하는 함수(정렬은 Heapsort 호출해야함)
	// 인덱스 1부터 넣는다
	void PushData(const T& Data)
	{
		if (m_Size + 1 == m_Capacity)
		{
			m_Capacity *= 2;

			T* tmp = new T[m_Capacity];
			memcpy(tmp, m_Data, sizeof(T) * m_Capacity);

			delete[] m_Data;

			m_Data = tmp;
		}

		++m_Size;
		m_Data[m_Size] = Data;
	}

	void Heapsort()
	{
		BuildMaxHeap();

		for (int i = m_Size; i > 1; --i)
		{
			T tmp = m_Data[i];
			m_Data[i] = m_Data[1];
			m_Data[1] = tmp;

			--m_Size;

			MaxHeapify(1);
		}
	}

	void Pop()
	{
		if (empty())
			assert(false);

		else if (m_Size == 1)
		{
			m_Size = 0;
			return;
		}

		// 가장 마지막에 추가된 노드를 루트로 올린다.
		m_Data[1] = m_Data[m_Size];
		--m_Size;

		MaxHeapify(1);
	}

	int GetSize() const
	{
		return m_Size;
	}

	void SetSize(int Size)
	{
		m_Size = Size;
	}

	int GetCapacity() const
	{
		return m_Capacity;
	}

	T GetTop() const
	{
		return m_Data[1];
	}

	T* GetData() const
	{
		return m_Data;
	}

	T GetData(int Index) const
	{
		return m_Data[Index];
	}

	bool empty() const
	{
		return m_Size == 0;
	}

	void SetSortFunction(bool (*pFunc)(const T&, const T&))
	{
		m_Func = pFunc;
	}
};

 

// List.h

#pragma once

#include <assert.h>

// friend : 아래처럼 Listnode에 List를 friend로 지정하면 List에서는 ListNode의
// private에 접근할 수 있다.

template <typename T>
class CListNode
{
	template <typename T>
	friend class CList;

	template <typename T>
	friend class CListIterator;

	template <typename T>
	friend class CListReverseIterator;

private:
	CListNode() :
		m_Next(nullptr),
		m_Prev(nullptr)
	{
	}

	~CListNode()
	{
	}

private:
	T		m_Data;
	CListNode<T>* m_Next;
	CListNode<T>* m_Prev;
};

/*
iterator : 노드를 순차적으로 방문하기 위해서  iterator를 만들어서 제공한다.
iterator를 이용해서 앞에서 부터 혹은 뒤에서부터 노드를 순차적으로 접근할 수 있게 해준다.
*/
template <typename T>
class CListIterator
{
	template <typename T>
	friend class CList;

public:
	CListIterator() :
		m_Node(nullptr)
	{
	}

	~CListIterator()
	{
	}

private:
	CListNode<T>* m_Node;

public:
	// iterator끼리 서로 가지고 있는 노드가 같을 경우 같다고 판단한다.
	bool operator == (const CListIterator<T>& iter)	const
	{
		return m_Node == iter.m_Node;
	}

	bool operator != (const CListIterator<T>& iter)	const
	{
		return m_Node != iter.m_Node;
	}

	bool operator == (const CListNode<T>* Node)	const
	{
		return m_Node == Node;
	}

	bool operator != (const CListNode<T>* Node)	const
	{
		return m_Node != Node;
	}

	void operator ++ ()
	{
		m_Node = m_Node->m_Next;
	}

	void operator ++ (int)
	{
		m_Node = m_Node->m_Next;
	}

	void operator -- ()
	{
		m_Node = m_Node->m_Prev;
	}

	void operator -- (int)
	{
		m_Node = m_Node->m_Prev;
	}

	T& operator * ()	const
	{
		return m_Node->m_Data;
	}
};

// 숙제 : reverseiterator 클래스를 만들어오기.
// reverseiterator 는 역방향으로 진행하는 iterator이다.

template <typename T>
class CListReverseIterator
{
	template <typename T>
	friend class CList;

public:
	CListReverseIterator() :
		m_Node(nullptr)
	{
	}

	~CListReverseIterator()
	{
	}

private:
	CListNode<T>* m_Node;

public:
	// iterator끼리 서로 가지고 있는 노드가 같을 경우 같다고 판단한다.
	bool operator == (const CListReverseIterator<T>& iter)	const
	{
		return m_Node == iter.m_Node;
	}

	bool operator != (const CListReverseIterator<T>& iter)	const
	{
		return m_Node != iter.m_Node;
	}

	bool operator == (const CListNode<T>* Node)	const
	{
		return m_Node == Node;
	}

	bool operator != (const CListNode<T>* Node)	const
	{
		return m_Node != Node;
	}

	void operator ++ ()
	{
		m_Node = m_Node->m_Prev;
	}

	void operator ++ (int)
	{
		m_Node = m_Node->m_Prev;
	}

	void operator -- ()
	{
		m_Node = m_Node->m_Next;
	}

	void operator -- (int)
	{
		m_Node = m_Node->m_Next;
	}

	T& operator * ()	const
	{
		return m_Node->m_Data;
	}
};


template <typename T>
class CList
{
public:
	CList()
	{
		m_Size = 0;

		// Begin과 End노드를 생성하고 두 노드를 서로 연결한다.
		m_Begin = new NODE;
		m_End = new NODE;

		m_Begin->m_Next = m_End;
		m_End->m_Prev = m_Begin;
	}

	~CList()
	{
		// Begin노드는 이전노드가 nullptr이다.
		// End노드는 다음노드가 nullptr이다.
		PNODE	DeleteNode = m_Begin;

		while (DeleteNode)
		{
			PNODE	Next = DeleteNode->m_Next;

			delete	DeleteNode;

			DeleteNode = Next;
		}
	}

private:
	typedef CListNode<T>	NODE;
	typedef CListNode<T>* PNODE;

public:
	typedef CListIterator<T>	iterator;
	typedef CListReverseIterator<T>	reverse_iterator;

private:
	// Begin과 End는 데이터를 저장하기 위한 노드는 아니다.
	// 명시적으로 시작과 끝을 의미하는 노드로 사용하기 위해 할당해두고 사용한다.
	// 실제 데이터를 저장하는 노드는 Begin과 End노드 사이에 위치하게 될것이다.
	PNODE	m_Begin;
	PNODE	m_End;
	int		m_Size;

public:
	void push_back(const T& data)
	{
		// 데이터를 저장해둘 노드를 생성한다.
		PNODE	Node = new NODE;

		Node->m_Data = data;

		// End노드와 End노드의 이전노드 사이에 새로 생성된 노드를 추가해주도록 한다.
		PNODE	Prev = m_End->m_Prev;

		// End노드의 이전노드의 다음노드를  End에서 새로 생성된 노드로 교체한다.
		Prev->m_Next = Node;

		// 새로 생성된 노드의 이전노드를 End의 이전노드로 지정한다.
		Node->m_Prev = Prev;

		// 새로 생성된 노드의 다음 노드를 End노드르 지정한다.
		Node->m_Next = m_End;

		// End노드의 이전노드를 새로 생성한 노드로 지정한다.
		m_End->m_Prev = Node;

		++m_Size;
	}

	void push_front(const T& data)
	{
		PNODE	Node = new NODE;

		Node->m_Data = data;

		// Begin노드와 Begin노드의 다음노드 사이에 새로 생성된 노드를 추가한다.
		PNODE	Next = m_Begin->m_Next;

		// 새로 생성된 노드의 다음노드로 Begin노드의 다음노드를 지정한다.
		Node->m_Next = Next;

		// Begin노드의 다음노드의 이전노드를 새로 생성된 노드로 지정한다.
		Next->m_Prev = Node;

		// Begin노드의 다음노드로 새로 생성된 노드를 지정한다.
		m_Begin->m_Next = Node;

		// 새로 생성된 노드의 이전노드로 Begin노드를 지정한다.
		Node->m_Prev = m_Begin;

		++m_Size;
	}

	// 마지막 노드를 제거한다.
	void pop_back()
	{
		if (empty())
		{
			assert(false);
		}

		// End노드의 이전 노드를 지워준다. 그러므로 End의 이전노드를 얻어오고
		// End의 이전 노드의 이전노드를 얻어와서 End와 연결해준다.
		PNODE	DeleteNode = m_End->m_Prev;

		PNODE	Prev = DeleteNode->m_Prev;

		// 지울노드의 이전노드의 다음노드를 End노드로 지정한다.
		Prev->m_Next = m_End;

		// End노드의 이전노드로 지울노드의 이전노드를 지정한다.
		m_End->m_Prev = Prev;

		// 노드를 제거한다.
		delete	DeleteNode;

		--m_Size;
	}

	void pop_front()
	{
		if (empty())
		{
			assert(false);
		}

		// Begin노드의 다음 노드를 지워준다.
		PNODE	DeleteNode = m_Begin->m_Next;

		// 지울 노드의 다음 노드를 얻어온다.
		PNODE	Next = DeleteNode->m_Next;

		// Begin노드의 다음노드로 지울 노드의 다음노드를 지정한다.
		m_Begin->m_Next = Next;

		// 지울노드의 다음노드의 이전노드로 Begin노드를 지정한다.
		Next->m_Prev = m_Begin;

		// 노드를 제거한다.
		delete	DeleteNode;

		--m_Size;
	}

	const T& front()	const
	{
		if (empty())
			assert(false);

		return m_Begin->m_Next->m_Data;
	}

	const T& back()	const
	{
		if (empty())
			assert(false);

		return m_End->m_Prev->m_Data;
	}


	// 클래스의 멤버함수는 함수의 뒤에 const를 붙일 수 있다.
	// 멤버함수의 뒤에 const가 붙으면 이 함수에서는 멤버변수의 값을 변경할 수 없게 된다.
	// const 객체가 만들어진 경우라면 뒤에 const가 붙은 멤버함수만 호출이 가능하다.
	int size()	const
	{
		return m_Size;
	}

	bool empty()	const
	{
		return m_Size == 0;
	}

	// 추가된 모든 노드를 제거한다.
	void clear()
	{
		if (empty())
			return;

		PNODE	DeleteNode = m_Begin->m_Next;

		// End노드가 아니라면 while을 돌면서 제거해주도록 한다.
		while (DeleteNode != m_End)
		{
			// 지울 노드의 다음노드를 미리 받아둬야 한다.
			// 왜냐하면 노드를 먼저 지워버린다면 다음노드를 얻어올 수 없기 때문이다.
			PNODE	Next = DeleteNode->m_Next;

			// 노드를 제거한다.
			delete	DeleteNode;

			// 지울 노드를 다음 노드로 갱신한다.
			DeleteNode = Next;
		}

		// Begin노드와 End노드를 서로 연결해준다.
		m_Begin->m_Next = m_End;
		m_End->m_Prev = m_Begin;

		// 노드의 개수를 0으로 초기화해준다.
		m_Size = 0;
	}

	iterator begin()	const
	{
		iterator	iter;
		iter.m_Node = m_Begin->m_Next;
		return iter;
	}

	iterator end()	const
	{
		iterator	iter;
		iter.m_Node = m_End;
		return iter;
	}

	reverse_iterator rbegin()	const
	{
		reverse_iterator	iter;
		iter.m_Node = m_End->m_Prev;
		return iter;
	}

	reverse_iterator rend()	const
	{
		reverse_iterator	iter;
		iter.m_Node = m_Begin;
		return iter;
	}

	void operator = (const CList<T>& list)
	{
		// 기존 정보를 제거한다.
		clear();

		m_Size = list.m_Size;

		PNODE	Node = list.m_Begin->m_Next;

		while (Node != list.m_End)
		{
			push_back(Node->m_Data);
			Node = Node->m_Next;
		}
	}

	// 삭제한 노드의 다음 노드를 가지고 있는 iterator를 반환해준다.
	iterator erase(const T& data)
	{
		iterator	iter = Find(data);

		if (iter == end())
			return iter;

		return erase(iter);
	}

	iterator erase(const iterator& iter)
	{
		if (iter == end() || iter == m_Begin)
			return end();

		// 지울 노드의 이전노드와 다음노드를 얻어온다.
		// 지울 노드의 이전노드와 다음노드를 서로 연결시켜준다.
		PNODE	Prev = iter.m_Node->m_Prev;
		PNODE	Next = iter.m_Node->m_Next;

		Prev->m_Next = Next;
		Next->m_Prev = Prev;

		delete	iter.m_Node;

		--m_Size;

		iterator	result;
		result.m_Node = Next;

		return result;
	}

	iterator Find(const T& data)
	{
		iterator	iter = begin();
		iterator	iterEnd = end();

		for (; iter != iterEnd; ++iter)
		{
			if (*iter == data)
				return iter;
		}

		// 못찾을 경우 end를 리턴한다.
		return iterEnd;
	}

	// 정렬
	void sort(bool (*pFunc)(const T&, const T&))
	{
		iterator	iter1 = begin();
		iterator	iter1End = end();
		--iter1End;

		iterator	iter2;
		iterator	iter2End = end();

		// 예를 들어 10개라면 0 ~ 8 까지만 반복한다.
		// 마지막 9번은 반복을 안한다.
		for (; iter1 != iter1End; ++iter1)
		{
			iter2 = iter1;
			++iter2;
			for (; iter2 != iter2End; ++iter2)
			{
				if (pFunc(*iter1, *iter2))
				{
					T	data = *iter1;
					*iter1 = *iter2;
					*iter2 = data;
				}
			}
		}
	}
};

 

// main.cpp

#define _CRTDBG_MAP_ALLOC

#include <iostream>
#include "Dijkstra.h"
#include <crtdbg.h>

int main()
{
	_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
	//_CrtSetBreakAlloc(157);

	CDijkstra<const char*>	dijkstra;

	dijkstra.insert("1");
	dijkstra.insert("2");
	dijkstra.insert("3");
	dijkstra.insert("4");
	dijkstra.insert("5");
	dijkstra.insert("6");
	dijkstra.insert("7");

	dijkstra.AddEdge("1", "2", 2);
	dijkstra.AddEdge("1", "6", 3);

	dijkstra.AddEdge("2", "3", 20);
	dijkstra.AddEdge("2", "5", 9);

	dijkstra.AddEdge("3", "5", 6);
	dijkstra.AddEdge("3", "4", 4);

	dijkstra.AddEdge("4", "7", 1);

	dijkstra.AddEdge("5", "6", 7);
	dijkstra.AddEdge("5", "7", 6);

	CList<const char*>	FindList;

	if (dijkstra.FindRoute("1", "4", &FindList))
	{
		CList<const char*>::iterator	iter;

		for (iter = FindList.begin(); iter != FindList.end(); ++iter)
		{
			std::cout << *iter << " -> ";
		}

		std::cout << std::endl;
	}

	else
	{
		std::cout << "길이 없다" << std::endl;
	}

	return 0;
}

 

main.cpp 출력 결과

이전에 구현했던 heap과, list를 이용해서 다익스트라 알고리즘을 구현해보았다.

 

최소힙을 이용한 다익스트라 알고리즘의 시간복잡도는 O(ElogV)이다(아래에 구현된 다익스트라를 보고 이해해보자).  

  1. while문도 결국 for문안에 if에서 pq에 push하는 만큼 돌아가는데 push하는 횟수는 최대 E 만큼이므로 결국 O(E) 만큼 while문을 돌 것이다(for문안에 if문 조건을 만족하는 횟수만큼 while문을 반복하므로, nested loop 개념으로 두 반복문 반복 횟수를 단순히 곱하면 안된다).
  2. while문안에서 heap에서 top, push를 하는데 top은 O(1)에 얻을 수 있고, push/pop은 O(logE)만큼 걸린다.
  3. 2번의 동작을 while문을 돌면서 진행하면 O(E) * ( O(1) + O(logE) ) = O(E) + O(ElogE) = O(ElogE)가 된다.

reference에 적힌 블로그에 다른 분이 구현하신 다익스트라를 보면 시간복잡도가 훨씬 잘 와닿는다

  int V;
  // 그래프의 인접 리스트. (연결된 정점 번호, 간선 가중치) 쌍을 담는다.
  vector<pair<int, int> > adj[MAX_V];

  vector<int> dijkstra(int src) {
    vector<int> dist(V, INF);
    dist[src] = 0;
    priority_queue<pair<int, int> > pq;
    pq.push(make_pair(0, src));

    while (!pq.empty()) {
      int cost = -pq.top().first;
      int here = pq.top().second;
      pq.pop();

      // 만약 지금 꺼낸 것보다 더 짧은 경로를 알고 있다면 지금 꺼낸 것을 무시한다.
      if (dist[here] < cost) continue;

      // 인접한 정점들을 모두 검사한다.
      for (int i = 0; i < adj[here].size(); ++i) {
        int there = adj[here][i].first;
        int nextDist = cost + adj[here][i].second;
        // 더 짧은 경로를 발견하면, dist[]를 갱신하고 우선순위 큐에 넣는다.
        if (dist[there] > nextDist) {
          dist[there] = nextDist;
          pq.push(make_pair(-nextDist, there));
        }
      }
    }

    return dist;
  }

 

Reference

https://sungjk.github.io/2016/05/13/Dijkstra.html

 

다익스트라 알고리즘(Dijkstra)

최단 경로 알고리즘인 다익스트라(Dijkstra) 알고리즘에 대한 설명입니다.

sungjk.github.io