본문 바로가기

Algorithm/알고리즘

[휴학 중 코테 부수기] # 2. 그리디 알고리즘

반응형

동빈나의 이코테 강의를 듣고 작성한 포스트입니다.

 

그리디 알고리즘(탐욕법)

  • 현재 상황에서 지금 당장 좋은 것만 고르는 방법
  • 일반적인 그리디 알고리즘은 문제를 풀기 위한 최소한의 아이디어를 떠올릴 수 있는 능력을 요구
  • 그리디 해법은 그 정당성 분석이 중요
    • 현재 상황에서 단순히 가장 좋아 보이는 것을 반복적으로 선택하는 것으로 최적의 해를 구할 수 있는지 검토하는 과정이 꼭 필요함

정당성 분석

그리디 알고리즘 문제에서는 문제 풀이를 위한 최소한의 아이디어를 떠올리고 이것이 정당한지 검토할 수 있어야 한다.

예시 문제 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개 사용

[답안 코드]

n = 1260
count = 0

# 큰 단위의 화폐부터 차례대로 확인하기
array = [500, 100, 50, 10]

for coin in array:
    count += n // coin # 해당 화폐로 거슬로 줄 수 있는 동전 개수 세기
    n %= coin

print(count)

[시간 복잡도 분석]

  • 화폐 종류가 K일 때, 소스코드의 시간 복잡도는 O(K)
  • 시간 복잡도는 거슬러 줘야 하는 금액과 무관하고, 화폐 종류에만 영향을 받는다.

예시 문제 3: 1이 될 때까지

[문제 상황] 

어떠한 수 N이 1이 될 때까지 다음 두 과정 중 하나를 반복적으로 선택해 수행하려 한다.

  1. N에서 1을 뺀다.
  2. 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]

import sys

N, K = map(int, sys.stdin.readline().split())

count = 0

while N != 1:
    if N % K == 0:
        N //= K
        count += 1
    else:
        N -= 1
        count += 1

print(count)

[답안 코드 2]

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으로 초기화 시킴

[답안 코드 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) # 총 그룹의 수 출력

 

반응형