탐욕 기법 & 분할 정복

Updated:

탐욕 기법

  • 문제 제시 : 거스름돈 줄이기
    • 손님이 지불한 금액에서 물건값을 제한 차액(거스름돈)을 지불하는 문제를 생각해보자
    • “어떻게 하면 손님에게 거스름돈으로 주는 지폐와 동전의 개수를 최소한으로 줄일 수 있을까?”
    • => 큰단위의 화폐를 가장 먼저 많이 사용!
  • 탐욕 알고리즘의 장점은 모두 경우의 수를 다 해보는게 아니라 우리가 할 수 있는 선택 중 최선의 선택을 해가는 알고리즘.
  • 내가 선택한 선택이 최선이라고 생각되어지면 그 외의 선택에 대해선 고민하지 않는다.
  • 나머지를 고려하지 않기 때문에 시간적 우위에 있다.
  • 탐욕 알고리즘은 최적해를 구하는데 사용되는 근시안적인 방법
  • 최적화 문제란 가능한 해들 중에서 가장 좋은(최대 또는 최소)해를 찾는 문제이다
  • 일반적으로, 머리 속에 떠오르는 생각을 검증 없이 바로 구현하면 Greedy 접근이 된다.
  • 여러 경우 중 하나를 선택 할 때마다 그 순간에 최적이라고 생각되는 것을 선택해 나가는 방식으로 진행하여 최종적인 해답에 도달한다.
  • 각 선택 시점에서 이루어지는 결정은 지역적으로는 최적이지만, 그 선택들을 계속 수집하여 최정적인 해답을 만들었다고 하여, 그것이 최적이라는 보장은 없다.
  • 일단, 한번 선택된 것은 번복하지 않는다. 이런 특성 때문에 대부분의 탐욕 알고리즘들은 단순하며, 또한 제한적인 문제들에 적용된다.
  • 단, 동전의 단위가 배수가 아니라 예를들어, 400원이 더 있다고 하면, 무조건 큰 단위부터 나누는 방법이 최선의 방법이 되지 않는다.

예시) 배낭 짐싸기

  • 도둑은 부자들의 값진 물건들을 훔치기 위해 보관 창고에 침입하였다.
  • 도둑은 훔친 물건을 배낭에 담아 올 계획이다. 배낭은 담을 수 있는 물건의 총 무게(W)가 정해져 있따.
  • 창고에는 여러개(n개)의 물건들이 있고, 각각의 물건에는 무게와 값이 정해져 있다.
  • 경배원들에게 발각되기 전에 배낭이 수용할 수 있는 무게를 초과하지 않으면서, 값이 최대가 되는물건들을 담아야 한다.
  • Knapsack 문제유형
  • 0-1 Knapsack
    • 배낭에 물건을 통째로 담아야 하는 문제
    • 물건을 쪼갤 수 없는 경우
  • Fractional Knapsack
    • 물건을 부분적으로 담는 것이 허용되는 문제
    • 물건을 쪼갤 수 있는 경우

0-1 Knapsack에 대한 완전 검색 방법

  • 완전 검색으로 물건들의 집합 S에 대한 모든 부분집합을 구한다.
  • 부분집합의 총 무게가 W를 초과하는 집합들은 버리고, 나머지 집합에서 총 값이 가장 큰 집합을 선택할 수 있다.
  • 물건의 개수가 증가하면 시간 복잡도가 지수적으로 증가한다 ( 크기 n인 부분집합의 수 2^n)
  • 만약 n이 30이면 2^30 은 대략 10억이 되버린다. 그래서 이 방법으로 구할 수 없다.

0-1 Knapsack에 대한 탐욕적 방법

  • 값이 비싼 물건부터 채운다.(최적 x)
  • 무게가 가벼운 물건부터 채운다.(최적 x)
  • 무게 당(예> kg당) 값이 높은 순서로 물건을 채운다.(최적 x)

Fractional Knapsack 문제

  • 물건의 일부를 잘라서 담을 수 있다.
  • 배낭에 물건을 꽉 채운다.
  • 탐욕적인 방법=> 단위무게가 비싼거부터 채워넣는다.

