Skip to content

Learntosurf / 11월 5주차 / 7문제 #99

Merged
learntosurf merged 10 commits intoAlgorithmStudy-Allumbus:mainfrom
learntosurf:main
Dec 2, 2024
Merged

Learntosurf / 11월 5주차 / 7문제 #99
learntosurf merged 10 commits intoAlgorithmStudy-Allumbus:mainfrom
learntosurf:main

Conversation

@learntosurf
Copy link
Collaborator

🌱WIL

  • 스터디 발제 문제 복습과 과제 문제를 하지 못하였다. 월요일 스터디 이후에 그날 혹은 다음날 바로 복습을 진행하도록 해야할 것 같다.
  • 이번주는 다른 할일들이 많아 매일 알고리즘 문제를 푸는 것이 어려웠다. 한 문제를 보고 고민하는 시간, 틀린 문제의 정답을 이해하는 시간이 오래걸리기 때문에 충분한 시간이 확보되지 않으니 급한 마음으로 매일 문제를 풀었던 것 같다. 아직까지는 못푸는 문제가 많아서 정답 풀이를 확인할 때가 많은데 그럴수록 문제들을 많이 풀어보는 것이 중요할 것 같다고 느낀다.
  • DP 문제들은 점화식을 세우고 DP 관련 식을 세우는 것이 중요하다. 따로 DP 부분 이외의 코드 처리를 해줄 것이 없기 때문에 문제 풀이가 간단하게 느껴지지만 그 부분을 생각해낼수 있는지 없는지가 그만큼 중요한 것 같다.
  • 알고리즘 어렵다.

🚀주간 목표 문제 수: 5개

푼 문제


백준 #2748. 피보나치 수 2: DP / 브론즈1

정리한 링크: 바로가기

🚩플로우 (선택)

  • $n$번째 정수를 입력받고, dp를 [0]*n+1로 초기화한다.
  • dp[1]는 1로 초기화하여, 0,1로 시작할 수 있도록 한다.
  • 피보나치 수열에 따라 for문을 2부터 시작해서 $n+1$ 전까지, 즉 $n$까지 반복해준다.
  • $n$번째 피보나치 수를 출력해준다.

🚩제출한 코드

n = int(input())

def fibonacci(n):
    dp = [0] * (n+1)
    
    dp[0] = 0
    dp[1] = 1
    
    for i in range(2, n+1):
        dp[i] = dp[i-1] + dp[i-2]
    
    return dp[n]

print(fibonacci(n))

💡TIL

  • 피보나치 수열은 같은 부분 문제 $f(n-1)$, $f(n-2)$가 여러번 반복된다. DP는 이러한 중복 계산을 방지하기 위해 이전에 계산한 값을 메모리에 저장하여 다시 사용한다.
  • $n$이 0과 1일 땐 각각 0과 1의 값을 가지므로 미리 dp배열에 저장해둔다.

백준 #11724. 연결 요소의 개수: 그래프 순회 / 실버2

정리한 링크: 바로가기

🚩플로우 (선택)

  1. 정점의 개수 $N$, 간선의 개수 $M$을 입력받는다.
  2. $M$개의 줄에서 간선의 정보가 주어진다. 이에 맞게 인접리스트를 구성한다.
  3. BFS 함수를 정의한다.
  4. 방문하지 않은 정점에서 BFS를 수행한다.
  5. 연결 요소의 개수를 출력한다.

🚩제출한 코드

from collections import defaultdict, deque
import sys
input = sys.stdin.readline

N, M = map(int, input().split()) # 정점의 개수, 간선의 개수 
graph = [[] for _ in range(N + 1)]
for _ in range(M):
    u, v = map(int, input().split())
    graph[u].append(v)
    graph[v].append(u)

# 방문 여부 리스트 초기화 
visited = [False] * (N + 1)

# 연결 요소 개수 
connected_components = 0 

def bfs(graph, start):
    queue = deque([start])  # 시작 정점을 큐에 추가
    visited[start] = True  # 시작 정점을 방문 처리
    while queue:
        current = queue.popleft()  # 큐에서 정점 꺼내기
        for neighbor in graph[current]:  # 현재 정점의 모든 이웃에 대해 반복
            if not visited[neighbor]:  # 방문하지 않은 경우만 처리
                visited[neighbor] = True  # 방문 처리
                queue.append(neighbor)  # 큐에 추가

