읽기 전

  • 불필요한 코드나 잘못 작성된 내용에 대한 지적은 언제나 환영합니다.
  • 개인적으로 배운 점을 정리한 글입니다.

문제 링크

문제 1149.RGB 거리

문제 풀이

n번째 집에 칠해질 색의 경우의 수는 세 가지로 각 색이 칠해졌을 경우의 비용을 계산하면서 이후 문제를 해결해야 한다.

n번째 집에 R을 칠하려면 n - 1번째 집에 G나 B가 칠해졌을 경우이고 G를 칠하려면 R이나 B를, B를 칠하려면 R이나 G를 칠한 상태여야 한다. 각 경우에 대해 산출하고 그 최솟값을 각각 n 번째 집에 R, G, B를 칠한 경우에 해당하는 변수로 저장하여 다음 문제로 진행한다.

  • 초기값 R, G, B는 1번째 집의 r, g, b 값이다.
  • 이후 R은 min(G + r, B + r)이고 G는 min(R + g, B + g)이며 B는 min(R + b, G + b)이다.
  • N번째 집까지 모두 완료하면 세가지 경우 중 최솟값을 출력한다.

python 코드

import sys

input = sys.stdin.readline

N = int(input())
R, G, B = map(int, input().split())
for _ in range(2, N + 1):
    r, g, b = map(int, input().split())
    R, G, B = min(G + r, B + r), min(R + g, B + g), min(R + b, G + b)
print(min(R, G, B))

'Algorithms > Baekjoon' 카테고리의 다른 글

BOJ #2156 포도주 시식  (0) 2021.07.26
BOJ #1463 1로 만들기  (0) 2021.07.26
BOJ #3020 개똥벌레  (0) 2021.07.19
BOJ #2143 두 배열의 합  (0) 2021.07.17
BOJ #12015 가장 긴 증가하는 부분 수열 2  (0) 2021.07.15

읽기 전

  • 불필요한 코드나 잘못 작성된 내용에 대한 지적은 언제나 환영합니다.
  • 개인적으로 사용해보면서 배운 점을 정리한 글입니다.

다이나믹 프로그래밍(Dynamic Programming, 동적 계획법)이란?

문제를 작은 문제로 나누어 해결된 결과를 저장한 뒤 나중에 등장할 큰 문제의 해결을 위해 기존 해결해뒀던 결과들을 조합하여 풀이하는 알고리즘이다. 문제의 최적 해결 방법이 부분 문제에 대한 최적 해결 방법으로 구성되는 경우의 문제라면 최적 부분 구조(Optimal Substructure)를 갖는다고 말하는데 해당 속성을 갖는 문제를 풀이할 수 있다. 최적 부분 구조의 문제를 해결하는 알고리즘으로 그리디 알고리즘과 분할 정복이 있는데 그리디 알고리즘은 "매 순간 최적이라 생각되는 답을 선택"하면서 문제를 해결하는 반명 다이나믹 프로그래밍은 "중복된 하위 문제(Overlapping Subproblem)의 결과를 저장"했다가 이후 문제 풀이에 사용한다는 차이점이 있다. 핵심은 "중복된 하위 문제"로 중복되지 않은 하위 문제로 구성되었다면 분할 정복으로 해결한다고 말한다.

Algorithm_Dynamic_Programming_001

그리디는 그 순간의 최적해만 고려한다는 점, 분할 정복은 중복된 하위 문제가 아니므로 단위만 작아졌을 분 답이 매번 달라 매번 해결해야 하는 차이가 있다.

다이나믹 프로그래밍 방법론

동적 계획법 구현에는 2 가지가 있는데 대표적 DP 문제인 피보나치 수열로 예시를 들어보자.

Algorithm_Dynamic_Programming_002

상향식(Bottom-Up) - 타뷸레이션(Tabulation)

더 작은 하위 문제부터 정답을 도출하고 작은 문제의 정답을 이용해 큰 문제의 정답을 도출한다. 타뷸레이션(Tabulation)이라고도 부른다. 불필요할 수 있는 값도 미리 계산하는 특성(Eager-Evaluation)이 있다.

def fibo_tabulation(k):
    dp = [0, 1] # 초기 조건
    for _ in range(k - 1):
        dp.append(dp[-1] + dp[-2]) # 작은 문제의 조합으로 다음 문제 해결
    return dp[k]

result = []
n = 10
for i in range(1, n + 1):
    result.append(fibo_tabulation(i))
print(result)

Algorithm_Dynamic_Programming_003

