동빈나의 이코테 강의를 듣고 작성한 포스트입니다.
그리디 알고리즘(탐욕법)
- 현재 상황에서 지금 당장 좋은 것만 고르는 방법
- 일반적인 그리디 알고리즘은 문제를 풀기 위한 최소한의 아이디어를 떠올릴 수 있는 능력을 요구
- 그리디 해법은 그 정당성 분석이 중요
- 현재 상황에서 단순히 가장 좋아 보이는 것을 반복적으로 선택하는 것으로 최적의 해를 구할 수 있는지 검토하는 과정이 꼭 필요함
정당성 분석
그리디 알고리즘 문제에서는 문제 풀이를 위한 최소한의 아이디어를 떠올리고 이것이 정당한지 검토할 수 있어야 한다.
예시 문제 1
[문제 상황]
루트 노드부터 시작하여 다른 노드로 이동할 때, 거쳐 가는 노드 값의 합을 최대로 만들고 싶다.
최적의 해는 무엇인가?
[문제 해결 아이디어]
1. 모든 경우를 다 따져보기
모든 경우를 다 따져보면
5, 7, 9 노드 순으로 이동할 때 21이라는 가장 큰 값을 얻는다는 것을 알 수 있다.
2. 그리디 알고리즘
그리디 알고리즘으로 푼다면
노드를 이동할 때, 지금 당장 좋은 것을 골라 이동하면 된다.
즉, 진행할 수 있는 노드 중 가장 큰 노드로 이동한다.
루트 노드 5에서는 7, 10, 8 중 가장 큰 10으로 이동하고 노드 10에서는 4, 3 중 가장 큰 4로 이동하는 것이다.
그렇다면 19라는 값을 얻게 되는데 이는 실제로 가장 큰 값은 아니다.
그리디 알고리즘 문제의 특징
위의 예시 문제를 통해 알 수 있는 그리디 알고리즘 문제의 특징은 다음과 같다.
- 일반적인 상황에서 그리디 알고리즘은 최적 해를 보장할 수 없을 때가 많다.
- 하지만 코테에서 대부분의 그리디 문제는 그리디 알고리즘으로 얻은 해가 최적 해가 되는 상황에서, 이를 추론할 수 있어야 풀리도록 출제한다.
- 즉, 코테에서 그리디 문제로 분류되는 문제는 그리디 알고리즘으로 얻은 해가 최적 해가 되는 경우에 출제되는 경우가 많다.
예시 문제 2: 거스름돈 동전의 개수
[문제 상황]
당신은 음식점의 계산을 도와주는 점원이다.
카운터에는 거스름돈으로 사용할 500원, 100원, 50원, 10원짜리 동전이 무한히 존재한다고 가정한다.
손님에게 거슬러 주어야 할 돈이 N원일 때 거슬러 주어야 할 동전의 최소 개수를 구하시오.
단, 거슬러 줘야 할 돈 N은 항상 10의 배수라서 거슬러 주지 못하는 경우는 없다.
[문제 해결 아이디어]
최적 해를 빠르게 구하기 위해서는 가장 큰 화폐 단위부터 돈을 거슬러 주면 된다.
- N원을 거슬러 줘야 할 때, 가장 먼저 500원으로 거슬러 줄 수 있을 만큼 거슬러 준다.
- 이후에 100원, 50원, 10원짜리 동전을 차례대로 거슬러 줄 수 있을 만큼 거슬러 주면 된다.
만약 N=1260이라면,
아래의 순서로 돈을 거슬러 주게 된다.
- Step 1) 500원 짜리
화폐 단위 | 500 | 100 | 50 | 10 |
손님이 받은 개수 | 2 | 0 | 0 | 0 |
- 잔돈: 1260 - 1000 = 260원
- Step 2) 100원 짜리
화폐 단위 | 500 | 100 | 50 | 10 |
손님이 받은 개수 | 2 | 2 | 0 | 0 |
잔돈: 260 - 200 = 60원
- Step 3) 50원 짜리
화폐 단위 | 500 | 100 | 50 | 10 |
손님이 받은 개수 | 2 | 2 | 1 | 0 |
잔돈: 60 - 50 = 10원
- Step 4) 10원 짜리
화폐 단위 | 500 | 100 | 50 | 10 |
손님이 받은 개수 | 2 | 2 | 1 | 1 |
잔돈: 10 - 10 = 0원
=> 총 6개의 동전으로 거슬러 줄 수 있다.
[정당성 분석]
가장 큰 화폐 단위부터 돈을 거슬러 주는 것이 최적 해를 보장하는 이유
- 가지고 있는 동전 중에서 큰 단위가 항상 작은 단위의 배수이므로 작은 단위의 동전들을 종합해 다른 해가 나올 수 없기 때문
- 만약 800원을 거슬러 줘야 하는데 화폐 단위가 500, 400, 100원이라면 위의 방법으로 최적 해를 얻을 수 없다.
- 위의 방법: 500 + 100 * 3 => 총 4개 동전 사용
- 최적 해: 400 * 2=> 총 2개 사용
- 만약 800원을 거슬러 줘야 하는데 화폐 단위가 500, 400, 100원이라면 위의 방법으로 최적 해를 얻을 수 없다.
[답안 코드]
n = 1260
count = 0
# 큰 단위의 화폐부터 차례대로 확인하기
array = [500, 100, 50, 10]
for coin in array:
count += n // coin # 해당 화폐로 거슬로 줄 수 있는 동전 개수 세기
n %= coin
print(count)
[시간 복잡도 분석]
- 화폐 종류가 K일 때, 소스코드의 시간 복잡도는 O(K)
- 시간 복잡도는 거슬러 줘야 하는 금액과 무관하고, 화폐 종류에만 영향을 받는다.
실전 문제: 큰 수의 법칙
[문제 상황]
동빈이의 큰 수의 법칙은 다양한 수로 이루어진 배열이 있을 때 주어진 수들을 M번 더하여 가장 큰 수를 만드는 법칙이다. 단, 배열의 특정한 인덱스(번호)에 해당하는 수가 연속해서 K번을 초과하여 더해질 수 없는 것이 이 법칙의 특징이다.
예를 들어 순서대로 [2, 4, 5, 4, 6]으로 이뤄진 배열이 있을 때, M=8이고 K=3이라고 가정하자. 이 경우 특정한 인덱스의 수가 연속해서 세 번까지만 더해질 수 있으므로 큰 수의 법칙에 따른 결과는 6+6+6+5+6+6+6+5인 46이 된다.
단, 서로 다른 인덱스에 해당하는 수가 같은 경우에도 서로 다른 것으로 간주한다.
배열의 크기 N, 숫자가 더해지는 횟수 M, 그리고 K가 주어질 때 동빈이의 큰 수의 법칙에 따른 결과를 출력하시오.
[문제 해결 아이디어]
가장 큰 수와 두 번째로 큰 수만 저장하면 된다.
가장 큰 수를 K번 더하고 두 번째로 큰 수를 한 번 더하는 연산을 반복한다.
[답안 코드 - 나의 풀이]
N, M, K = map(int, input().split())
array = list(map(int, input().split()))
sum = 0
cnt = 0
# 가장 큰 수와 두 번째로 큰 수 인덱스
first_idx = 0
second_idx = 0
# 배열 중 제일 큰 값의 인덱스 저장
for i in range(N):
if array[i] >= array[first_idx]:
first_idx = i
# 배열 중 두 번째로 큰 값의 인덱스 저장
for j in range(N):
if array[j] >= array[second_idx] and array[j] <= array[first_idx] and j != first_idx:
second_idx = j
for i in range(M):
if cnt == 3:
sum += array[second_idx]
cnt = 0
continue
sum += array[first_idx]
cnt += 1
print(sum)
[답안 풀이 - 교재 풀이]
# 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] # 두 번째로 큰 수
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억 이상처럼 커지면 시간 초과 판정을 받을 것이다.
수학적 아이디어를 이용하면 더 효율적으로 문제를 해결할 수 있다.
가장 큰 수와 두 번째로 큰 수가 더해질 때는 특정한 수열 형태로 반복해서 더해지는 특징이 있다. 이때, 반복되는 수열의 길이는 (K+1)이다. 수열이 반복되는 횟수는 M을 (K+1)로 나눈 몫이다. 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)
실전 문제: 숫자 카드 게임
[문제 상황]
숫자 카드 게임은 여러 개의 숫자 카드 중에서 가장 높은 숫자가 쓰인 카드 한 장을 뽑는 게임이다. 게임의 룰은 아래와 같다.
- 숫자가 쓰인 카드들이 N x M 형태로 놓여 있다. N은 행의 개수, M은 열의 개수이다.
- 먼저 뽑고자 하는 카드가 포함되어 있는 행을 고른다.
- 그다음 선택된 행에 포함된 카드들 중 가장 숫자가 낮은 카드를 뽑아야 한다.
- 따라서 처음에 카드를 골라낼 행을 선택할 때, 최종적으로 가장 높은 숫자의 카드를 뽑을 수 있도록 전략을 세워야 한다.
게임의 룰에 맞게 카드를 뽑는 프로그램을 만드시오.
[문제 해결 아이디어]
각 행에서 가장 작은 수를 찾고, 그들 중 가장 큰 수를 출력하면 된다.
[답안 코드 - 나의 풀이]
N, M = map(int, input().split())
minimum = []
for i in range(N):
row = list(map(int, input().split()))
row.sort()
minimum.append(row[0])
minimum.sort()
print(minimum[N-1])
[답안 코드 - 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중 for문을 이용한 풀이]
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)
실전 문제: 1이 될 때까지
[문제 상황]
어떠한 수 N이 1이 될 때까지 다음 두 과정 중 하나를 반복적으로 선택해 수행하려 한다.
- N에서 1을 뺀다.
- N을 K로 나눈다.
- 단, 두 번째 연산은 N이 K로 나눠질 때만 선택할 수 있다.
- 예를 들어 N=17, K=4
- 1번 과정을 한 번 수행하면 N은 16이 된다.
- 이후에 2번 과정을 두 번 수행하면 N은 1이 된다.
- 결과적으로 이 경우 전체 과정을 실행한 횟수는 3이 된다.
- 이는 N을 1로 만드는 최소 횟수다.
N과 K가 주어질 때 N이 1이 될 때까지 1번 혹은 2번의 과정을 수행해야 하는 최소 횟수를 구하는 프로그램을 작성하세요.
[문제 조건]
난이도 | ⭐️ |
풀이 시간 | 15분 |
시간 / 메모리 제한 | 2초 / 128MB |
[입력 조건]
- 첫째 줄에 N( 1 <= N <= 100,000)과 K(2 <= K <= 100,000)가 공백을 기준으로 하여 각각 자연수로 주어진다
[출력 조건]
- 첫째 줄에 N이 1이 될 때까지 1번 혹은 2번의 과정을 수행해야 하는 횟수의 최솟값을 출력한다.
- 입력 예시
25 5
- 출력 예시
2
[문제 해결 아이디어]
- 주어진 N에 대해 최대한 많이 나누기를 수행한다.
- N의 값을 줄일 때 2 이상의 수로 나누는 작업이 1을 빼는 작업보다 수를 훨씬 많이 줄일 수 있다.
[정당성 분석]
- 가능하면 최대한 많이 나누는 작업이 최적의 해를 항상 보장할 수 있을까?
- N이 아무리 큰 수여도, K로 계속 나눈다면 기하급수적으로 빠르게 줄일 수 있다.
- K가 2 이상이기만 하면, K로 나누는 것이 1을 빼는 것보다 항상 빠르게 N을 줄일 수 있다.
- 또한 N은 항상 1에 도달하게 된다.
[답안 코드 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)
[답안 코드 2]
문제에서는 N의 범위가 10만 이하이므로, 위의 답안 코드 1처럼 일일이 1씩 빼도 문제를 해결할 수 있다. 하지만 N이 100억 이상의 큰 수가 되는 경우를 가정했을 때에도 빠르게 동작하려면, N이 K의 배수가 되도록 효율적으로 한 번에 빼는 방식으로 소스코드를 작성할 수 있다.
n, k = map(int, input().split())
result = 0
while True:
# N이 K로 나눠 떨어지는 수가 될 때까지 빼기
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)
답안 코드 1과 비교하여 답안 코드 2는 시간 복잡도가 낮다.
반복문에서 답안 코드 1은 N이 K로 나눠 떨어질 때만 N을 K로 나누는데
답안 코드 2는 매 반복 마다 N을 K로 나누기 때문에 반복 횟수에 따라 N의 크기가 빠르게 줄어든다.
따라서 시간 복잡도를 로그 시간 복잡도로 얻을 수 있다.
예시 문제 4: 곱하기 혹은 더하기
[문제 설명]
각 자리가 숫자(0부터 9)로만 이루어진 문자열 S가 주어졌을 때,
왼쪽부터 오른쪽으로 하나씩 모든 숫자를 확인하며
숫자 사이에 'x' 혹은 '+' 연산자를 넣어 결과적으로 만들어질 수 있는 가장 큰 수를 구하는 프로그램을 작성하시오.
- 단, + 보다 x를 먼저 계산하는 일반적인 방식과는 달리, 모든 연산은 왼쪽에서부터 순서대로 이뤄진다고 가정
- 예를 들어 02984로 만들 수 있는 가장 큰 수
- ((((0 + 2) x 9) x 8) x 4) = 576
- 만들어질 수 있는 가장 큰 수는 항상 20억 이하의 정수가 되도록 입력이 주어진다.
- 일반적인 프로그래밍 언어에서 int 형을 사용할 경우 약 21억까지 값 생성할 수 있기 때문
[문제 조건]
난이도 | ⭐️ |
풀이 시간 | 30분 |
시간 / 메모리 제한 | 1초 / 128MB |
기출 | Facebook 인터뷰 |
[입력 조건]
- 첫째 줄에 여러 개 숫자로 구성된 하나의 문자열 S(1 <= S의 길이 <= 20)가 주어진다.
[출력 조건]
- 첫째 줄에 만들어질 수 있는 가장 큰 수를 출력한다.
- 입력 예시 1
02984
- 출력 예시 1
576
- 입력 예시 2
567
- 출력 예시 2
210
[문제 해결 아이디어]
- 대부분의 경우 '+' 보다는 'x'가 더 값을 크게 만든다.
- 단, 두 수 중 하나라도 0 혹은 1인 경우, 곱하기 보다는 더하기를 수행하는 것이 효율적이다.
- 따라서 두 수에 대해 연산을 수행할 때,
- 두 수 중 하나라도 1 이하인 경우에는 더하고
- 두 수가 모두 2 이상인 경우에는 곱하면 된다.
[오답 풀이]
S = sys.stdin.readline().rstrip()
result = 0
for s in S:
s_num = ord(s) - 48
if result == 0:
result += s_num
continue
if s_num == 0:
continue
else:
result *= s_num
print(result)
- 각 자리 수가 0일 때만 예외 처리 하고 1은 제외시켰기에 정답이 아님
- 0 뿐 아니라 1일 때에도 곱셈이 아닌 덧셈 처리를 해주어야 함
- 수를 문자열로 받아 각 문자를 조회
- 문자열로 받아왔기 때문에, 아스키코드를 이용해 다시 정수형으로 변환
- 지금까지 연산 결과가 0인 경우에는 현재의 수를 더해줌
- 현재 자리의 수가 0인 경우에는 continue(0을 더하는 것은 무시 가능하므로)
- 0이 아니면 지금까지 연산 결과에 현재 자리 수를 곱함
[답안 코드 1] - 나의 코드 수정
0과 1인 경우에 덧셈 하도록 수정
import sys
S = sys.stdin.readline().rstrip()
result = int(S[0])
for s in S:
s_num = ord(s) - 48
if s_num <= 1 or result <= 1:
result += s_num
else:
result *= s_num
print(result)
[답안 코드 2] - 동빈님 코드
import sys
data = input() # 문자열로 받음
# 첫 번째 문자를 숫자로 변경하여 대입
result = int(data[0])
for i in range(1, len(data)):
# 두 수 중 하나라도 0 또는 1인 경우, 더하기 수행
num = int(data[i])
if num <= 1 or result <= 1:
result += num
else:
result *= num
print(result)
예시 문제 5: 모험가 길드
[문제 설명]
한 마을에 모험가가 N명 있다.
- 모험가 길드에서는 N명의 모험가를 대상으로 '공포도'를 측정했다.
- 공포도가 높은 모험가는 위험 상황에서 대처 능력이 떨어진다.
- 공포도가 X인 모험가는 반드시 X명 이상으로 구성한 모험가 그룹에 참여해야 한다.
N명의 모험가로 여행을 떠날 수 있는 그룹 수의 최댓값을 구하는 프로그램을 작성하시오.
(단, 모든 모험가를 특정 그룹에 넣을 필요는 없다.)
예를 들어 N=5이고, 각 모험가의 공포도가 다음과 같다고 가정하자.
2 3 1 2 2
- 이 경우 그룹 1에 공포도가 1, 2, 3인 모험가를 한 명씩 넣고, 그룹 2에 공포도가 2인 나머지 두 명을 넣으면 총 2개의 그룹을 만들 수 있다.
[문제 조건]
난이도 | ⭐️ |
풀이 시간 | 30분 |
시간 / 메모리 제한 | 1초 / 128MB |
기출 | 핵심 유형 |
[입력 조건]
- 첫째 줄에 모험가의 수 N이 주어진다. (1 <= N <= 100,000)
- 둘째 줄에 각 모험가의 공포도 값이 N 이하의 자연수로 주어지며, 각 자연수는 공백으로 구분한다.
[출력 조건]
- 여행을 떠날 수 있는 그룹 수의 최댓값을 출력한다.
- 입력 예시 1
5
2 3 1 2 2
- 출력 예시 1
2
[문제 해결 아이디어 1 - 나]
공포도가 작은 모험가는 적은 인원으로 많은 그룹을 만들 수 있음
따라서 공포도가 작은 모험가부터 그룹을 결성해야 최대한 많은 그룹을 이룰 수 있음
- 공포도 리스트를 작은 수부터 오름차순 정렬한 후 처리함
- 공포도가 큰 모험가는 불필요하게 많은 인원을 낭비시키므로 제외하는 것이 나음
시간 복잡도를 최소화 하기 위해, 전체를 단 한 번만 순회하도록 설계
순회하며 그룹 내 최대 공포도가 커지는 경우에는 그룹에 필요한 인원 수를 늘려줘야 함
=> 현 그룹의 최대 공포도는 그룹 결성에 필요한 인원 수와 같음
- 현 그룹의 인원 수를 관리하는 count 변수와 현 그룹의 최대 공포도인 big 변수를 운용
- 순회하며 현재 공포도가 big에 저장된 공포도 보다 큰 경우에
- big을 현재 공포도로 업데이트 하고
- 현 그룹에 한 명을 추가했다는 의미로 count를 증가시킴
- 현재 공포도가 big에 저장된 공포도와 같은 경우
- 현 그룹에 한 명을 추가했다는 의미로 count를 증가시킴
- 현 그룹의 인원 수인 count와 현 그룹의 최대 공포도인 big이 같아지면
- 새로운 그룹이 하나 결성되었기 때문에 group을 증가시키고
- 현 그룹의 인원 수인 count를 0으로 초기화 시킴
- 순회하며 현재 공포도가 big에 저장된 공포도 보다 큰 경우에
[답안 코드 1] - 나의 코드
import sys
N = sys.stdin.readline().rstrip()
fear = list(map(int, sys.stdin.readline().split()))
group = 0 # 총 그룹의 수
big = 0 # 현 그룹의 최대 공포도
count = 0 # 현 그룹의 인원 수
fear.sort() # 오름차순 정렬
for i in range(len(fear)):
if big < fear[i]: # 제일 큰 공포도가 바뀌었을 때
big = fear[i]
count += 1
else: # 공포도 같을 때
count += 1
if count == big:
group += 1
count = 0
print(group)
[문제 해결 아이디어 2 - 동빈님]
나의 코드와 달리, 현 그룹의 최대 공포도에 대한 big 변수가 없다.
알고리즘을 조금 수정하면 big 변수를 count 변수로 대체할 수 있기 때문이다.
- 오름차순 정렬 이후에 공포도가 가장 낮은 모험가부터 하나씩 확인한다.
- 앞에서부터 공포도를 하나씩 확인하며
- '현재 그룹에 포함된 모험가의 수'가 '현재 확인하고 있는 공포도'보다 크거나 같다면 이를 그룹으로 설정한다.
- 이러한 방법을 이용하면 공포도가 오름차순으로 정렬되어 있다는 점에서, 항상 최소한의 모험가의 수만 포함해 그룹을 결성하게 된다.
[답안 코드 2] - 동빈님 코드
n = int(input())
data = list(map(int, input().split()))
data.sort()
result = 0 # 총 그룹의 수
count = 0 # 현재 그룹에 포함된 모험가의 수
for i in data: # 공포도를 낮은 거부터 하나씩 확인하며
count += 1 # 현재 그룹에 해당 모험가를 포함시키기
if count >= i: # 현재 그룹에 포함된 모험가의 수 >= 현재 공포도: 그룹 결성
result += 1 # 총 그룹의 수 증가시킴
count = 0 # 현재 그룹에 포함된 모험가 수 초기화
print(result) # 총 그룹의 수 출력
'Algorithm > 알고리즘' 카테고리의 다른 글
[휴학 중 코테 부수기] # 4. 그래프 탐색 알고리즘: DFS/BFS (2) | 2023.11.06 |
---|---|
[휴학 중 코테 부수기] # 3. 구현(Implementation) (1) | 2023.11.04 |
[BOJ](Python) 백준 2075번 N번째 큰 수 (0) | 2023.09.26 |
[알고리즘](Python) 그래프 개념과 파이썬 코드 구현 (3) | 2023.09.19 |
[알고리즘](Python) 백준 2696번 중앙값 구하기 - 우선순위 큐 (0) | 2023.09.19 |