for i in range(1, N+1):
    if not visited[i]: # 방문하지 않은 정점에서 BFS 수행
        bfs(graph, i)
        connected_components += 1

print(connected_components)

💡TIL

  • 그래프 관련 문제를 풀 때는 문제 상황을 그래프로 모델링한 후에 푸는 것이 보편적이다. 모델링한 그래프의 연결관계를 나타내는 두가지 방식이 인접 행렬, 인접 리스트이다.
    • 인접 행렬: 그래프의 연결관계를 행렬로 표현하여 이차원 배열로 나타내는 방식이다.
      • adj[i][j] : 노드 i에서 j로 가는 간선이 존재할 경우 1, 아니면 0
    • 인접 리스트: 각각의 노드에 연결된 노드들을 원소로 갖는 리스트의 배열을 의미한다.
      • adj[i] : i번째 노드에 연결된 노드들을 원소로 갖는 리스트
      • 인접 리스트는 그래프의 전체 노드 목록을 저장한다.
      • 무방향 그래프의 경우에는 본인 노드 인덱스의 리스트 내에서 서로를 원소로 가지게 된다.
      • Python에서는 하나의 노드가 키(key)가 되고, 그 노드에 인접한 노드가 값(value)이 되는 딕셔너리로 구현할 수 있다.
      • 실제 연결된 노드에 대한 정보만 저장하기 때문에, 모든 원소의 개수의 합이 간선의 개수와 동일하다.
      • 노드 $i$$j$의 연결 여부를 알고 싶을 때, adj[i] 리스트를 순회하며 $j$ 원소가 존재하는지 확인해야한다. 인접 행렬은 adj[i][j]가 1인지 0인지만 확인하면 $i$$v$ 노드의 연결 여부를 $O(1)$로 확인 가능하다.

백준 #12865. 평범한 배낭: DP / 골드5

정리한 링크: 바로가기 (정리중)

🚩제출한 코드

import sys
input = sys.stdin.readline

N, K = map(int, input().split()) # 물품의 개수, 버틸 수 있는 무게 

items = []
for _ in range(N):
    W, V = map(int, input().split()) 
    items.append((W, V)) # (물건의 개수, 물건의 가치)

def backpack(N, K, items):
    dp = [0] * (K+1) # 배낭 크기만큼 DP 테이블 초기화 
    
    for weight, value in items:
        for w in range(K, weight-1, -1): # 역순으로 반복
            dp[w] = max(dp[w], dp[w-weight] + value)
    
    return dp[K]

print(backpack(N, K, items))

💡TIL

  • DP 2차원 풀이와 냅색 알고리즘을 공부해보아야겠다.

백준 #1463. 1로 만들기: DP / 실버3

정리한 링크: 바로가기

🚩플로우 (선택)

  1. $N$을 입력받는다.
  2. $N$의 크기만큼 DP 테이블을 초기화한다.
  3. 점화식을 세우고, 그에 맞게 DP 테이블을 채운다.
    • dp[1] = 0 이다. 연산을 진행할 필요가 없다.
    • 2의 배수일때, 3의 배수일때, 1을 뺄때를 나누어 구한다.
  4. dp[N]의 결과를 출력한다.

🚩제출한 코드

N = int(input())

