Greedy 알고리즘은 현재 주어진 상황에서 가장 이득이 되는 최선의 경우를 선택하는 알고리즘을 의미한다.
Greedy 알고리즘은 지금의 선택이 앞으로 남은 선택들에 대해 어떤 영향을 끼칠지 고려하지 않는다.
Greedy 알고리즘은 최적해를 구하는 알고리즘이 아니다.
Greedy 알고리즘 정당성 증명
[전략 1] - 승수를 최대화하기 위해서는 상대편 선수보다 우리편 선수가 rating이 높은 선수들 중 가장 rating이 낮은 선수를 내보내는 것이 최선이다.
[전략 2] - 상대편 선수의 rating이 모든 우리편 선수보다 높을 경우, 우리편의 가장 낮은 rating을 가진 선수를 내보내는 것이 최선이다.
근데 정말 이게 최선(greedy)일까? 이 전략들로 최적해를 구할 수 있을까? -> 정당성 증명!!
greedy choice property
전략 1의 경우
만일 가장 rating이 낮은 선수 말고 다른 선수를 내보내는 최적해가 존재한다면?
러시아 | 2,000 | x | 1,900 |
---|---|---|---|
한국 | B | A | 2,000 |
B > A이고 A가 한국의 가장 낮은 rating을 가진 선수라고 생각해보자. 2000 vs B는 B의 승리이고, x vs A는 승부 결과를 알 수 없다.
이제, A와 B를 바꾼다고 가정해보자 (우리가 세운 가설대로)
2000 vs A의 승부 결과를 알 수 없다. 그러나 x vs B는 B의 승리일 것이다. 즉, 최적해는 그대로다. 이 말은 곧 greedy 방식을 택해도 우리가 손해볼 일은 없다는 것이다!
전략 2의 경우
만일 가장 rating이 낮은 선수 말고 다른 선수를 내보내는 최적해가 존재한다면?
러시아 | 999,999 | x | 1,900 |
---|---|---|---|
한국 | B | A | 2,000 |
B > A이고 A가 한국의 가장 낮은 rating을 가진 선수라고 생각해보자. 999,999 vs B는 상대팀의 승리이고, x vs A는 승부 결과를 알 수 없다.
이제, A와 B를 바꾼다고 가정해보자 (우리가 세운 가설대로)
999,999 vs A의 승부도 상대팀의 승리다. 그러나 x vs B는 결과를 알 수 없다. 즉, 최적해는 그대로다. 말은 곧 greedy 방식을 택해도 우리가 손해볼 일은 없다는 것이다!
Optimal substructure
코드를 작성해보자
#include <iostream>
#include <vector>
#include <set>
#include <algorithm>
using namespace std;
static int C, N;
int solve(const vector<int>& rus, const vector<int>& kor) {
int ret = 0;
multiset<int> ratings(begin(kor), end(kor));
for (int rusIdx = 0; rusIdx < N; ++rusIdx) {
if (*ratings.rbegin() < rus[rusIdx])
ratings.erase(begin(ratings));
else {
ratings.erase(ratings.lower_bound(rus[rusIdx]));
ret++;
}
}
return ret;
}
int main() {
ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr);
cin >> C;
while (C--) {
cin >> N;
vector<int> rus(N), kor(N);
for (int i = 0; i < N; ++i) cin >> rus[i];
for (int i = 0; i < N; ++i) cin >> kor[i];
cout << solve(rus, kor) << '\n';
}
}
요소의 중복을 허용하고 트리 구조로 자료를 저장하는 multiset stl 컨테이너를 사용해서 한국 선수를 레이팅 순으로 정렬해서 저장한다.
첫 번째 if문은 필패하는 경우의 수를 의미한다. 필패하는 경우 가장 낮은 레이팅을 가진 선수를 내보내는게 최선이다.
두 번째 else문은 이기는 경우를 의미한다. 이기는 경우 이길 수 있는 선수 중 가장 낮은 레이팅을 가진 선수를 내보내는게 최선이다.
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
static int C, N;
int solve(vector<int>& rus, vector<int>& kor) {
int ret = 0, size = rus.size() - 1;
sort(begin(rus), end(rus)); // 러시아 선수 레이팅 순 정렬
sort(begin(kor), end(kor)); // 한국 선수 레이팅 순 정렬
while (rus[size] > kor.back()) size--; // 필패하는 경우 카운트
int startKorIdx = N - size - 1; // 최소 rating 선수를 제외한 시작 인덱스값
for (int korIdx = startKorIdx, rusIdx = 0; korIdx < N; ++korIdx) {
if (rus[rusIdx] <= kor[korIdx]) { // 이기는 경우
rusIdx++; // 다음 선수와 비교하기 위해 증가
ret++; // 승수 증가
}
}
return ret;
}
int main() {
ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr);
cin >> C;
while (C--) {
cin >> N;
vector<int> rus(N), kor(N);
for (int i = 0; i < N; ++i) cin >> rus[i];
for (int i = 0; i < N; ++i) cin >> kor[i];
cout << solve(rus, kor) << '\n';
}
}
어떤 순서로 도시락을 데우던지간에 일단 모든 도시락을 데워야 하므로 최소 sum(모든 도시락 데우는 시간)은 소요된다.
그러므로 결과에 영향을 주는 것은 마지막에 데우는 도시락을 먹는데 걸리는 시간 이다. 따라서 먹는데 오래걸리는 도시락 먼저 데우는 것이 최선임을 알 수 있다.
정말 그게 최선일까?? (정당성 증명)
greedy choice property
Optimal substructure
그러므로 greedy 방법을 택하는 것이 최적해를 이끄는 과정과 동일하다는 것을 알 수 있다.
코드를 작성해보자
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
static int C = 0;
int solve(vector<pair<int, int>>& vec) {
// 먹는 시간이 긴 순으로 정렬함
sort(vec.begin(), vec.end(), [](pair<int, int> a, pair<int, int> b){return a.second > b.second;});
int ret = 0, beginEat = 0;
for(const auto& time : vec) {
beginEat += time.first; // 현재시간 + 데우는 데 걸리는 시간
ret = max(ret, beginEat + time.second); // 먹기시작한 시간 + 먹는시간
}
return ret;
}
int main() {
ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr);
cin >> C;
while (C--) {
int n = 0; cin >> n;
vector<pair<int, int>> vec(n);
for (int i = 0; i < n; ++i) cin >> vec[i].second; // 데우는 시간
for (int i = 0; i < n; ++i) cin >> vec[i].second; // 먹는 시간
cout << solve(vec) << '\n';
}
} // 수행 시간: 32ms
먹기 시작한 시간 (현재시간 + 데우는 데 걸리는 시간)
+ 먹는 시간
을 계산해서 return 한다.문자는 함정이다. 문자는 전혀 신경쓰지 않고 숫자만 신경쓰면 된다.
여러 번 테스트 케이스를 만들어서 손으로 풀어보자. 낮은 숫자 순으로 만들면 최소 반복이 된다는 것을 확인할 수 있다.
정당성 증명은 스킵한다. (교재 참고)
코드를 작성해보자
#include <iostream>
#include <queue>
using namespace std;
int main() {
ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr);
int C = 0; cin >> C;
while (C--) {
int n = 0; cin >> n;
priority_queue<int, vector<int>, greater<int>> pq;
for (int i = 0; i < n; ++i) {
int input; cin >> input;
pq.push(input);
}
int ans = 0;
while (pq.size() != 1) {
int lhs = pq.top(); pq.pop();
int rhs = pq.top(); pq.pop();
int tmp = lhs + rhs;
ans += tmp; // 값에 더해준다.
pq.push(tmp); // push 하면 자동으로 크기 순으로 정렬되어 들어간다.
}
cout << ans << '\n';
}
} // 수행시간: 0ms
자세한 설명 감사합니다!