그리디 알고리즘은 DP 사용 시 지나치게 많은 일을 한다는 것에서 착안하여 고안된 알고리즘이다. Greedy는 ‘탐욕스러운, 욕심 많은’ 이란 뜻으로 Greedy 알고리즘은 말 그 대로 여러 경우 중 하나를 결정해야 할 때마다 그 순간에 최적이라고 생각되는 것을 선택해 나가는 방식으로 진행하여 최종적인 해답에 도달하는 방법이다.
탐욕 알고리즘이 잘 작동하는 문제는 대부분 탐욕스런 선택 조건(greedy choice property)과 최적 부분 구조 조건(optimal substructure)이라는 두 가지 조건을 만족된다. 아래 조건이 만족되는 경우 탐욕 알고리즘은 최적해를 구하지 못한다. 하지만, 이러한 경우에도 탐욕 알고리즘은 근사 알고리즘으로 사용이 가능할 수 있으며, 대부분의 경우 계산 속도가 빠르기 때문에 실용적으로 사용할 수 있다. 이 경우 역시 어느 정도까지 최적해에 가까운 해를 구할 수 있는지를 보장하려면 엄밀한 증명이 필요하다.
MST를 만들기위해 이용되는 프림 알고리즘(O(V^2))과 크루스칼 알고리즘(O(E log V))은 대표적인 그리디 알고리즘이다. (MST란 Minimum Spanning Tree의 약자로, 그래프에 있는 모든 정점을 포함하면서 가중치 총 합이 가장 작은 트리를 말한다.)
위 그래프의 MST는 다음과 같다.
프림 알고리즘은 트리를 유지하면서 가중치가 낮은 간선을 선택해가는 방법이다.
다음은 프림 알고리즘을 구현한 코드이다. 백준 1197번: 최소 스패닝 트리의 답이기도하다.
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
int V, E, a, b, c;
bool visit[10001];
vector<pair<int, int>> edge[10001];
int prim(){
int ans = 0;
priority_queue<pair<int, int>> pq;
pq.push({0, 1}); // 1번 정점부터 시작
while (!pq.empty()){
int cost = -pq.top().first;
int node = pq.top().second;
pq.pop();
if(visit[node]) continue;
visit[node] = true;
ans+=cost;
for(int i=0; i<edge[node].size(); i++){
cost = edge[node][i].first;
if(visit[edge[node][i].second]) continue;
pq.push({edge[node][i]}); // 현재 정점에서 이동 가능한 방문하지 않은 정점
}
}
return ans;
}
int main(){
ios_base::sync_with_stdio(0); cin.tie(0);
cin >> V >> E;
for(int i=0; i<E; i++){
cin >> a >> b >> c;
edge[a].push_back({-c, b}); // max heap to min heap
edge[b].push_back({-c, a});
}
cout << prim();
}
크루스칼 알고리즘은 트리를 유지하며 만들어가는 것이 아니라, 트리를 마구 생성해서 잇는 기법이다. 따라서 임의의 정점을 선택했을 때, 그 정점이 어느 집합의 원소인지 알아내는 연산이 추가로 필요하다.
다음은 union-find를 이용하여 크루스칼 알고리즘을 구현한 코드이다. 백준 1197번: 최소 스패닝 트리의 답이기도하다.
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
int V, E, a, b, c, root[10001]{};
struct edge {int node1, node2, weight;}; // edge 구조체
bool compare(edge u, edge v) {return u.weight < v.weight;} // weight로 비교하여 오름차순 정렬
vector<edge> eg; //edge type 벡터
int find(int x){ // root를 찾음
if(root[x]==x) return x;
return root[x] = find(root[x]); // 경로 단축
}
void union_(int x, int y){ // 정점 연결
root[find(x)] = find(y);
}
int main(){
ios_base::sync_with_stdio(0); cin.tie(0);
cin>>V>>E;
for(int i=1; i<=V; i++) root[i] = i; // union-find를 위한 초기화
for(int i=0; i<E; i++){
cin>>a>>b>>c;
if(a!=b) eg.push_back({a,b,c});
}
sort(eg.begin(), eg.end(), compare); // 정렬
int ans = 0;
for(int i=0; i<eg.size(); i++){
if(find(eg[i].node1)!=find(eg[i].node2)) { // 연결되지 않은 경우
union_(eg[i].node1, eg[i].node2); // 연결
ans+=eg[i].weight; // 연결된 weight를 답에 더해줌
}
}
cout << ans;
}