본문 바로가기
KraftonJungle2기/Today I Learned

[TIL] Algorithm - 동적 프로그래밍 기본

by SooooooooS 2023. 4. 27.
728x90

1. 그래프 문제 풀이

1. 1388번 - 바닥 장식

import sys

N, M = map(int, sys.stdin.readline().split())
graph = [list(sys.stdin.readline().strip()) for _ in range(N)]
visited = [[0] * M for _ in range(N)]

# 같은 행에 존재하는지 확인해야하므로 열 번호 증가
def col(x, y) :
    visited[x][y] = 1
    stack = [(x, y)]
    
    while stack :
        curx, cury = stack.pop()
        for i in range(1, M) :
            ny = cury + i
            if ny < M and graph[curx][ny] == '-' and visited[curx][ny] == 0 :
                visited[curx][ny] = 1
                stack.append((curx, ny))
            else : # 단 연속적으로 존재하는 것이 아니면 같을 수 없다.
                return

# 같은 열에 존재하는지 확인해야하므로 행 번호 증가
def row(x, y) :
    visited[x][y] = 1
    stack = [(x, y)]
    
    while stack :
        curx, cury = stack.pop()
        for i in range(1, N) :
            nx = curx + i
            if nx < N and graph[nx][cury] == '|' and visited[nx][cury] == 0 :
                visited[nx][cury] = 1
                stack.append((nx, cury))
            else : # 단 연속적으로 존재하는 것이 아니면 같을 수 없다.
                return

count = 0
for i in range(N) :
    for j in range(M) :
        if visited[i][j] == 0 :
            if graph[i][j] == '-' :
                col(i, j)
                count += 1
            elif graph[i][j] == '|' :
                row(i, j)
                count += 1
print(count)
알아보기 편하게 하기 위해 '-'의 경우와 '|' 경우를 나눠서 함수를 구현했다.
1. - 의 경우
    : 같은 행에 위치하면서 연속적으로 존재해야하므로 현재 y좌표에 순서대로 더해보면서 왼쪽부터 오른쪽으로 확인
2. | 의 경우
    : 같은 열에 위치하면서 연속적으로 존재햐야하므로 현재 x좌표에 순서대로 더해보면서 위쪽에서 아래쪽으로 확인

2. 2667번 - 단지번호붙이기

# 단지 번호 붙이기
import sys
from collections import deque

N = int(sys.stdin.readline())
apart = [list(sys.stdin.readline().strip()) for _ in range(N)]

result = []
visited = [[0] * N for _ in range(N)]

dx = [-1, 1, 0, 0]
dy = [0, 0, -1, 1]

def bfs(x, y) :
    count = 1 
    queue = deque()
    
    visited[x][y] = 1
    queue.append((x, y))
    
    while queue :
        curx, cury = queue.popleft()
        for i in range(4) :
            nx, ny = curx + dx[i], cury + dy[i]
            if 0 <= nx < N and 0 <= ny < N and apart[nx][ny] == '1' and visited[nx][ny] == 0 :
                count += 1
                visited[nx][ny] = 1
                queue.append((nx, ny))
    return count # 한 단지 내에 있는 집의 수 반환

for i in range(N) :
    for j in range(N) :
        if visited[i][j] == 0 and apart[i][j] == '1' :
            result.append(bfs(i, j))

result.sort()
print(len(result))
for i in result :
    print(i)
그래프 탐색의 기본 문제
BFS를 이용하여 탐색하며 찾은 위치들을 count하도록 하여 결론 도출

3. 18405번 - 경쟁적 전염

# 경쟁적 전염
import sys
from collections import deque

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

graph = [list(map(int, sys.stdin.readline().split())) for _ in range(N)]

S, X, Y = map(int, sys.stdin.readline().split())

dx = [-1, 1, 0, 0]
dy = [0, 0, -1, 1]

def bfs() :
    time = 0
    start = []
    for i in range(N) :
        for j in range(N) :
            if graph[i][j] != 0 :
                start.append((graph[i][j] ,i, j))
    # 낮은 번호의 바이러스부터 증식한다.
    start.sort() # 바이러스의 번호에 따라 정렬
    queue = deque(start)
    snum = queue[0][0] # 가장 낮은 번호
    fnum = queue[len(queue)-1][0] # 가장 높은 번호
    past = 0 # 전에 탐색한 번호
    
    while queue :
        # 큐 = 선입선출
        num, curx, cury = queue.popleft()
        # 전의 번호가 가장 높은 번호이고 현재 번호가 가장 낮은 번호이면
        # 1초 증가
        if past == fnum and num == snum : 
            time += 1
        if time == S :
            break
        for i in range(4) :
            nx, ny = curx + dx[i], cury + dy[i]
            if 0 <= nx < N and 0 <= ny < N and graph[nx][ny] == 0 :
                graph[nx][ny] = num
                queue.append((num, nx, ny))
        past = num

bfs()
print(graph[X-1][Y-1])
문제 조건 : 시험관에 존재하는 모든 바이러스는 1초마다 상, 하, 좌, 우의 방향으로 증식해 나간다. 단, 매 초마다 번호가 낮은 종류의 바이러스부터 먼저 증식한다. 또한 증식 과정에서 특정한 칸에 이미 어떠한 바이러스가 존재한다면, 그 곳에는 다른 바이러스가 들어갈 수 없다.

