ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 그리디 알고리즘 (Greedy Algorithm)
    코딩 테스트/코딩 테스트 알고리즘 2022. 10. 26. 12:03

    01. 그리디 알고리즘이란?

    • 현재 상황에서 가장 좋은 것만 고르는 방식이다.
    • 탐욕법이라고도 한다.
    • 사전에 특정 알고리즘을 공부하지 않아도 풀이를 떠올릴 수 있는 경우가 많다.

    02. 그리디 알고리즘의 예시 : 거스름돈 문제

    내가 가게의 계산 직원이라고 생각했을 때, 거스름돈이 500원, 100원, 50원, 10원 단위로 있다고 생각하자.
    손님에게 거슬러 줘야 할 돈이 N원일 때, 가장 적은 동전을 써서 거슬러줄 수 있는 경우는?
    • 전형적인 그리디 알고리즘의 예시
    • "가장 큰 화폐 단위부터" 돈을 거슬러 주면 됨
    • 1370원을 거슬러 줘야 한다고 가정할 때
      • 가장 큰 단위인 500원을 2번 거슬러 준다. (남은 돈 370원)
      • 두 번째 큰 단위인 100원을 3번 거슬러 준다 (남은 돈 70원)
      • 세 번째 큰 단위인 50원을 2번 거슬러 준다. (남은 돈 20원)
      • 가장 작은 단위인 10원을 2번 거슬러 준다. (남은 돈 0원)
      • 결과적으로 2 + 3 + 2 + 2 = 11개가 가장 적게 쓸 수 있는 동전 개수
    • 다음 수행에 상관없이 일단 가장 많이 거스름돈을 깎을 수 있는 단위를 사용한다는 것이 중요

    03. 그리디 알고리즘의 정당성 판별

    • 중요한 것은, 지금 상황에서 가장 좋은 것만을 골라도 문제를 풀 수 있는지를 파악하는 것이 중요
    • 기준에 따라 좋은 것을 선택하는 알고리즘이므로, "특정 기준"을 제시해주는 경우가 많다.
      • "가장 작은 순서대로", "가장 큰 순서대로"
    • 거스름돈 문제에 그리디 알고리즘을 적용할 수 있었던 이유는?
      • 가지고 있는 동전 개수가 항상 작은 단위의 배수이므로, 작은 단위의 동전들을 조합해 다른 해가 나올 수 없기 때문
      • 예를 들어, 500원, 400원, 100원인 경우 800원을 거슬러 줘야 한다고 했을 때
        • 그리디 알고리즘을 적용하면 500 + 100 + 100 + 100 => 4번이지만
        • 400 + 400으로 다른 최적해가 존재한다.
    • 정당성을 검토해보고, 그렇지 못하다면 동적 프로그래밍이나 그래프 알고리즘을 고민해보자.

    04. 실전 문제 풀이

    1. 큰 수의 법칙

    더보기
    // 그리디 알고리즘 >> p.92 큰 수의 법칙
    
    #include <iostream>
    #include <vector>
    #include <algorithm>
    #include <functional>
    
    using namespace std;
    
    int main()
    {
    	int n, m, k;
    	cin >> n >> m >> k;
    
    	vector<int> nums(n);
    
    	for (int i = 0; i < n; ++i)
    	{
    		cin >> nums[i];
    	}
    
    	// 첫 작성 답안
     //	{
     //		int answer = 0;
     //		int max = -1, max2 = -1;
     //
     //		for (int i = 0; i < n; ++i)
     //		{
     //			if (max <= nums[i])
     //			{
     //				max2 = max;
     //				max = nums[i];
     //			}
     //		}
     //
     //		int cnt = 0;
     //		while (m > 0)
     //		{
     //			if (cnt == 3)
     //			{
     //				answer += max2;
     //				cnt = 0;
     //			}
     //			else
     //			{
     //				answer += max;
     //				++cnt;
     //			}
     //			--m;
     //		}
     //
     //		cout << answer;
     //	}
     //
    	// 개선된 버전
    	{
    		int answer = 0;
    		int max = -1, max2 = -1;
    
    		sort(nums.begin(), nums.end(), greater<int>());
    
    		// 가장 큰 수와 두번째로 큰 수
    		max = nums[0];
    		max2 = nums[1];
    
    		// 가장 큰 수 등장 횟수
    		int maxCount = (m / (k + 1)) * k + (m % (k + 1));
    
    		answer += maxCount * max;
    		answer += (m - maxCount) * max2;
    
    		cout << answer;
    	}
    	
    	return 0;
    }
    • "가장 큰 수"를 무조건 많이 더해야 하므로, 그리디 알고리즘이라고 할 수 있음
    • k번 이상이 연속될 수 없으므로, k번 가장 큰 수를 더한 이후 1번은 두 번째 큰 수를 더해줘야 한다.
    • 연산을 while루프당 한 번씩 해 주는 방식으로 처리할 수도 있지만, 효율적으로 계산하기 위해 수학적 아이디어를 사용할 수 있다.
    • 가장 큰 수가 8, 두 번째로 큰 수가 6이라고 할 때, M이 8이고 K가 3이라면
      • (8 + 8 + 8 + 6) + (8 + 8 + 8 + 6) 이 답이다.
      • 8 8 8 6이라는 수열이 반복되는 것을 볼 수 있다.
      • 이 수열이 등장하는 횟수를 찾으면, 계산을 간단히 처리할 수 있다.
        • 수열의 크기는 k + 1이므로, 수열이 등장하는 횟수는 m / (k+1)이다.
        • 그렇다면, 가장 큰 수가 등장하는 횟수는, 수열 등장 횟수 * k이다. k * (m / (k + 1))
        • 만약, m이 k + 1로 나누어 떨어지지 않는 경우, 가장 큰 수는 M을 수열이 등장하는 횟수로 나누었을 때 나머지만큼 더 등장한다. m % (k + 1)
        • 최종적으로, 가장 큰 수가 등장하는 횟수 k * (m / (k + 1)) + m % (k + 1)
        • 두 번째로 큰 숫자가 등장하는 횟수는 m 에서 가장 큰 수가 등장하는 횟수를 빼주면 된다.

    2. 숫자 카드 게임

    더보기
    // 그리디 알고리즘 >> p.96 숫자 카드 게임 
    
    #include <iostream>
    #include <vector>
    #include <algorithm>
    
    using namespace std;
    
    int main()
    {
    
    	// 첫 작성 답안
    	{
     //		int n, m, input, ans = 0;
     //		cin >> n >> m;
     //
     //		vector<vector<int>> cards(n);
     //		for (int i = 0; i < n; ++i)
     //		{
     //			for (int j = 0; j < m; ++j)
     //			{
     //				cin >> input;
     //				cards[i].push_back(input);
     //			}
     //		}
    
     //		// 최소값이 가장 큰 행을 찾는다.
     //		for (int i = 0; i < n; ++i)
     //		{
     //			sort(cards[i].begin(), cards[i].end());
     //		}
     //
     //		// 각 행의 최소값을 비교한다. 
     //		for (int i = 0; i < n; ++i)
     //		{
     //			if (cards[i][0] > ans)
     //				ans = cards[i][0];
     //		}
     //
     //		cout << ans;
    	}
    
    	// 개선안
    	{
    		int n, m, input;
    		cin >> n >> m;
    
    		int ans = 0;
    		for (int i = 0; i < n; ++i)
    		{
    			// 한 줄씩 입력 받아서 행의 가장 작은 수 찾는다.
    			int minVal = 10001;
    			for (int j = 0; j < m; ++j)
    			{
    				cin >> input;
    				minVal = min(minVal, input);
    			}
    
    			// 행의 최소값들을 비교해 가장 큰 수를 찾는다.
    			ans = max(ans, minVal);
    		}
    
    		cout << ans;
    	}
    
    	return 0;
    }
    • 한 행 입력 당, 가장 작은 수를 찾고, 모든 행 중 작은 수 중 가장 큰 수를 찾으면 된다.

    3. 1이 될 때까지

    더보기
    // 그리디 알고리즘 >> p.99 1이 될 때까지
    
    #include <iostream>
    
    using namespace std;
    
    int main()
    {
    	int n, k, ans = 0;
    
    	cin >> n >> k;
    
    	// n 이 k 이상일 경우 계속 나눈다. 
    	while (n >= k)
    	{
    		// 나누어 떨어지는 경우
    		if (n % k == 0)
    		{
    			n /= k;
    			++ans;
    		}
    		// 나누어 떨어지지 않는 경우 배수가 되도록 뺀다.
    		else
    		{
    			ans += n % k;
    			n -= n % k;
    		}
    	}
    
    	ans += n - 1;
    
    	cout << ans;
    }
    • 나누기 연산을 가장 많이 하는 것이 최적해를 찾는 방법이다.
      • 나누가 연산이 n을 가장 많이 감소시키고, n을 가장 적은 연산 횟수로 1로 만드는 것이 목적이므로

    <이것이 취업을 위한 코딩 테스트다> 를 보고 정리한 자료입니다.

    댓글

GameDev.