문제 제시

  • 김대리는 소프트웨어 개발팀들의 회의실 사용 신청을 처리하는 업무를 한다. 이번 주 금요일에 사용 가능한 회의실은 하나만 존재하고, 다수의 회의가 신청된 상태이다.
  • 회의는 시작 시간과 종료 시간이 있으며, 회의 시간이 겹치는 회의들은 동시에 열릴 수 없다.
  • 가능한 많은 회의가 열리기 위해서는 회의들을 어떻게 배졍해야 할까?
  • 선택 가능한 회의 중 종료시간이 빠른 순서로 해결하는게 좋다.
  • 종료 시간이 빠른 순서로 활동들을 정렬
  • 첫 번째 활동을 선택한다.
  • 선택한 활동의 종료시간보다 빠른 시작 시간을 가지는 활동을 모두 제거한다.
  • 남은 활동들에 대해 앞의 과정을 반복한다.

탐욕 알고리즘의 필수 요소

  • 탐욕적 선택 속성
    • 탐욕적 선택은 최적해로 갈 수 있음을 보여라
    • 즉, 탐욕적 선택은 항상 안전하다.
  • 최적 부분 구조
    • 최적화 문제를 정형화하라
    • 하나의 선택을 하면 풀어야 할 하나의 하위 문제가 남는다.
  • 원문제의 최적해 = 탐욕적 선택 + 하위 문제의 최적해 임을 증명하라.
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Scanner;

public class MeetingRoomTest {
	static class Meeting implements Comparable<Meeting>{
		int start, end;


		public Meeting(int start, int end) {
			super();
			this.start = start;
			this.end = end;
		}


		@Override
		public int compareTo(Meeting o) {
			int value = this.end - o.end;
			if(value != 0) return value; // 종료시간이 다르면

			// 종료시간이 같다면 시작시간이 빠른 순서로.
			return this.start - o.start;
		}


		@Override
		public String toString() {
			return "Meeting [start=" + start + ", end=" + end + "]";
		}

	}
	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		int N = sc.nextInt(); //회의 개수

		Meeting[] meetings = new Meeting[N];
		for (int i = 0; i < N; i++) {
			meetings[i] = new Meeting(sc.nextInt(), sc.nextInt());
		}

		for(Meeting meeting: getSchedule(meetings)) {
			System.out.println(meeting);
		}
	}

	static ArrayList<Meeting> getSchedule(Meeting[] meetings){
		ArrayList<Meeting> list = new ArrayList<Meeting>();

		Arrays.sort(meetings); // 종료시간 기준 오름차순 정렬
		list.add(meetings[0]); // 첫회의 추가.

		for(int i=1,size=meetings.length; i<size; i++) {
			if(list.get(list.size()-1).end <= meetings[i].start) {
				list.add(meetings[i]);
			}
		}
		return list;
	}
}

분할 정복 기법

예시) 가짜 동전 찾기

  • n개의 동전들 중에 가짜 동전이 하나 포함되어 있다. 가짜 동전은 진짜 동전에 비해 아주 조금 가볍다. 진짜 동전들의 무게가 동일하다고 할때 양팔 저울을 이용해서 가짜 동전을 찾아보자
  • 양팔 저울을 최소로 사용해서 가짜 동전을 찾는 방법은 무엇인가?
  • 예를 들어, 동전이 24(진짜 23, 가짜 1)개 있다면?
  • 둘씩 쪼개어서 몸무게가 낮은 것을 비교해가면서 구한다.

분할정복 기법

  • 유래 : 1805년 12월 2일 아우스터리츠 전투에서 나폴레옹이 사용한 전략
  • 전력이 우세한 연합군을 공격하기 위해 나폴레옹은 연합군의 중앙부로 쳐들어가 연합군을 둘로 나눔
  • 둘로 나뉜 연합군을 한 부분씩 격파함
  • 설계 전략
    • 분할 : 해결할 문제를 여러 개의 작은 부분으로 나눈다.
    • 정복 : 나눈 작은 문제를 각각 해결한다.
    • 통합 : 필요하다면 해결된 해답을 모은다.