앞선 작은 문제들을 해결하고 그 결과를 더해 이후의 값을 만드는 모습이다.

하향식(Top-Down) - 메모이제이션(Memoization)

하위 문제의 정답이 도출되었는 지 확인하며 문제를 해결한다. 따라서 재귀함수 형태로 구성된다.

def fibo_memoization(dp, k):
    if k <= 1:
        return k
    if len(dp) > k:
        return dp[k]
    dp.append(fibo_memoization(dp, k - 2) + fibo_memoization(dp, k - 1))
    # 더 작은 문제의 결과값을 재귀호출로 확인
    return dp[k]

result = []
n = 10
for i in range(1, n + 1):
    result.append(fibo_memoization([0, 1], i))
print(result)

Algorithm_Dynamic_Programming_004

메모이제이션은 결국 재귀함수로 더 작은 문제 해결을 위해 호출되기 때문에 함수의 리턴값이나 저장되는 형태를 명확히 해야 한다.

읽기 전

  • 불필요한 코드나 잘못 작성된 내용에 대한 지적은 언제나 환영합니다.
  • 개인적으로 사용해보면서 배운 점을 정리한 글입니다.

고등수학 교과과정에 포함되어 있으나 순서쌍을 생성하는 코드는 잘 모르고 있어 정리하게 되었다. backtracking 등 완전 탐색에 많이 쓰이는 데 전체 원소에 대한 순열이 아니라 특정 개수만을 뽑는 경우를 정리해보려 한다.

순열 순서쌍 생성하기

순열(Permutation)은 서로 다른 n개의 대상에서 r개를 추출해 나열된 순서쌍을 의미하며 그 경우의 수에 대한 표현은 $ _{n}P_r(0\le r\le n)$이다. n과 r이 같을 때 순열의 경우의 수는 n!이 된다. (n개의 자리에 n개의 원소가 나열될 경우의 수) 공식은 고등학교에서 배웠듯이 $\cfrac{r!}{(n-r)!}(0\le r \le n)$ 이다.

이제 iterable한 객체를 입력받아 추출할 원소의 개수 r을 입력받았을 때 생성할 수 있는 순열의 순서쌍을 출력하자.

python 코드

def permutation(arr, r):
    arr = sorted(arr) # 이쁘게 만들기 위해
    result = [] # 중간값 저장
    visited = [0 for _ in range(len(arr))] # 방문 체크
    def generate(buf):
        if len(buf) == r: # 만약 모두 추출했으면
            result.append(list(buf)) # 주소 재할당 후 저장 
            return # 종료
        for i in range(len(arr)): # 모든 원소에 대해
            if not visited[i]: # 아직 사용되지 않은 원소라면
                buf.append(arr[i]) # 중간결과에 추가
                visited[i] = 1 # 방문체크
                generate(buf) # 재귀호출
                buf.pop() # backtracking
                visited[i] = 0 # backtracking
    generate([])
    return result

data = "ABC"
for i in range(1, len(data) + 1):
    print(["".join(x) for x in permutation(data, i)])
data = [1, 2, 3]
print(permutation(data, 2))

Algorithm_Make Permutation and Combination Pair_001

위 코드는 재귀 방식으로 구현되었다. result에 buf를 더하는 시점에 relocate 하지 않아 메모리 주소 재할당이 이루어지지 않으면 최종 buf 결과인 빈 리스트로 모든 원소가 바뀌어버린다.

순회 중 데이터가 변경되어 나머지 연결된 원소들의 데이터가 변경됨을 막기 위해 generator가 쓰이는데 itertools 모듈의 permutations 함수가 generator를 사용해 구현되어 있다. 원리는 permutation cycle을 활용한다는데 아직은 이해가 잘 안되서 추후에 따로 시간을 들여 정리해보려 한다.

조합 순서쌍 생성하기

조합(Combination)은 서로 다른 n개의 원소에서 r개를 추출하는 방법에 대한 개념이다. 순서를 고려하지 않으므로 순열과는 달리 뽑기만 하면 되겠다.

$\binom{n}{k}=\ _{n}C_{r}=\cfrac{n!}{(n-r)!r!}(0\le r \le n)$

python 코드

def combination(arr, r):
    arr = sorted(arr)
    result = []

    def generate(buf, idx):  # idx = 이전 방문 좌표
        if len(buf) == r:
            result.append(list(buf))
            return

        for i in range(idx, len(arr)):
            buf.append(arr[i])
            generate(buf, i + 1)
            buf.pop()  # backtracking
    generate([], 0)
    return result

