멋사 부트캠프/알고리즘

02. 탐욕 알고리즘(Greedy)

cTosMaster 2025. 4. 23. 12:09

1. 정의

   현재 위치에서 최적의 해를 찾는 알고리즘.

   즉, 현재 위치의 최선이  전체의 최선이라는 전제를 기반으로 한다.

 

2. 활용 제약조건

    현재 위치에서의 선택이 전체에 미치는 영향이 제한적이어야 하며,
    그 영향이 국소적인 범위 안에서 해결 가능해야 한다.

3. 사용 예시

  • 체육복 문제 (±1 범위 내 대여)
  • 회의실 배정 (가장 빨리 끝나는 회의 선택)
  • 동전 거스름돈 (가장 큰 단위부터 사용)
  • 다익스트라 알고리즘 (가장 가까운 정점부터 탐색)
  • Kruskal, Prim (최소 비용 간선 선택)

   실 사용 예시 = > 네비게이션, 라우팅 알고리즘(OSPF)등...

 

4. 이해하기 쉽게 설명

    - 어렸을 적 오락실에서 게임을 떠올려 보자. 

      마지막에 랭킹 등록할 때, "AAA" 초기화가 된 것에서 1, 2, 3번째 글자를 입력할 때,
      앞으로 갔다가 뒤로 갔다가, 빠르게 입력하려고 노력했을 것이다.  

      1, 2, 3번째 입력하는 3가지의 경우를 각각 최적으로 입력하다보니 결론적으로 가장 빠르게 입력할 수 있다는 것.

      어렸을 때부터 그리디 알고리즘을 우리는 벌써 다루어 본 것이다.

 

 

 

5. 예제 코드 

//프로그래머스 : 조이스틱
//[전제 조건]
// 1. 문자열 자릿수에 맞처 'A'로 전부 초기화됨.
// 2. 문자열의 글자를 입력해야 하는데 A-Z까지 최적의 움직임으로 찾는 것.

class Solution {
    public int solution(String name) {
        int answer = 0;
        char[] charr = name.toCharArray(); 
        int count = 0;
        
        //상,하 이동 최적 계산 -> 단순 누적
        for(char res : charr){
            answer += Math.min(res - 'A', 'Z' - res + 1);
        }
        
        //좌우 최적 경로 찾기
        answer += getWide(name);    
        return answer;
    }
    // 그리디 : 좌우 최적 경로 찾기 
    // 최악의 해 : name.length - 1; => 정방향으로만 가는 경우
    // 돌아서 가는게 최악보다 최적일 경우 그 경로를 갱신하여 리턴함
    // JAEANE의 경우 -> 국소 최적을 검사하는 인덱스 -> E, N, E
    public int getWide(String name) {
        int len = name.length();
        int minMove = len - 1;

        for (int i = 0; i < len; i++) {
            int next = i + 1;
            //'A'는 바꿔줄 필요가 없으므로 건너뜀.
            while (next < len && name.charAt(next) == 'A') {  
                next++;
            }
            int move = i + i + (len - next); // 시작점(0) -> 오른쪽 -> 현재위치(i) -> 왼쪽 -> next  (정방향 진행 후 돌아가기)
            int reverseMove = (len - next) * 2 + i; // 시작점(0) -> 왼쪽 -> 오른쪽 -> 현재위치(i) (역방향 진행 후 돌아가기)
            minMove = Math.min(minMove, Math.min(move, reverseMove));  //최악보다 최적일 경우 리턴함.
	}
        return minMove;
    }
}

 

전략 : 좌/우 “이동” 최적화에 대한 그리디 전략

  • 국소최적 : 바꿔야 할 문자 기준 ⇒ 정방향 진행 후 왼쪽으로 돌아가기 vs 역방향 진행 후 왼쪽으로 돌아가기
  • 전체최적 : 정방향으로만 진행하기 vs 진행 후 돌아가기(국소최적)
//백준 : ATM
import java.util.Arrays;
import java.util.Scanner;

public class greedy_atm {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int[] wastetimes = new int[n];
        int sum = 0;
        int answer = 0;

        for(int i =0; i<n; i++){
            wastetimes[i] = sc.nextInt();
        }
        Arrays.sort(wastetimes);
        for(int i=0; i<n; i++){
            sum += wastetimes[i];
            answer += sum;
        }
        
        System.out.println(answer);
    } 
}

전략 : 시간[크기] 최적화에 대한 그리디 전략

   * 국소 최적 : Arrays.sort(wastetimes); → 작은 시간부터 정렬 후 구하는 것이 최적

   * 전체 최적 : sum += wastetimes[i], answer += sum (대기기간 누적)

 

//백준 : 한줄로 서기
import java.util.*;

public class greedy_linearray {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int[] memlist = new int[n]; //각 번호사람 : 기억하는 왼쪽사람 수
        int[] answer = new int[n];

        //기억하는 사람수 입력
        for (int i = 0; i < n; i++) {
            memlist[i] = sc.nextInt();
        }
        //answer -> 사람번호 재배치
        for (int i = 0; i < n; i++) {
            int cnt = 0;            //
            int pnum = i + 1;       //사람번호
            //빈자리인지 연속 탐색
            for (int j = 0; j < n; j++) {
                if (answer[j] == 0) {           // 빈 자리인지 검사
                    if (cnt == memlist[i]) {    // 중복될 경우 1개씩 cnt 증가
                        answer[j] = pnum;
                        break;
                    }
                    cnt++;
                }
            }
        }

        for (int res : answer) {
            System.out.print(res + " ");
        }
    }
}

전략 : 인덱스 기반 순서 배치 그리디 전략

* 국소 최적 : 내 앞에 큰 사람 수만큼 현재 위치에서 먼저 배치함. (다음사람은 한칸 밀어버림)

* 전체 최적 : 국소 최적을 만족하면 결론적으로 앞에 정의된 큰사람만큼 정렬됨.