Overview
기본적 Greedy Algorithm
최단 작업 우선 스케줄링
- shortest- job- first scheduling
- Example : 평균 대기 시간 최소로 만드는 대기열 순서
#include <vector>
#include <algorithm>
#include <numeric>
template<typename T>
auto compute_wating_times(std::vector<T>& service_items)
{
std::vector<T>W(service_times.size());
W[0] = 0;
for (auto i = ; i < service_times.size(); i++)
W[i] = W[i - 1] + service_times[i - 1];
return W
}
분할 가능 배낭 문제
- fractional knapsack problem
- 0-1 knapsack problem은 NP-완전 문제로 다항 시간 솔루션 사용 불가 그런데, 분할 가능 배낭문제는 그리디 방식으로 해결 가능하다.
- 문제 정의의 작은 차이가 문제 해결 방법에 큰 변화를 가져올 수 있다는 점을 확인하자.
0-1 배낭 문제
- O = {O1, O2, ... On}
- i 번째 O의 무게 Wi라고 정의
- 최대 무게 T 배낭에 넣은 물건들의 무게 합이 T를 넘기면 안된다.
- 가격 Vi 최대 무게를 넘지 않으면서 최대 가격을 내야된다 !
- NP-완전 문제이므로 모든 조합을 조사해서 무게 T를 넘지 않는 가장 높은 가격을 찾아내야 한다.
분할 가능 배낭 문제
- 주어진 물건을 원하는 형태로 얼마든지 분할할 수 있고 각 물건의 일부분만 배낭에 담을 수 있다고 가정
- 곡물, 기름등을 원하는 양만큼 덜어서 배낭에 담는다고 생각
- 단위 무게 당 가격을 기준으로 정렬하고 그리디 방식에 따라 단위 무게당 가격이 높은 순서로 물건을 선택
- 단위 무게 당 가격이 비싼 물건들 위주로 선택된다.
#include <iostream>
#include <algorithm>
#include <vector>
struct Object
{
int id;
int weight;
double value;
double value_per_unit_weight;
Object (int i, int w, double v) : id(i), weight(w), value(v),
value_per_unit_wiehgt(v / w) {}
inline bool operator< (const Object& obj) const
{
return this->value_per_unit_weight < obj.value_per_unit_weight;
}
friend std::sotream& operator<<(std::ostream& os, const Object& obj);
};
auto fill_knapsack(std::vector<Object>& objects, int knapsack_capacity)
{
std::vector<Object> knapsack_contents;
knapsack_contents:reserve(objects.size());
std::sort(objects.begin(), objects.end());
std::reverse(objects.begin(), objects.end());
auto current_object = objects.begin();
int current_total_weight = 0;
while (current_total_weight < knapsack_capacity && current_object != objects.end())
{
knapsack_contents.push_back(*current_object);
current_total_weight += current_object->weight;
current_object++;
}
int weight_of_last_obj_to_remove = current_total_weight - knapsack_capacity;
Object& last_object = knapsack_contents.back();
if (weight_of_last_obj_to_remove > 0)
{
last_object.weight -= weight_of_last_obj_to_remove;
last_object.value -= last_object.value_per_unit_weight * weight_of_last_obj_to_remove;
}
return knapsack_contents;
}
- 단위 무게당 가격 기준 내림차순 정렬 후
- 배낭 최대 용량 까지 물건을 채워 넣는다.
- 배낭 최대 무게 초과 시 마짐가에 넣은 물건 일부를 덜어내어 배낭 최대 무게에 맞도록 설정
int main()
{
std::vector<Object> objects;
objects.reserve(7);
std::vector<int> weights {1, 2, 5, 9, 2, 3, 4};
std::vector<double> values {10, 7, 15, 10, 12, 11, 5};
for (auto i = 0; i < 7; i ++)
{
objects.push_back(Object(i + 1, weights[i], values[i]));
}
int knapsack_capacity = 7;
auto solution = fill_knapsack(objects, knapsack_capacity);
- 분할 가능 배낭 문제는 물건의 일부만을 배낭에 담을 수 있다는 점이 0-1 배낭 문제와 다르다.
그리디 알고리즘의 요구 조건
- 앞에서는 그리디 접근 방식의 최적 솔루션을 제공하는 문제의 예였다.
- 모든 문제에 그리디 방식을 적용할 수 있는 것은 아니다.
- 그리디의 최적 솔루션은 최적 부분 구조, 그리디 선택 두 속성을 모두 갖고 있어야 얻을 수 있다.
- 최적 부분 구조(Optional subsctructure) : 주어진 문제 P에 대한 최적 솔루션이 P의 부분 문제들의 최적 솔루션으로 구성될 경우, 문제 P가 최적 부분 구조를 갖는다고 말한다.
- 그리디 선택 (greedy choice) : 주어진 문제 P에 대한 지역적 최적 솔루션을 반복적으로 선택해 전체 최적 솔루션을 구할 수 있을 경우, 문제 P가 그리디 선택 속성을 갖는다고 말한다.
최소 신장 트리 문제
- MST, Minimum Spanning Tree 문제
- 정점(Vertex)의 집합 V와 가중치를 갖는 Edge의 집합 E로 구성된 그래프 G = <V, E>가 주어질 때, 모든 정점을 연결하고 연결된 에지의 가중치 합이 최소인 Tree를 구하시오.
- 최소 신장 트리 T를 구하는 그리디 알고리즘
- 그래프 G의 모든 Edge를 최소 Heap H에 추가
- H로부터 Edge e를 하나 꺼낸다. e는 모든 에지 중 가장 가중치가 작은 Edge
- e의 양 끝 정점이 이미 T에 있을 경우 e로 인해 T에서 사이클이 발생할 수 있다. 이런 경우 e를 버리고 2단계로 이동한다.
- 최소 신장 트리 T에 e를 추가하고 2단계로 이동한다.
- 2 ~ 4를 반복하면서 가장 작은 가중치의 Edge를 찾고, 사이클 발생하지 않으면 해당 에지와 양 끝 정점을 최종 솔루션에 추가한다.
- 이를 크루스칼 최소 신장 트리 알고리즘이라고 부른다.
- 최소 신장 트리는 최적부분구조와 그리디 선택 속성을 갖는지 확인해보면 제대로 작동하는 알고리즘인지 알 수 있다.
- 최적 부분 구조
- 최소 신장 트리가 더 작은 최소 신장 트리의 집합으로 구성되지 않는다고 가정
- 그래프 G의 정점으로 구성된 최소 신장 트리 T가 있다고 가정한다. T에서 Edge e를 하나 선택하여 제거한다. e를 제거하면 T가 더 작은 트리인 T1 T2로 나눠진다.
- MST 문제가 최적 부분 구조 속성을 갖지 않는다고 가정했으므로 T1보다 작은 가중치를 갖는 신장 트리 T1'이 존재해야된다. 이 신장 트리 T1'과 T2를 e 엣지로 연결한 새로운 시장 트리를 T'이라고 해보자.
- T'의 전체 가중치가 T의 전체 가중치보다 작으므로 처음에 T가 최소 신장 트리라고 가정했던 가설이 틀리게 된다. 그러므로 MST 문제는 최적 부분 구조 속성을 만족해야된다.
- 그리디 선택
- 정점과 연결된 에지 중 최소 가중치 에지는 반드시 최소 신장 트리 T에 속해야 된다.
- 정점 v에 연결되어 있는 에지 중 최소 가중치를 갖는 에지를 (u, v)라고 가정한다.
- 만약 (u, v)가 T에 속하지 않는다면 T는 v와 연결되어 있는 다른 에지를 포함해야한다. 이 에지를 (x, v)라고 가정한다. (u, v)가 최소 가중치 에지이므로 (x, v)의 가중치는 (u, v)의 가중치보다 커야한다.
- T에서 (x, v)를 제거하고 (u, v)를 추가할 경우 전체 가중치가 더 작은 트리를 얻을 수 있다. 이는 T가 최소 신장 트리라는 가정에 위배된다. 그러므로 MST 문제는 그리디 선택 속성을 만족해야한다.
디스조인트-셋 자료 구조
- Disjoint-Set or Union-Find 자료 구조는 트리 형태 원소로 구성된 Forest이다.
- 각각의 원소는 숫자 ID에 의해 표현되며 Rank와 부모에 대한 포인터를 가진다.
- make-set(x) 연산 지원
- x를 ID로 갖는 원소를 디스조인트-셋 자료 구조에 추가한다.
- 새로 추가한 운소의 랭크는 0, 부모 포인터는 자기 자신을 가리키도록 설정한다.
- find(x) 연산 지원
- 원소 x에서 시작해서 부모 포인터를 따라 반복적으로 이동해 트리의 루트를 반환한다.
- 루트 원소의 부노는 자신이다. make-set(x) 연산 시 각 원소는 트리의 루트이며 find() 연산 시 자기 자신이 반환된다.
- union(x, y) 연산 지원
- 두 개의 원소 x, y에 대해 union() 연산을 수행하면 먼저 x, y의 루트를 찾는다.
- 두 루트가 같으면 x, y는 같은 트리에 속해있음을 의미한다. 이 경우 아무런 작업도 하지 않는다.
- 만약, 두 루트가 다르면 높은 랭크 루트를 낮은 랭크 루트의 부모로 설정한다.
- union(x, y) 연산을 거듭할 수록 더 많은 트리가 병합 되어 전체 트리 개수는 줄어들고, 각 트리의 크기는 점점 거대해진다.
- 크루스칼 알고리즘 구현 방법
- 그래프 정점 개수가 N이라면, 먼저 N개 원소를 갖는 디스조인트-셋 자료 구조를 초기화하고, 크루스칼 알고리즘 2단계 - 3단계에서 최소 힙을 이용해 가장 가중치가 작은 에지를 선택하고, 사이클 여부를 검사한다. 이 때 union(x, y) 연산을 활용할 수 있다. 즉, 에지의 양 끝 두 정점에 대해 union(x, y)를 수행하여 실제로 두 트리가 병합하면 해당 Edge를 MST에 추가한다. 만약 x, y 루트가 같으면 두 정점을 잇는 에지에 의해 사이클이 발생한다는 의미다. 그런 경우 아무 작업 없이 그대로 종료하고 해당 에지를 MST에 추가하지 않는다.
- 모든 정점을 이용해 디스조인트-셋 자료 구조를 초기화한다.
- 가중치가 가장 작은 에지를 MST에 추가한다.
- union(2, 4)을 수행하고, (2, 4) 엣지를 MST에 추가한다.
- 크루스칼 알고리즘 대로 MST에 엣지를 추가하다보면, (1, 5) Edge를 검토할 차례가 나타난다. (그림참조) 1, 5가 같은 트리에 존재하는데, 해당 에지를 연결하면 사이클이 발생하므로 MST에 추가할 수 없다.
크루스칼 MST 알고리즘 구현
- Disjoint-set 자료 구조를 사용하지 않는 크루스칼 알고리즘 시간 복잡도는 O(ElogE)이다.
- Disjoint-set 자료 구조 사용 시 O(Ea(V))로 줄어든다.
- a는 여기서 아커만 함수의 역함수이다. 아커만 함수의 역함수는 로그 함수보다 훨씬 느리게 증가한다. 그러므로 정점 개수가 적은 그래프에서는 두 구현의 성능 차이가 작지만, 정점이 많은 그래프에서는 큰 성능 차이가 발생할 수 있다.
그래프 컬러링
- 주어진 그래프 G에서 서로 인접한 정점끼리 같은 색을 가지지 않도록 모든 정점에 색상을 지정해야 한다. 는 것으로 정의가 된다.
- 그래프 컬러링에서는 에지의 가중치는 사용하지 않는다.
- 택시 예약 스케줄 작성, 스도쿠 퍼즐 풀기, 시험 시간표 작성 등을 모델링 한 후 컬러링 문제 형태로 해결
- 그래프 컬러링에 필요한 최소 개수의 색상 수를 찾는 것은 NP-완전 문제이므로 문제를 조금 변경하여 시간 복잡도를 크게 변경할 수 있다.
구현
- 그래프 컬러링의 평가는 얼마나 적은 수의 색상을 사용했는가에 의해 결정된다.
- 가능한 적은 수의 색상을 사용하는 최적의 그래프 컬러링 방법 찾기는 NP-완전 문제이지만 그리디 방식이 유용한 근사치를 제공하곤 한다.
- 컴파일하는 프로그램의 변수에 CPU 레지스터 할당 시 그래프 컬러링이 사용된다.