data = "ABC"
for i in range(1, len(data) + 1):
    print(["".join(x) for x in combination(data, i)])
data = [1, 2, 3]
print(combination(data, 2))

Algorithm_Make Permutation and Combination Pair_002

중복 원소가 존재하는 배열의 순열

중복 원소가 존재할 경우 중복된 순열 순서쌍이 저장될 수 있다. 만약 문제에서 허용된다면 그대로 진행해도 되겠지만 그렇지 않을 경우 set 자료형을 써서 해결해야 한다. 이 문제를 코드로 해결해보자.

위 코드에서 기존 배열을 정렬했으므로 중복 원소는 서로 인접한 상태다. 따라서 버퍼 배열에 원소를 추가할 때 중복 원소를 허용할 지 여부만 체크하면 되겠다.

python 코드

def permutation(arr, r):
    arr = sorted(arr, r)
    result = []
    visited = [0 for _ in range(len(arr))]
    def generate(buf):
        if len(buf) == r:
            result.append(list(buf))
            return
        for i in range(len(arr)):
            if not visited[i] and (i == 0 or arr[i] != arr[i - 1] or visited[i - 1]):
                buf.append(arr[i])
                visited[i] = 1
                generate(buf)
                buf.pop()
                visited[i] = 0
    generate([])
    return result

data = "AABC"
for i in range(1, len(data) + 1):
    print(["".join(x) for x in permutation(data, i)])
data = [1, 2, 2, 3]
print(permutation(data, 2))

Algorithm_Make Permutation and Combination Pair_003

추가된 조건 세 가지의 의미는 다음과 같다.

  • i == 0: 첫 번째 원소이므로 중복 여부 상관 없음
  • arr[i] != arr[i - 1]: 중복 원소가 아니므로 상관 없음
  • visited[i - 1]: 이미 앞선 글자가 사용된 상태여야 이후 글자에 대해 허용한다. 만약 그렇지 않다면 이미 i - 1번째 글자(동일 글자)에 대한 순회가 모두 끝나 필요가 없는 상태임을 의미하기 때문이다.

중복 원소가 존재하는 배열의 조합

조합의 경우도 순열과 크게 다르지 않다. 중복 원소를 뽑을 경우의 조건만 순열의 경우처럼 체크하면 되겠다.

def combination(arr, r):
    arr = sorted(arr)
    result = []
    visited = [0 for _ in range(len(arr))]

    def generate(buf, idx):
        if len(buf) == r:
            result.append(list(buf))
            return
        for i in range(idx, len(arr)):
            if not visited[i] and (i == 0 or arr[i] != arr[i - 1] or visited[i - 1]):
                buf.append(arr[i])
                visited[i] = 1
                generate(buf, i + 1)
                buf.pop()
                visited[i] = 0
    generate([], 0)
    return result

data = "AABC"
for i in range(1, len(data) + 1):
    print(["".join(x) for x in combination(data, i)])
data = [1, 2, 2, 3]
print(permutation(data, 2))

Algorithm_Make Permutation and Combination Pair_004

읽기 전

  • 불필요한 코드나 잘못 작성된 내용에 대한 지적은 언제나 환영합니다.
  • 개인적으로 배운 점을 정리한 글입니다.

문제 링크

Programmers. 큰 수 만들기

문제 풀이

앞 자리서부터 다음 자리 수가 현재 위치보다 크다면 제거하고 다시 원점에서 탐색을 반복하여 주어진 횟수만큼 제거된 숫자가 만들 수 있는 가장 큰 수가 될 것이다.

처음엔 buffer에 저장하여 추출할 글자를 빼고 다시 재할당하거나 리스트로 특정 좌표를 pop하는 형태로 구현했다. 그 결과 timeout이 계속 발생해 데이터의 relocate를 막아야 함을 알아낼 수 있었다. 탐색기준은 항상 2개의 자리수만큼 비교하므로 deque를 두 개 선언해서 popleft를 적절히 사용하면 relocate 없이 해결할 수 있을 듯 하다.

  1. 전체 글자를 우측 deque에 삽입

  2. 뺀 숫자를 count하며 주어진 k와 같아질 때까지 아래 탐색과정을 반복한다.

  3. 만약 좌측 큐가 존재하고(이전 단계들에서 통과한 숫자들) 좌측 큐의 마지막 원소가 우측 큐 첫번째 원소보다 작다면 계속 pop하고 그렇지 않다면 빠져나온다.

  4. 좌측 큐의 원소를 빼고나서 k와 같아졌다면 2번 과정을 빠져나온다.

  5. 아직 더 빼야 한다면 우측 큐가 존재하는 동안 첫 번째 원소를 popleft하고 우측 큐의 첫 번째 원소와 비교하여 같거나 크면 좌측 큐에 삽입하고 작으면 그대로 삽입하지 않아 추출된 채로 카운트 1을 올리고 3번으로 회귀한다.

  6. 2번 단계 반복문이 모두 끝났음에도 더 빼야 한다면 좌측 큐의 모든 원소를 3번 과정에 따라 단조 감소 형태이므로 남은 개수만큼 pop한다.

  7. 이후 큐에 남아있는 값들을 리스트에 모아 string화하여 출력한다.

