1-1. Greedy 개념 & 실전 문제

Speedwell🍀·2021년 12월 24일
0

그리디(Greedy)

'현재 상황에서 지금 당장 좋은 것만 고르는 방법'

매 순간 가장 좋아보이는 것을 선택 ⇒ 현재의 선택이 나중에 미칠 영향은 고려 안함

'사전에 외우고 있지 않아도 풀 수 있을 가능성이 높은 문제 유형'

정렬, 최단 경로 등의 알고리즘 유형은 사용 방법을 정확히 알고 있어야 함

그리디 알고리즘은 다익스트라 알고리즘과 같은 특이 케이스를 제외하고는 단순 암기를 통해 대처하긴 어렵

"창의력" 요구하는 문제 유형 ⇒ 문제를 풀기 위한 최소한의 아이디어를 떠올릴 수 있는 능력 요구

문제에서 '가장 큰 순서대로', '가장 작은 순서대로' 같은 기준을 알게 모르게 제시

대체로 정렬 알고리즘과 짝을 이뤄 출제됨


거스름돈 예제

당신은 음식점의 계산을 도와주는 점원이다. 카운터에는 거스름돈으로 사용할 500원, 100원, 50원, 10원짜리 동전이 무한히 존재한다고 가정한다. 손님에게 거슬러 줘야 할 돈이 N원일 때 거슬러줘야 할 동전의 최소 개수를 구하라. 단, 거슬러 줘야 할 돈 N은 항상 10의 배수이다.

n = 1260
count = 0
# 큰 단위의 화폐부터 차례대로 확인
coin_types = [500, 100, 50, 10]
for coin in coin_types:
	count += n // coin # 해당 화폐로 거슬러 줄 수 있는 동전의 개수 세기
	n %= coin
print(count)

'화폐의 종류'만큼 반복을 수행해야 한다.

화폐의 종류가 K개 ⇒ 시간 복잡도 O(K)

즉, 이 알고리즘의 시간 복잡도는 동전의 총 종류에만 영향 받고, 거슬러 줘야 하는 금액의 크기와는 무관.


그리디 알고리즘의 정당성

대부분의 문제는 그리디 알고리즘을 이용했을 때 '최적의 해'를 찾을 수 없을 가능성 다분.

탐욕적으로 문제에 접근했을 때 정확한 답을 찾을 수 있다는 보장이 있을 때는 매우 효과적/직관적

그리디 알고리즘으로 문제의 해법을 찾았을 때, 해법이 정당한지 검토해야 함

위의 거스름돈 예제에 그리디 알고리즘을 적용할 수 있는 이유는 '가지고 있는 동전 중에서 큰 단위가 항상 작은 단위의 배수이므로 작은 단위의 동전들을 종합해 다른 해가 나올 수 없기 때문'

동전의 단위가 서로 배수 형태가 아니라 무작위로 주어진 경우엔 그리디 알고리즘 사용 불가

예를 들어 800원 거슬러 줘야 하는데, 화폐 단위가 500원, 400원, 100원인 경우

그리디 알고리즘 적용 ⇒ 4개의 동전(500원 + 100원 + 100원 + 100원)

최적의 해 ⇒ 2개의 동전(400원 + 400원)

결론: 대부분의 그리디 알고리즘 문제에서는 이처럼 문제 풀이를 위한 최소한의 아이디어를 떠올리고 이것이 정당한지 검토할 수 있어야 답을 도출할 수 있다.

어떤 코딩 테스트 문제를 만났을 때, 바로 문제 유형을 파악하기 어렵다면 그리디 알고리즘 의심하고, 문제를 해결할 수 있는 탐욕적인 해결법이 존재하는지 고민 ⇒ 찾을 수 없다면, 다이나믹 프로그래밍이나 그래프 알고리즘 등으로 고민



실전 문제

1. 큰 수의 법칙

난이도: ⭐
풀이 시간: 30분
시간 제한: 1초
메모리 제한: 128MB
기출: 2019 국가 교육기관 코딩 테스트


다양한 수로 이루어진 배열이 있을 때 주어진 수들을 M번을 더하여 가장 큰 수를 만드는 법칙.

단, 배열의 특정한 인덱스에 해당하는 수가 연속해서 K번을 초과하여 더해질 수 없음.

예1) 순서대로 2, 4, 5, 4, 6으로 이루어진 배열이 있을 때 M이 8이고, K가 3이라고 가정. 이 경우 특정한 인덱스의 수가 연속해서 3번까지만 더해질 수 있으므로 큰 수의 법칙에 따른 결과는 6+6+6+5+6+6+6+5인 46

단, 서로 다른 인덱스에 해당하는 수가 같은 경우에도 서로 다른 것으로 간주.

예2) 순서대로 3, 4, 3, 4, 3으로 이루어진 배열이 있을 때 M이 7이고, K가 2라고 간주. 이 경우 두 번째 원소에 해당하는 4와 네 번째 원소에 해당하는 4를 번갈아 두 번씩 더하는 것이 가능. 결과적으로 4+4+4+4+4+4+4인 28


배열의 크기 N, 숫자가 더해지는 횟수 M, 그리고 K가 주어질 때 동빈이의 큰 수의 법칙에 따른 결과를 출력하시오.

입력 조건

  • 첫째 줄에 N(2≤N≤1000), M(1≤M≤10000), K(1≤K≤10000)의 자연수가 주어지며, 각 자연수는 공백으로 구분.
  • 둘째 줄에 N개의 자연수가 주어진다. 각 자연수는 공백으로 구분. 단, 각각의 자연수는 1 이상 10000 이하의 수로 주어진다.
  • 입력으로 주어지는 K는 항상 M보다 작거나 같다.

출력 조건

  • 첫째 줄에 동빈이의 큰 수의 법칙에 따라 더해진 답을 출력한다.
# 입력 예시
5 8 3
2 4 5 4 6

# 출력 예시
46

<내가 쓴 답>

n, m, k = map(int, input().split())
data = list(map(int, input().split()))


list.sort(reverse=True)
    
result = list[0] * ((m // k)*k) + list[1] * (m % k)
print(result)

<해설>

"반복되는 수열"에 대해 파악 필요. 위의 예시에선 수열 {6, 6, 6, 5}가 반복됨.

수열의 길이 : (K+1)

수열이 반복되는 횟수 : (M / (K+1)) ⇒ 여기에 K를 곱해주면 가장 큰 수가 등장하는 횟수

이때 M이 K+1로 나누어 떨어지지 않는 경우도 고려해야 함. ⇒ M을 (K+1)로 나눈 나머지만큼 가장 큰 수가 추가로 더해짐.

가장 큰 수가 더해지는 횟수 : int(M / (K + 1)) * K + M % (K + 1)


<최종 답안>

# N, M, K를 공백을 기준으로 구분하여 입력 받기
n, m, k = map(int, input().split())
# N개의 수를 공백을 기준으로 구분하여 입력 받기
data = list(map(int, input().split()))

data.sort() # 입력 받은 수들 정렬하기
first = data[n - 1] # 가장 큰 수
second = data[n - 2] # 두 번째로 큰 수

# 가장 큰 수가 더해지는 횟수 계산
count = int(m / (k + 1)) * k
count += m % (k + 1)

result = 0
result += (count) * first # 가장 큰 수 더하기
result += (m - count) * second # 두 번째로 큰 수 더하기

print(result) # 최종 답안 출력


2. 숫자 카드 게임

난이도: ⭐
시간 제한: 1초
메모리 제한: 128MB
기출: 2019 국가 교육기관 코딩 테스트


숫자 카드 게임은 여러 개의 숫자 카드 중에서 가장 높은 숫자가 쓰인 카드 한 장을 뽑는 게임이다.
단, 게임의 룰을 지키며 카드를 뽑아야 하고 룰은 다음과 같다.

  1. 숫자가 쓰인 카드들이 N x M 형태로 놓여 있다. 이때 N은 행의 개수를 의미하며, M은 열의 개수를 의미한다.
  2. 먼저 뽑고자 하는 카드가 포함되어 있는 행을 선택한다.
  3. 그다음 선택된 행에 포함된 카드들 중 가장 숫자가 낮은 카드를 뽑아야 한다.
  4. 따라서 처음에 카드를 골라낼 행을 선택할 때, 이후에 해당 행에서 가장 숫자가 낮은 카드를 뽑을 것을 고려하여 최종적으로 가장 높은 숫자의 가드를 뽑을 수 있도록 전략을 세워야 한다.

카드들이 N x M 형태로 놓여 있을 때, 게임의 룰에 맞게 카드를 뽑는 프로그램을 만드시오.


입력 조건

  • 첫째 줄에 숫자 카드들이 행의 개수 N과 열의 개수 M이 공백을 기준으로 하여 각자 자연수로 주어진다. (1 <= N, M <= 100)
  • 둘째 줄부터 N개의 줄에 걸쳐 각 카드에 적힌 숫자가 주어진다. 각 숫자는 1 이상 10,000 이하의 자연수이다.

출력 조건

  • 첫째 줄에 게임의 룰에 맞게 선택한 카드에 적힌 숫자를 출력한다.
# 입력 예시 1
3 3
3 1 2
4 1 4
2 2 2

# 출력 예시 1
2
# 입력 예시 2
2 4
7 3 1 8
3 3 3 4

# 출력 예시 2
3

문제를 푸는 아이디어 ➡️ 각 행마다 가장 작은 수를 찾은 뒤에 그 중에서 가장 큰 수


<답안1> min() 함수 이용

# N, M을 공백으로 구분하여 입력받기
n, m = map(int, input().split())

result = 0

# 한 줄씩 입력받아 확인
for i in range(n):
	data = list(map(int, input().split()))
    # 현재 줄에서 가장 작은 수 찾기
    min_value = min(data)
    # 가장 작은 수 중에서 가장 큰 수 찾기
    result = max(result, min_value)

print(result)

<답안2> 2중 반복문 구조 이용

# N, M을 공백으로 구분하여 입력받기
n, m = map(int, input().split())

result = 0

# 한 줄씩 입력받아 확인
for i in range(n):
	data = list(map(int, input().split()))
    # 현재 줄에서 가장 작은 수 찾기
    min_value = 10001
    for a in data:
    	min_value = min(min_value, a)
    # 가장 작은 수 중에서 가장 큰 수 찾기
    result = max(result, min_value)

print(result)


3. 1이 될 때까지

난이도: ⭐
시간 제한: 1초
메모리 제한: 128MB
기출: 2018 E 기업 알고리즘 대회


어떤 수 N이 1이 될 때까지 다음의 두 과정 중 하나를 반복적으로 선택하여 수행하려고 한다. 단, 두 번째 연산은 N이 K로 나누어떨어질 때만 선택할 수 있다.

  1. N에서 1을 뺀다.
  2. N을 K로 나눈다.

예를 들어 N이 17, K가 4라고 가정하자. 이때 1번의 과정을 한 번 수행하면 N은 16이 된다. 이후에 2번의 과정을 두 번 수행하면 N은 1이 된다. 결과적으로 이 경우 전체 과정을 실행한 횟수는 3이 된다. 이는 N을 1로 만드는 최소 횟수이다.


N과 K가 주어질 때 N이 1이 될 때까지 1번 혹은 2번의 과정을 수행해야 하는 최소 횟수를 구하는 프로그램을 작성하시오.

입력 조건

  • 첫째 줄에 N(2 <= N <= 100,000)과 K(2 <= K <= 100,000)가 공백으로 구분되며 각각 자연수로 주어진다. 이때 입력으로 주어지는 N은 항상 K보다 크거나 같다.

출력 조건

  • 첫째 줄에 N이 1이 될 때까지 1번 혹은 2번의 과정을 수행해야 하는 횟수의 최솟값을 출력한다.
# 입력 예시
25 5

# 출력 예시
2

문제를 푸는 아이디어 ➡️ 최대한 많이 나누기

다음의 과정을 반복할 수 없을 때까지 반복하면 된다.

  1. N이 K의 배수가 될 때까지 1씩 빼기
  2. N을 K로 나누기

❓위의 방법이 빠르게 동작하면서 동시에 최적의 해를 보장한다는 것은 어떻게 알 수 있을까?
✔️ N이 클수록 K로 나누었을 때 줄어드는 양이 더 많다. 그러므로 K로 최대한 많이 나눌 수 있도록 하는 것이 최적의 해를 보장하는 것이다.


<답안1> 단순하기 풀기

n, k = map(int, input().split())
result = 0

# N이 K 이상이라면 K로 계속 나누기
while n >= k:
	# N이 K로 나누어 떨어지지 않는다면 N에서 1씩 빼기
    when n % k != 0:
    	n -= 1
        result += 1
    # K로 나누기
    n //= k
    result += 1
    
# 마지막으로 남은 수에 대하여 1씩 빼기
while n > 1:
	n -= 1
    result += 1

print(result)

<답안2> N이 K의 배수가 되도록 효율적으로 한 번에 빼기

n, k = map(int, input.split())
result = 0

while True:
	# (N == K로 나누어떨어지는 수)가 될 때까지 1씩 빼기
    target = (n // k) * k
    result += (n - target)
    n = target
    # N이 K보다 작을 때(더 이상 나눌 수 없을 때) 반복문 탈출
    if n < k:
    	break
    # K로 나누기
    result += 1
    n //= k

# 마지막으로 남은 수에 대하여 1씩 빼기
result += (n - 1)
print(result)

0개의 댓글