위의 조건을 보면 작은 번호의 바이러스를 먼저 증식시켜야 한다. 이때 큐를 이용하면 처음에만 오름차순으로 정렬해서 넣어주면 선입선출 구조이기 때문에 계속해서 순서대로 실행되기 때문에 첫 입력값을 정렬하여 넣어주었다.

하지만 문제는 언제 시간(초)를 증가시켜야 하는가인데, 바이러스 번호 중 가장 작은 것과 가장 큰 것을 저장하여 현재 번호가 가장 작은 번호이고 전에 탐색한 번호가 가장 큰 번호일 때 1초가 지났다고 생각하고 증가시켜줬다.

2. 동적 프로그래밍(Dynamic Programming)

분할 정복과 마찬가지로 부분 문제의 해를 결합해 문제를 해결
Overlapping Subproblems :부분 문제를 한번만 풀어 그 해를 테이블에 저장함으로써 각 부분 문제를 풀 때 마다 다시 계산하는 일을 피하므로 부분 문제가 서로 중복될 때, 즉 부분 문제가 다시 자신의 부분 문제를 공유할 때 적용한다. 
Optimization Problem : 최적화 문제는 다양한 해를 가지는데 이 중 최적의 값을 찾기 원한다. 즉, 최적 부분 구조(Optimal Subtructure)를 가져 하나의 문제에 대한 최적해는 각각을 독립적으로 풀 수 있는 연관된 부분 문제들을의 최적해를 이용한다.

1. 구현 방법

1. Top-Down (하향식)

    : 프로시저를 자연스럽게 재귀적으로 쓰지만 각각의 부분 문제의 결과를 보통 배열이나 해시테이블에 저장해 놓는다.

      프로시저는 현재 부분 문제를 이전에 풀었는지 알아보고 풀었다면 저장되어 있는 값을 사용하고 아니면 계산을한다.

      이 재귀 과정을 메모되었다(Memoization)라고 한다.

 

▶ 하향식 동적 프로그래밍 방법으로 피보나치 수열 의사코드

▶ 하향식 동적 프로그래밍 방법으로 피보나치 수열 구현

import sys

N = int(sys.stdin.readline())

# 하향식 동적 프로그래밍
dic = {0 : 0, 1 : 1}
def top_down_fib(n) :
    if n not in dic.keys() :
        dic[n] = top_down_fib(n-1) + top_down_fib(n-2)
    return dic[n]
    
print(top_down_fib(N))

 

2.  Bottom-Up (상향식) 

    : 특정 부분 문제를 푸는 것이 더 작은 부분 문제를 푸는 것에만 의존

      즉, 특정 부분 문제를 풀 때는 그 해에 영향을 미치는 더 작은 부분 문제를 모두 풀어 그 해를 모두 저장해놓는다.

 

▶ 상향식 동적 프로그래밍 방법으로 피보나치 수열 의사코드

▶ 상향식 동적 프로그래밍 방법으로 피보나치 수열 구현

import sys

N = int(sys.stdin.readline())

# 상향식 동적 프로그래밍
def bottom_up_fib(n) :
    memo = [0] * (n+1)
    memo[1] = 1
    
    for i in range(2, n+1) :
        memo[i] = memo[i-1] + memo[i-2]
    
    return memo[n]

print(bottom_up_fib(N))

< 참고 >

🔗 https://ko.wikipedia.org/wiki/동적_계획법

🔗 https://hongjw1938.tistory.com/47

🔗 https://doorbw.tistory.com/53


2. DP 문제 풀이

1. 1904번 - 01타일

import sys

N = int(sys.stdin.readline())

def binary(n) :
    if n < 3 :
        return n
    
    a = 1
    b = 2
    cnt = 2
    while cnt != n :
        cur = (a + b) % 15746
        a, b = b ,cur
        cnt += 1
    return cur

print(binary(N))
import sys

N = int(sys.stdin.readline())

def binary(n) :
    dp = [0] * (n+1)
    dp[1] = 1
    if n == 1 :
        return dp[1]
    
    dp[2] = 2
    for i in range(3, n+1) :
        dp[i] = (dp[i-1] + dp[i-2]) % 15746
    return dp[n]

print(binary(N))

위의 문제 점화식 찾기

첫번째 코드는 그때그때 구한 값을 변수에 저장하여 구한 것이고
두번째 코드는 리스트에 값을 저장하여 사용하는 방식이다.

훨씬 빠르고 메모리도 덜 사용하는 코드는 첫번째 코드이다.
이때 주의할 점은 인덱스 오류를 조심해야하는데 특히, dp배열을 사용하여 코드를 작성할 때,
위의 코드에서 보듯이 n = 1일 경우 dp[2]는 존재할 수 없다. 이를 위한 예외처리를 해주지 않으면 인덱스 오류가 발생한다.

실제로 dp문제를 풀때 많이 겪었던 문제였는데 오랜만에 풀어서 잊고 있었다.
주의해야겠다🥲 

 

728x90