python 코드

from collections import deque

def solution(number, k):
    left_q, right_q = deque(), deque()
    cnt = 0
    for num in number:
        right_q.append(num)
    while cnt < k:
        while left_q:
            if cnt < k and left_q[-1] < right_q[0]:
                left_q.pop()
                cnt += 1
            else:
                break
        if cnt == k:
            break
        while right_q:
            x = right_q.popleft()
            if right_q and right_q[0] <= x:
                left_q.append(x)
            else:
                cnt += 1
                break
        if not right_q:
            break
    while cnt < k:
        left_q.pop()
        cnt += 1
    sol = list(left_q) + list(right_q)
    return "".join(sol)

'Algorithms > Programmers' 카테고리의 다른 글

Programmers. 다리를 지나는 트럭  (0) 2021.08.10
Programmers. 도둑질  (0) 2021.08.10
Programmers. 등굣길  (0) 2021.08.10
Programmers. 소수 찾기  (0) 2021.08.10
Programmers. 디스크 컨트롤러  (0) 2021.07.22

읽기 전

  • 불필요한 코드나 잘못 작성된 내용에 대한 지적은 언제나 환영합니다.
  • 개인적으로 배운 점을 정리한 글입니다.

문제 링크

Programmers. 디스크 컨트롤러

문제 풀이

요청과 처리 완료에 들어가는 시간이 평균적으로 짧아야 한다. 다만 한 번에 한 개의 작업만 가능하다는 점에서 길이가 긴 작업을 먼저 진행하면 뒤이은 작업은 길이가 긴 작업의 진행 시간만큼 대기해야 하므로 평균 처리 시점의 증가로 이어진다.

하나의 작업을 진행하는 동안 접수된 작업 요청들 중 가장 길이가 작은 것들을 우선 처리하고 이후 시점을 갱신하여 다시 대기열을 추가하는 작업이 필요하다.

  • 모든 주어진 작업을 완료해야 하므로 전체 진척도를 별도의 값으로 기록한다. 버퍼 count의 값이 주어진 작업 배열의 길이와 같아질 때까지 반복하면 된다.
  • 각 순회 단계에서 어떤 작업을 처리할 지 대기열을 생성해야 한다. 우선 요청 시간 범위start, end[-1, 0]으로 두고 주어진 작업들 중 해당 요청시간 범위에 있는 작업들을 [작업 시간, 요청 시각]으로 바꿔 대기열 힙에 저장한다.
  • 만약 대기열 힘에 삽입된 작업이 있으면 pop한 뒤 작업 count를 1 더한다. 이후 요청 접수 시작 시간을 기존 시각으로 바꾸고 작업이 진행되었다는 가정 하에 현재 시간을 작업 종료 시간으로 바꾼다.
  • 만약 대기열 힙에 어떤 작업도 삽입되지 않았다면 요청 접수 시간이 짧음을 의미하므로 현재 시점에서 1을 더한다.

python 코드

import heapq

def solution(jobs):
    start, now = -1, 0
    heap = []
    cnt = 0
    answer = 0
    while cnt < len(jobs):
        for job in jobs:
            if start < job[0] <= now:
                heapq.heappush(heap, [job[1], job[0]])
        if heap:
            x = heapq.heappop(heap)
            cnt += 1
            start = now
            now += x[0]
            answer += now - x[1]
        else:
            now += 1
    return answer // len(jobs)

'Algorithms > Programmers' 카테고리의 다른 글

Programmers. 다리를 지나는 트럭  (0) 2021.08.10
Programmers. 도둑질  (0) 2021.08.10
Programmers. 등굣길  (0) 2021.08.10
Programmers. 소수 찾기  (0) 2021.08.10
Programmers. 큰 수 만들기  (0) 2021.07.22

+ Recent posts