def operation(N):
    dp = [0] * (N+1)
    
    for i in range(2, N+1): # 1은 연산이 필요하지 않음 
        dp[i] = dp[i-1] + 1
        
        if i%2==0:
            dp[i] = min(dp[i], dp[i//2]+1)
        
        if i%3==0:
            dp[i] = min(dp[i], dp[i//3]+1)

    return dp[N]

print(operation(N))

💡TIL

  • DP는 최적화 문제를 해결하는 알고리즘이다. 최대값 또는 최소값을 구하는 문제이거나, 경우의 수를 구하는 문제를 풀 때는 DP를 떠올릴 수 있어야 한다.
    • 해당 문제도 최소 연산 횟수를 구하는 문제이기 때문에 최적화 문제에 속한다.
    • ex. 10 → 9 → 3 → 1: 3번
      • Bottom-up 풀이법: X=10인 경우 > X=9인 경우 > X=3인 경우,…
      • 10을 구할 때는 9의 결과를, 9를 구할 때는 3의 결과를 이용한다. 앞에서 구한 결과값을 저장하였다가 후에 사용하는 것이기 때문에 전의 결과를 다음 결과에 이용하게 되는 점화식을 활용한 Dp 문제이다.
  • 이전 값들을 저장하고 min(), max() 연산을 사용하여 변화된 값과 비교하여 현재 값을 결정하게 하는 풀이들이 많다.
    • $i = 2 , i = 3 , …, i = x$ 까지 하나씩 계산하며 DP 테이블에 값을 저장한다.

백준 #2579. 계단 오르기: DP 실버3

정리한 링크: 바로가기

🚩플로우 (선택)

  • DP에 저장할 값은 DP[i]번째까지 왔을 때 점수의 최대값이다.

    • 첫번째 계단 DP[0] = stair[0]
    • 두번째 계단 DP[1] = stair[0] + stair[1]
    • 세번째 계단 DP[2] = stair[1] + stair[2] or stair[0] + stair[2]
    • 네번째 계단 DP[2] = stair[0] + stair[1] + stair[3] + stair[4] or stair[0] + stair[2] + stair[4]

    DP[i] = max(DP[i-3] + stair[i-1] + stair[i], DP[i-2] + stair[i])

🚩제출한 코드

import sys 
input = sys.stdin.readline

N = int(input())
stair = [int(input()) for _ in range(N)]

# N에 따른 예외 처리
if N == 1:
    print(stair[0])
    exit()

if N == 2:
    print(stair[0] + stair[1])
    exit()

dp = [0] * (N+1)
dp[0] = stair[0]
dp[1] = stair[0] + stair[1]
dp[2] = max(stair[0] + stair[2], stair[1] + stair[2])

for i in range(3, N):
    dp[i] = max(dp[i-2] + stair[i], dp[i-3] + stair[i-1] + stair[i])

print(dp[N-1])

💡TIL

배운 점이 있다면 입력해주세요


백준 #1541. 잃어버린 괄호: 그리디 / 실버2

정리한 링크: (바로가기)

🚩플로우 (선택)

코드를 풀이할 때 적었던 플로우가 있나요?

🚩제출한 코드

💡TIL

  • DP 문제 풀이법
    • 주어진 문제를 더 작은 단위로 나누어 생각하고, 점화식을 찾는다.
      “현재 상태로 이전 상태들로 표현할 수 있는가?”
    • 점화식을 적용하기 전에 초기조건을 설정한다. 초기조건은 일반적으로 가장 작은 크기의 문제에 해당한다.
      "처음 몇 개의 경우는 예외적으로 처리해야 하는가?”
    • dp[i]의 의미를 명확히 정의한다. 문제에서 요구하는 최종 답을 dp의 어떤 값과 연결해야하는지를 고민한다.
      "이 값이 무엇을 나타내는가?”
    • dp[i]를 이전 값(dp[i-1], dp[i-2],..)으로 표현할 방법을 찾는다.
      "현재 상태를 바로 이전 상태들과 연결하는 법”

백준 #12026. BOJ 거리: DP / 실버1

정리한 링크: 바로가기

🚩플로우 (선택)

  1. 보도블록의 글자 순서를 따라야 하므로, B -> O -> J의 규칙을 만족하는 점프만 유효하다.
  2. 스타트는 이전 블록에서 현재 블록으로 점프했을 때의 에너지를 계산하여 최소값을 갱신한다.
  3. 초기값으로 dp[1] = 0 (출발점에서는 에너지가 필요하지 않음), 나머지 dp[i] = inf로 설정한다.
  4. DP 점화식을 세운다.
    • dp[i]: 1번 보도블록에서 $i$번 보도블록에 도달하는 최소 에너지이다.

🚩제출한 코드

import sys 
input = sys.stdin.readline
INF = sys.maxsize

N = int(input())
blocks = input().split()

dp = [INF] * N
dp[0] = 0 # 1번 블록에서 출발 

for i in range(1, N): # 목표 블록 (1번 블록 이후부터 N번까지)
    for j in range(i): # 이전 블록들 (0번부터 i-1번까지 탐색)
        if blocks[j] == 'B' and blocks[i] != 'O':
            continue
        elif blocks[j] == 'O' and blocks[i] != 'J':
            continue
        elif blocks[j] == 'J' and blocks[i] != 'B':
            continue
        dp[i] = min(dp[i], dp[j] + (i-j)**2)

if dp[-1] == INF:
    print(-1)
else:
    print(dp[-1])

💡TIL

  • dp[i] 초기화(INF)를 잊지 말고, dp[0]만 0으로 설정해야 한다.
    • 초기 상태에서 dp[i]는 ‘아직 도달할 수 없음’을 나타내야 한다. 이 상태를 무한대(INF)로 설정한다.
      • dp[i]를 0으로 초기화하면, 아직 도달하지 않은 블록도 "에너지가 0"인 상태로 간주된다.
    • 점프를 통해 블록 $i$에 도달할 수 있다면, dp[i]INF 대신 실제 최소 에너지 값으로 갱신된다.

백준 1495. 기타리스트: DP / 실버1

정리한 링크: 바로가기

🚩플로우 (선택)

  1. DP 배열을 초기화한다.
  2. 각 곡에 대해 현재 가능한 볼륨(dp[i-1][j])에서 P + V[i]P - V[i]를 계산하고, 범위 내에 있다면 dp[i][j]를 갱신한다.
  3. 마지막 곡의 DP 배열(dp[N])을 뒤에서부터 탐색하여, 가능한 최대 볼륨 값을 찾는다. 찾지 못했다면 볼륨을 조절할 수 없다는 의미이므로 -1을 출력한다.

🚩제출한 코드

import sys
input = sys.stdin.readline

N,S,M = map(int,input().split())
V = list(map(int,input().split()))

dp = [[0]*(M+1) for _ in range(N+1)]
dp[0][S] = 1 # 시작 볼륨을 설정 

for i in range(1, N+1): # 각 곡에 대해
    for j in range(M+1): # 가능한 볼륨에 대해
        if dp[i-1][j] != 0: # 이전 곡에서 해당 볼륨이 가능하다면
            if 0 <= j + V[i-1] <= M: # 볼륨을 올릴 수 있다면
                dp[i][j + V[i-1]] = 1
            if 0 <= j - V[i-1] <= M: # 볼륨을 낮출 수 있다면
                 dp[i][j-V[i-1]] = 1
volume = -1
for i in range(M, -1, -1): # 최대 볼륨부터 탐색
    if dp[N][i] == 1: # 가능한 볼륨을 찾으면 
        volume = i
        break
    
print(volume)

💡TIL

  • 2차원 dp에 값을 저장하는 풀이에 익숙해질 것.
    • 이 문제는 곡 개수($N$, 행)만큼 최대 볼륨값($M$, 열) 길이의 배열을 만들어서 풀이한다.
  • 마지막 행을 내림차순으로 탐색하면 최대값을 빠르게 찾을 수 있다.
  • 런타임 에러가 발생할 때는 입력 조건과 로직을 함께 점검해야 한다. 특정 상황에서 입력 조건이나 예외 상황 처리가 누락된 경우 런타임 에러가 발생할 가능성이 있다.

Copy link
Collaborator

@Mingguriguri Mingguriguri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

과제 문제는 못 풀었지만, 매일 꾸준히 문제를 푸셨던 점에서 멋지다고 느껴지네요!! 고생 많으셨습니다

Comment on lines 12 to 18
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1차원 배열로 어떻게 풀지, 싶었는데 훨씬 더 효율적인 풀이인 것 같네요!
2차원이 아닌 1차원으로 접근한 방식에 배워갑니다. :D

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

중간에 잃어버린 괄호에 대한 코드가 없는 것 같네요! 확인해주세요!
추가로 앞으로 PR 올리실 때 Asignee랑 Reviewrs도 함께 등록해주시면 감사하겠습니다!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

보통 이런 문제는 2차원 배열로 만들어 풀긴 하는데 1차원 리스트로도 푸셨네요. 메모리 제한이 있을때는 이런 방식으로 푸는게 더 좋을거 같네요 참고가 되었습니다!

Comment on lines 12 to 18
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

보통 이런 문제는 2차원 배열로 만들어 풀긴 하는데 1차원 리스트로도 푸셨네요. 메모리 제한이 있을때는 이런 방식으로 푸는게 더 좋을거 같네요 참고가 되었습니다!

@learntosurf learntosurf merged commit 1eb63e3 into AlgorithmStudy-Allumbus:main Dec 2, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants