본문 바로가기
Main/Algorithm

2. Greedy Algorithm(그리디 알고리즘) 실전문제

by 임형욱의 블로그 2022. 1. 30.

※ 본 자료는 '이것이 코딩 테스트다 with Python - 나동빈' 서적을 바탕으로 학습하기 위해 작성하였습니다.※

 


1.

<큰 수의 법칙>

기출: 2019 국가 교육기관 코딩 테스트

 

'큰 수의 법칙'은 일반적으로 통계 분야에서 다루어지는 내용이지만, 이 책에서는 색다른 방식으로 다르게 사용하고 있습니다. 이 책에서는 다양한 수로 이루어진 배열이 있을 때 주어진 수들을 M번 더하여 가장 큰 수를 만드는 법칙 입니다.

단, 배열의 특정한 인덱스(번호)에 해당하는 수가 연속해서 K번을 초과하여 더해질 수 없는 것이 이 법칙의 특징입니다.

 

예를 들어 순서대로 2, 4, 5, 4, 6으로 이루어진 배열이 있을 때 M이 8이고, K가 3이라고 가정해봅시다.

이 경우 특정한 인덱스의 수가 연속해서 세 번까지만 더해질 수 있으므로 큰 수의 법칙에 따른 결과는 

6+6+6+5+6+6+6+5인 46이 됩니다. 단, 서로 다른 인덱스에 해당하는 수가 같은 경우에도 서로 다른 것으로 간주합니다.

 

예를 들어 순서대로 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 <= 10,000), K(1 <= K <= 10,000)의 자연수가 주어지며, 각 자연수는 공백으로 구분한다.
    • 둘째 줄에 N개의 자연수가 주어진다. 각 자연수는 공백으로 구분한다. 단, 각각의 자연수는 1 이상 10,000 이하의 수로 주어진다.
    • 입력으로 주어지는 K는 항상 M보다 작거나 같다.
  • 출력 조건
    • 첫째 줄에 동빈이의 큰 수의 법칙에 따라 더해진 답을 출력한다.
# N, M, K를 공백으로 구분하여 입력받기
n, m, k = map(int, input().split())
# N개의 수를 공백으로 구분하여 입력받기
data = list(map(int, input().split()))

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

result = 0

while True:
    for i in range(k):# 가장 큰 수를 K번 더하기
        if m == 0: # m이 0이라면 반복문 탈출
            break
        result += first
        m -= 1 # 더할 때마다 1씩 빼기
    if m == 0:# m이 0이라면 반복문 탈출
        break
    result += second # 두 번째로 큰 수를 한 번 더하기
    m -= 1 # 더할 때마다 1식 빼기
    
print(result) # 최종 답안 출력

 

이 문제는 M이 10,000 이하이므로 이 방식으로도 문제를 해결할 수 있지만, M의 크기가 100억 이상처럼 커진다면 시간 초과 판정을 받을 것입니다. 간단한 수학적 아이디어를 이용해 더 효율적으로 문제를 해결해봅시다.

예를 들어 N이 5이고 입력값이 다음과 같이 주어졌다고 가정합시다. 

2 4 5 4 6

가장 큰 수: 6, 두 번째로 큰 수: 5

이때 M이 8이고, K가 3이라면 다음과 같이 더했을 때 합을 최대로 할 수 있습니다. 

다시 말해 ( 6 + 6 + 6 + 5) + (6 + 6 + 6 + 5)로 정답은 46이 됩니다.

 

이 문제를 풀려면 가장 먼저 반복되는 수열에 대해서 파악해야 합니다. 가장 큰 수와 두 번째로 큰 수가 더해질 때는 특정한 수열 형태로 일정하게 반복해서 더해지는 특징이 있습니다. 위의 예시에서는 수열 {6,6,6,5}가 반복됩니다.

그렇다면 반복되는 수열의 길이는 어떻게 될까요? 바로 (K + 1)로 위의 예시에서는 4가 됩니다.

따라서 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] # 가장 큰 수 , 이유: sort
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.

<숫자 카드 게임>

2019 국가 교육기관 코딩 테스트

 

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

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

예를 들어 3 X 3 형태로 카드들이 다음과 같이 놓여 있다고 가정해봅시다.

3           1             2

 4           1             4 

 2           2             2 

여기서 카드를 골라낼 행을 고를 때 첫 번째 혹은 두 번째 행을 선택하는 경우, 최종적으로 뽑는 카드는 1입니다.

하지만 세 번째 행을 선택하는 경우 최종적으로 뽑는 카드는 2입니다. 따라서 이 예제에서는 세 번째 행을 선택하여 숫자 2가 쓰여진 카드를 뽑는 것이 정답입니다. 

 

<실전문제> 카드들이 N X M 형태로 놓여 있을 때, 게임의 룰에 맞게 카드를 뽑는 프로그램을 만드시오.

  • 입력 조건
    • 첫째 줄에 숫자 카드들이 놓인 행의 개수 N과 열의 개수 M이 공백을 기준으로 하여 각각 자연수로 주어진다.    (1 <= N, M <= 100)
    • 둘째 줄부터 N개의 줄에 걸쳐 각 카드에 적힌 숫자가 주어진다. 각 숫자는 1 이상 10,000 이하의 자연수이다.
  • 출력 조건
    • 첫째 줄에 게임의 룰에 맞게 선택한 카드에 적힌 숫자를 출력한다.

min() 함수를 이용하는 답안 예시

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중 반복문 구조를 이용하는 답안 예시

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)

<문제 해설>

이 문제를 푸는 아이디어는 바로 "각 행마다 가장 작은 수를 찾은 뒤에 그 수 중에서 가장 큰 수"를 찾는 것입니다.

이 문제는 문제 설명이 길어서 지문 이해에 시간이 많이 소요될 수 있지만, 문제의 아이디어를 떠올리는 것은 쉬운 문제에 속합니다.

입력 조건에서 입력으로 들어오는 수는 모두 10,000 이하이므로 단순히 배열에서 가장 작은 수를 찾는 기본 문법을 이용하여 각 행에서 가장 작은 수를 찾은 다음 그 수 중에서 가장 큰 수를 찾는 방식으로 문제를 해결할 수 있습니다.

이 문제는 앞서 다루었던 "큰 수의 법칙"문제보다 난이도가 낮습니다. 다만, 이 문제를 해결하기 위해서는 리스트에서 가장 작은 원소를 찾아주는 min() 함수를 이용할 수 있거나, 2중 반복문 구조를 이용할 수 있어야 합니다.


3.

<1이 될 때까지>

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번의 과정을 수행해야 하는 횟수의 최솟값을 출력한다.

주어진 N에 대하여 최대한 많이 나누기를 수행하면 됩니다.

N의 값을 줄일 때 2 이상의 수로 나누는 작업이 1을 빼는 작업보다 수를 훨씬 많이 줄일 수 있습니다.

가능하면 최대한 많이 나누는 작업이 최적의 해를 보장할 수 있을까요?

N이 아무리 큰 수여도, K로 계속 나눈다면 기하급수적으로 빠르게 줄일 수 있습니다.

다시 말해 K가 2 이상이기만 하면, K로 나누는 것이 1을 빼는 것보다 항상 빠르게 N을 줄일 수 있습니다.

또한 N은 항상 1에 도달하게 됩니다. (최적의 해 성립)

 

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

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

문제에서는 N의 범위가 10만 이하이므로, 이처럼 일일일 1씩 빼도 문제를 해결할 수 있습니다. 하지만 N이 100억 이상의 큰 수가 되는 경우를 가정했을 때에도 빠르게 동작하려면, N이 K의 배수가 되도록 효율적으로 한 번에 빼는 방식으로 소스코드를 작성할 수 있습니다.

n, k = map(int, input().split())
result = 0 # 총 연산을 수행하는 횟수

while True:
    # (N == k로 나누어떨어지는 수)가 될 때까지 1씩 빼기
    target = (n // k ) * k 
    # 만약 N이 K로 나누어떨어지지 않는 다고 할 때,
    # 가장 가까운 K로 나누어떨어지는 수가 어떤건지 찾고자 할 때 사용할 수 있습니다.
    result += (n - target)
    n = target
    # N이 K보다 작을 때(더 이상 나눌 수 없을 때) 반복문 탈출
    if n < k:
        break
    result += 1
    n //= k

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

 

 

'Main > Algorithm' 카테고리의 다른 글

1. Greedy Algorithm(그리디 알고리즘)  (0) 2022.01.28

댓글