정화 코딩

EDOC 2024-1 6회차 과제 (동적 계획법 알아보기 4) 본문

Group/EDOC

EDOC 2024-1 6회차 과제 (동적 계획법 알아보기 4)

jungh150c 2024. 5. 18. 02:11

093. Dance Dance Revolution (백준 2342번)

 

https://www.acmicpc.net/problem/2342

 

import sys
input = sys.stdin.readline

data = list(map(int, input().split()))
n = len(data) - 1
l = [0 for _ in range(n + 1)]
r = [0 for _ in range(n + 1)]
p = [0 for _ in range(n + 1)]

for i in range(n):
    if l[i] == data[i] or r[i] == data[i]:
        l[i + 1] = l[i]
        r[i + 1] = r[i]
        p[i + 1] = p[i] + 1
    elif l[i] == 0:
        l[i + 1] = data[i]
        r[i + 1] = r[i]
        p[i + 1] = p[i] + 2
    elif r[i] == 0:
        l[i + 1] = l[i]
        r[i + 1] = data[i]
        p[i + 1] = p[i] + 2
    else:
        ldif = abs(l[i] - data[i])
        if ldif == 3:
            ldif = 1
        rdif = abs(r[i] - data[i])
        if rdif == 3:
            rdif = 1
        if ldif < rdif:
            l[i + 1] = data[i]
            r[i + 1] = r[i]
            p[i + 1] = p[i] + ldif + 2
        else:
            l[i + 1] = l[i]
            r[i + 1] = data[i]
            p[i + 1] = p[i] + rdif + 2

print(p[n])

처음에는 그리디 알고리즘이 생각났다. 순간마다 가장 힘이 적게 들도록 선택하면 최종 값도 최적이 될 것 같았기 때문이다. 하지만 제출해보니 틀렸다. (오답)

이 문제를 그리디 알고리즘으로 풀면 안 되는 이유는, 해당 순간에는 최적이 아닐지라도 최종적으로는 최적이 되도록 하는 선택이 있을 수 있기 때문이다. 예를 들어, 왼쪽 발은 2에 있고 오른쪽 발은 1에 있는 상황에서 지시 사항으로 4와 1이 추가로 들어왔다고 생각해보자. 매 순간에서 최선의 선택을 하는 그리디 알고리즘으로 해결하면 오른쪽 발을 4로 옮기고 왼쪽 혹은 오른쪽 발을 다시 1로 옮겨야 하므로 사용되는 힘은 3 + 3 = 6이다. 하지만 만약 해당 순간에서는 비효율적인 것처럼 보이더라도 4가 들어왔을 때 오른쪽 발을 옮기지 않고 왼쪽 발을 옮겨서 다음에 들어오는 1을 위해 오른쪽 발은 1에 유지하면 사용되는 힘은 4 + 1 = 5이다. 이러한 경우가 있을 수 있기 때문에 그리디 알고리즘이 아닌 다이나믹 프로그래밍을 사용해야 한다.

 

import sys
input = sys.stdin.readline

p = [[0, 2, 2, 2, 2], # 0에서 다른 칸으로 움직일 때 드는 힘
     [2, 1, 3, 4, 3], # 1에서 다른 칸으로 움직일 때 드는 힘
     [2, 3, 1, 3, 4], # 2에서 다른 칸으로 움직일 때 드는 힘
     [2, 4, 3, 1, 3], # 3에서 다른 칸으로 움직일 때 드는 힘
     [2, 3, 4, 3, 1]] # 4에서 다른 칸으로 움직일 때 드는 힘

data = list(map(int, input().split()))
n = len(data) - 1

# dp[n][l][r] : n번째 지시 사항까지 완료한 후 왼발의 위치가 l이고 오른발의 위치가 r일 때 최소 누적 합
dp = [[[sys.maxsize for _ in range(5)] for _ in range(5)] for _ in range(n + 1)]
dp[0][0][0] = 0

for i in range(1, n + 1):
    for j in range(5):
        if j != data[i - 1]:
            for k in range(5):
                dp[i][data[i - 1]][j] = min(dp[i - 1][k][j] + p[k][data[i - 1]], dp[i][data[i - 1]][j])
    for j in range(5):
        if j != data[i - 1]:
            for k in range(5):
                dp[i][j][data[i - 1]] = min(dp[i - 1][j][k] + p[k][data[i - 1]], dp[i][j][data[i - 1]])

ans = sys.maxsize
for i in range(5):
    ans = min(ans, dp[n][i][data[n - 1]])
    ans = min(ans, dp[n][data[n - 1]][i])

print(ans)

dp[n][l][r] : n번째 지시 사항까지 완료한 후 왼발의 위치가 l이고 오른발의 위치가 r일 때 최소 누적 합

n번째 지시 사항이 x라고 하면, 왼쪽 발이 x에 있는 경우(dp[n][x][_])와 오른쪽 발이 x에 있는 경우(dp[n][_][x]) 중 최소 값n번째 지시 사항까지 완료할 때 사용되는 최소 힘이 되는 것이다. (정답)

 


 