거듭제곱

  • 반복 알고리즘 : O(n)
  • 슈도코드
    Iterative_Power(x,n)
     result <- 1
     for i in l -> n
         result <= result * X
     RETURN result
    
  • 분할 정복 기반의 알고리즘 : O(log2n)
  • 슈도코드
    Recursive_Power(x,n)
     if n==1 : RETURN x
     if n is even
         y<- Recursive_Poser(x, n/2)
         RETURN y*y
     else
         y<- Recursive_Poser(x,(n-1)/2)
         RETURN y*y*x
    
  • C^n = C^n/2 * C^n/2(n은 짝수)
  • = C^(n-1)^2 * C^(N-1)^2 * C (n은 홀수)
public class SquareNumber {
	static int callCnt;
	
	//반복 알고리즘
	static long exp1(long x, long y) {
		callCnt++;
		if(y==1) return x;
		return x * exp1(x, y-1);
	}

	// 분할정복
	static long exp2(long x, long y) {
		callCnt++;
		if(y==1) return x;
		long r = exp2(x, y/2);
		long res = r * r;

		if(y%2==1) res *= x; //홀수일 떄
		return res;
	}
	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		int x = sc.nextInt();
		int y = sc.nextInt();

//		System.out.println(exp1(x,y));
//		System.out.println(callCnt);

		System.out.println(exp2(x,y));
		System.out.println(callCnt);
	}
}


이진 검색

  • 자료의 가운데에 있는 항목의 키 값과 비교하여 다음 검색의 위치를 결정하고 검색을 계속 진행하는 방법
  • 목적 키를 찾을 때까지 이진 검색을 순환적으로 반복 수행함으로써 검색 범위를 반으로 줄여가면서 보다 빠르게 검색을 수행함
  • 이진 검색을 하기 위해서는 자료가 정렬된 상태여야 한다.
  • O(log2N)의 시간복잡도걸린다.

병뚜껑 속의 숫자 게임

  • 술래가 병뚜껑 속 숫자를 확인한 후, 다음 사람부터 숫자르 맞히기 시작한다. 술래는 up 또는 down을 통해 게임에 참여한 사람들이 병뚜껑 속 숫자에 점점 가까워질 수 있도록 힌트를 제시한다.

검색과정

  • 자료의 중앙에 있는 원소를 고른다.
  • 중앙 원소의 값과 찾고자 하는 목표 값을 비교한다.
  • 중앙 원소의 값과 찾고자 하는 목표 값이 일치하면 탐색을 끝낸다.
  • 목표 값이 중앙 원소의 값보다 작으면 자료의 왼쪽 반에 대해서 새로 검색을 수행하고, 크다면 자료의 오른쪽 반에 대해서 새로 검색을 수행한다.
  • 찾고자 하는 값을 찾을 때까지 위의 과정을 반복한다.
  • 슈도코드
    binarySearch(S[], n, key)
     start <- 0 
     end <- n-1
    
     while start <= end
         mid <- (start + end)/2
         if S[mid] == key
             return mid
         elif S[mid] <key
             start <- mid + 1
         elif S[mid] > key
             end <- mid - 1
     END WHILE
     RETURN -1;
    
import java.util.Arrays;

public class BinarySearchTest {

	public static void main(String[] args) {
		int[] arr = {10,4,6,20,35,7};
		Arrays.sort(arr); //값들을 오름차순 정렬
		// 4,6,7,10,20,15
		System.out.println(binarySearch(arr,6));
		System.out.println(binarySearch(arr,35));
		System.out.println(binarySearch(arr,50));

		System.out.println(Arrays.binarySearch(arr,6));
		System.out.println(Arrays.binarySearch(arr,35));
		System.out.println(Arrays.binarySearch(arr,50));
	}
	//key에 해당하는 원소의 인덱스 리턴.
	static int binarySearch(int[] arr, int key) {
		int start = 0, end = arr.length-1;
		while(start<=end) {
			int mid = (start+end)/2; //중간 위치
			if(arr[mid] == key) {
				return mid;
			}else if(arr[mid]<key) {
				start = mid+1;
			}else {
				end = mid-1;
			}
		}
		//못찾았다면
		return -1;
	}
}

Leave a comment