문제 상황
한 회사 A가 곧 IPO(기업공개)를 진행하려 한다고 가정해 봅시다. 주식을 좋은 가격에 판매하기 위해 A는 IPO 전에 여러 프로젝트를 수행해 자본을 늘리고 싶어 합니다. 하지만 A의 자원은 제한적이어서, 최대 k개의 서로 다른 프로젝트만 완료할 수 있습니다. 과연 최대 k개의 프로젝트를 마친 뒤 총 자본을 최대화하려면 어떤 프로젝트를 어떤 순서로 선택해야 할까요?
문제 정의
여러 개의 프로젝트가 주어집니다. 각 프로젝트 i마다 순수익 Pi가 있으며, 해당 프로젝트를 시작하려면 최소 자본 Ci가 필요합니다. 처음에 우리가 가진 자본은 W입니다. 프로젝트 하나를 완료할 때마다 그 프로젝트의 순수익을 얻게 되고, 이 수익은 총 자본에 더해집니다.
정리하면, 주어진 프로젝트 목록에서 최대 k개의 서로 다른 프로젝트를 선택해 최종 자본을 최대화하고, 그 최대화된 자본 값을 출력하는 것이 목표입니다.
입력 예시
k = 2, W = 0, 이익 배열 [1,2,4], 자본 배열 [0,1,1]이 주어졌을 때 출력은 5입니다.
처음 자본이 0이므로 인덱스 0의 프로젝트(필요 자본 0)만 시작할 수 있습니다. 이 프로젝트를 완료하면 이익 1을 얻어 자본이 1이 됩니다. 자본 1이 되면 인덱스 1 또는 2의 프로젝트를 시작할 수 있는데, 더 큰 이익을 내는 인덱스 2의 프로젝트를 선택하는 것이 유리합니다. 따라서 최종 답은 0 + 1 + 4 = 5가 됩니다.
풀이 접근 방법
이 문제는 그리디(Greedy) 기법과 우선순위 큐(priority queue)를 조합하면 효율적으로 해결할 수 있습니다. 핵심 아이디어는 다음과 같습니다.
- 현재 자본으로 시작할 수 있는 프로젝트 중에서 항상 순수익이 가장 큰 프로젝트를 선택한다.
- 이를 위해 '필요 자본' 기준의 최소 힙(min-heap)과, '시작 가능한 프로젝트의 이익'을 관리하는 최대 힙(max-heap), 두 개의 우선순위 큐를 사용합니다.
알고리즘 단계
- 우선순위 큐 pqCapital(자본 기준 최소 힙)과 pqMain(이익 기준 최대 힙)을 생성합니다.
- n := Profits의 크기로 설정합니다.
- i를 0부터 n-1까지 반복하면서 { Profits[i], Capital[i] } 쌍을 pqCapital에 삽입합니다.
- i를 0부터 k-1까지 반복하면서 다음을 수행합니다.
- pqCapital이 비어 있지 않고, pqCapital의 top 요소의 자본값(second)이 W 이하인 동안, top 요소를 pqMain으로 옮긴 뒤 pqCapital에서 제거합니다.
- pqMain이 비어 있다면 더 이상 진행할 수 있는 프로젝트가 없으므로 반복문을 빠져나갑니다.
- W에 pqMain의 top 요소의 이익값(first)을 더하고, pqMain에서 해당 요소를 제거합니다.
- W를 반환합니다.
즉, 매 단계마다 현재 자본으로 실행 가능한 프로젝트들을 최대 힙에 모아 놓고, 그중 이익이 가장 큰 것을 하나씩 실행하는 방식입니다. 이렇게 하면 매번 최선의 선택을 하게 되어 최종 자본이 최대화됩니다.
C++ 구현 예제
아래 구현을 통해 더 잘 이해해 보겠습니다.
예제 코드
#include <bits/stdc++.h>
using namespace std;
struct Comparator{
bool operator() (pair <int, int> a, pair <int, int> b){
return !(a.second < b.second);
}
};
class Solution {
public:
int findMaximizedCapital(int k, int W, vector<int>& Profits, vector<int>& Capital) {
priority_queue < pair <int, int>, vector < pair <int, int> >, Comparator> pqCapital;
priority_queue < pair <int ,int> > pqMain;
int n = Profits.size();
for(int i = 0; i < n; i++){
pqCapital.push({Profits[i], Capital[i]});
}
for(int i = 0; i < k; i++){
while(!pqCapital.empty() && pqCapital.top().second <= W){
pqMain.push(pqCapital.top());
pqCapital.pop();
}
if(pqMain.empty()) break;
W += pqMain.top().first;
pqMain.pop();
}
return W;
}
};
main(){
Solution ob;
vector<int> v = {1,2,4}, v1 = {0,1,1};
cout << (ob.findMaximizedCapital(2,0, v, v1));
}
입력
2
0
[1,2,4]
[0,1,1]
출력
5
복잡도 분석
- 시간 복잡도: 모든 프로젝트를 힙에 삽입하는 데 O(n log n), 각 반복에서 힙 연산이 발생하므로 전체적으로 O((n + k) log n)입니다.
- 공간 복잡도: 두 개의 우선순위 큐에 프로젝트 정보를 저장하므로 O(n)입니다.
마무리
IPO 문제는 '매 순간 최선의 선택을 한다'는 그리디 전략이 최적해를 보장하는 대표적인 유형입니다. 두 개의 우선순위 큐를 활용해 '지금 시작할 수 있는 프로젝트'와 '그중 가장 수익성 높은 프로젝트'를 효율적으로 관리하는 것이 핵심 포인트입니다. 비슷한 패턴은 작업 스케줄링, 리소스 할당 등 다양한 알고리즘 문제에서도 응용되므로 꼭 익혀 두시기 바랍니다.