094. 행렬 곱셈 순서 (백준 11049번)

 

https://www.acmicpc.net/problem/11049

 

import sys
input = sys.stdin.readline

# bottom-up 방식

n = int(input())
m = []
# dp[i][j] : i번째 행렬부터 j번째 행렬까지 곱할 때 필요한 곱셈 연산의 수
dp = [[0 for _ in range(n)] for _ in range(n)]

for _ in range(n):
    a, b = map(int, input().split())
    m.append([a, b])

for i in range(1, n):
    for j in range(n - i):
        dp[j][j + i] = sys.maxsize
        for k in range(j, j + i):
            dp[j][j + i] = min(dp[j][j + i], dp[j][k] + dp[k + 1][j + i] + m[j][0] * m[k][1] * m[j + i][1])

print(dp[0][n - 1])

컴퓨터 알고리즘 수업에서 배운 bottom-up 방식으로 푼 풀이이다. (정답)

dp[i][j] : i번째 행렬부터 j번째 행렬까지 곱할 때 필요한 곱셈 연산의 수

점화식 ⇒ dp[i][j] : dp[i][k] + dp[k+1][j] + m[i][0] * m[k][1] * m[j][1]

점화식을 말로 풀어보면 다음과 같다. k를 i부터 (j-1)까지 증가시켜가면서 i번째 행렬부터 k번째 행렬까지 곱할 때 필요한 곱셈 연산의 수(이미 계산된 값)와 (k+1)번째 행렬부터 j번째 행렬까지 곱할 때 필요한 곱셈 연산의 수(이미 계산된 값)의 합 중 최소를 찾는다. 거기에 그 두 행렬를 곱할 때 필요한 곱셈 연산의 수인 m[i][0] * m[k][1] * m[j][1]를 더해주어야 한다. 

 

이 순서로 dp 테이블을 채우면 된다.

 

import sys
input = sys.stdin.readline

# top-down & memoization 방식

n = int(input())
m = []
# dp[i][j] : i번째 행렬부터 j번째 행렬까지 곱할 때 필요한 곱셈 연산의 수
dp = [[-1 for _ in range(n)] for _ in range(n)]

for _ in range(n):
    a, b = map(int, input().split())
    m.append([a, b])

def execute(s, e):
    if dp[s][e] != -1:
        return dp[s][e]
    if s == e:
        return 0
    if s + 1 == e:
        return m[s][0] * m[s][1] * m[e][1]
    res = sys.maxsize
    for i in range(s, e):
        res = min(res, m[s][0] * m[i][1] * m[e][1] + execute(s, i) + execute(i + 1, e))
    dp[s][e] = res
    return dp[s][e]

print(execute(0, n - 1))

교재에 나와있는 top-down & memoization 방식으로 푼 풀이이다. (정답)

나머지는 전부 비슷한데 재귀적으로 푼 것 뿐이다. 계산하지 않은 값은 -1로 두어 구분하도록 하고, 한번도 계산하지 않았던 값에 대해서만 계산하고 이미 계산한 적이 있다면 저장된 값을 가져와야 한다. 

 

그런데 두 풀이 모두 python으로 제출하면 시간 초과가 나오고 pypy로 제출해야 맞았습니다가 나온다. pypy로 제출한 경우에서 굳이 비교해보자면  bottom-up 방식이 top-down 방식보다 조금 더 빠른 것을 확인할 수 있었다. 

 


 

095. 외판원 순회 (백준 2098번)

 

https://www.acmicpc.net/problem/2098

 

import sys
input = sys.stdin.readline

n = int(input())
w = []

for _ in range(n):
    w.append(list(map(int, input().split())))

dp = [[0 for _ in range(1 << n)] for _ in range(n)]

def tsp(c, v):
    if v == (1 << n) - 1: # 모든 도시를 방문한 경우
        if w[c][0] == 0:
            return(sys.maxsize)
        else:
            return w[c][0]
    if dp[c][v] != 0: # 이미 방문한 도시인 경우
        return dp[c][v]
    
    mint = sys.maxsize
    for i in range(n):
        # 방문한 적이 없고, 갈 수 있는 도시인 경우
        if ((v & (1 << i)) == 0 and w[c][i] != 0):
            # 방문한 도시에 추가하여 다시 tsp 함수 호출
            mint = min(mint, tsp(i, (v | (1 << i))) + w[c][i])
    dp[c][v] = mint

    return dp[c][v]

print(tsp(0, 1)) # 1번 도시(인덱스 0)부터 시작한다고 가정

특정 노드에서 특정 노드까지의 최단 경로도 아니고 원래 위치로 다시 돌아오는 최단 순회 경로는 풀어본 적이 없어서 감도 안 오고 어렵게 느껴졌다. 그래서 태그를 슬쩍 보니 비트마스킹??? 들어본 적은 있지만 뭔지는 몰라서 이참에 알아보자.

 

비트 연산자

AND 연산 (&) : 둘 다 1이면 1 반환 (ex) 1010 & 1111 = 1010

OR 연산 (|) : 하나라도 1이면 1 반환 (ex) 1010 | 1111 = 1111

XOR 연산 (^) : 둘이 같으면 0, 다르면 1 반환 (ex) 1010 ^ 1111 = 0101

NOT 연산 (~) : 반전 (0이면 1, 1이면 0 반환) (ex) ~1010 = 0101

왼쪽 shift (<<) : 왼쪽으로 특정 칸만큼 이동 (ex) 001010 << 2 = 101000

오른쪽 shinft (>>) : 오른쪽으로 특정 칸만큼 이동 (ex) 001010 >> 2 = 000010

 

비트마스킹

컴퓨터는 내부적으로 bit 단위로 연산을 한다. 이러한 비트의 특성을 이용해서 이진수 표현을 자료구조로 쓰는 기법을 비트마스킹이다. 

 

비트마스킹을 통한 집합 표현

비트마스킹을 이용하면 집합을 쉽게 표현할 수 있다. 구체적으로, 원소의 개수가 n개인 집합이 있다고 하면, 원소에 0부터 n-1까지 번호를 부여해서 부분 집합으로 나타낼 수 있다. 번호에 해당하는 비트 하나가 하나의 원소 상태며, 비트가 1이면 해당 원소가 집합에 포함되어 있다는 의미고, 0이면 포함되지 않는다는 의미이다. 예를 들어, 0부터 4까지의 수가 있을 때 1, 3, 4로 이루어진 집합은 01011로 표현할 수 있는 것이다. 

 

비트마스킹의 장점

1. 빠른 수행 시간

2. 간결한 코드

3. 적은 메모리 사용량

 

관련 문제

백준 11723번: 집합 (https://www.acmicpc.net/problem/11723)

 

다시 이 문제로 돌아와서...

외판원 순회 문제는 영어로 Traveling Salesman problem (TSP) 문제라고 한다. 

w[i][j] : 도시에서 도시로 가기 위한 비용 (갈 수 없으면 값이 0이다.)

dp[c][v] : 현재 도시가 c, 현재까지 방문한 모든 도시 리스트가 v일 때, 앞으로 남은 모든 도시를 경유하는 데 필요한 최소 비용

→ 현재까지 방문한 도시 리스트 v를 비트마스킹으로 표현한다. (ex) 1~4번 도시 중 1, 3번 도시 방문 → 0101(2) = 5(10)

⇒ 점화식: 방문하지 않은 도시 i에 대해 반복하면서, dp[c][v] = min(dp[c][v], dp[i][v | (1 << i)] + w[c][i])

참고로, 어느 도시를 시작 도시로 둘 것인지는 중요하지 않고 그냥 임의로 두면 된다. 어차피 어디서 시작하든 모든 도시를 돌며 한바퀴를 도는 경로를 찾는 것이기 때문이다. (내가 처음에 헷갈렸던 부분) (정답)

 


 

096. 가장 긴 증가하는 부분 수열 5

 

https://www.acmicpc.net/problem/14003

 

import sys
input = sys.stdin.readline

n = int(input())
a = [0] + list(map(int, input().split()))

dp = [0 for _ in range(n + 1)]
b = [0 for _ in range(n + 1)]
idx = 0
maxl = 1
b[1] = a[1]
dp[1] = 1

def binarysearch(l, r, now):
    while l < r:
        mid = (l + r) // 2
        if b[mid] < now:
            l = mid + 1
        else:
            r = mid
    return l

for i in range(2, n + 1):
    if b[maxl] < a[i]:
        maxl += 1
        b[maxl] = a[i]
        dp[i] = maxl
    else:
        idx = binarysearch(1, maxl, a[i])
        b[idx] = a[i]
        dp[i] = idx

idx = maxl
ans = [0 for _ in range(idx + 1)]
for i in range(n, 0, -1):
    if dp[i] == idx:
        ans[idx] = a[i]
        idx -= 1

print(maxl)
for i in range(1, maxl + 1):
    print(ans[i], end = ' ')

어려워서 책에서 점화식을 어떻게 정의했는지까지만 참고했다.

dp[i] : 0부터 i까지 i를 포함하는 가장 길게 증가하는 수열의 길이

앞의 계산을 뒤의 계산에서 활용하기 위해서 마지막 요소를 포함시켜 점화식을 정의하는 방식이다. dp에서 몇번 봤던 방식인 것 같다. 아무튼 여기까지는 알겠으나 태그를 보니 이분 탐색이 있었다. 이분 탐색은 너무 뜬금없게 느껴지고 어떻게 같이 활용하는 것인지 어려워서 가장 긴 증가하는 부분 수열 1~4를 먼저 풀어보기로 했다.

https://jungh150c.tistory.com/109

(정답)

 

Comments