Skip to content

Minjeong / 1월 1주차 / 5문제#117

Merged
Mingguriguri merged 5 commits intomainfrom
minjeong
Jan 7, 2025
Merged

Minjeong / 1월 1주차 / 5문제#117
Mingguriguri merged 5 commits intomainfrom
minjeong

Conversation

@Mingguriguri
Copy link
Collaborator

@Mingguriguri Mingguriguri commented Jan 4, 2025

🌱WIL

이번 한 주의 소감을 작성해주세요!
1. 트리 DP

  • 트리 구조에서 DP(동적 계획법)를 적용하는 문제를 처음 풀어봤는데, DFS와 DP를 동시에 사용하는 방식이 신선하게 다가왔다.
  • 트리 DP 문제를 해결할 때는 DFS 결과를 저장해 재사용하는 것이 핵심이며, 자식 노드의 방문 여부를 정확하게 체크하는 것이 중요하다.
  • 이 과정을 통해 트리 문제에 대한 이해가 깊어지고, 새로운 영역으로의 확장감을 느낄 수 있었다.

2. 구현 문제

  • 실버 난이도 2개와 골드 난이도의 문제 1개를 풀었다.
  • 문제를 해결하며 리스트 슬라이싱과 형변환 과정이 **O(N)**의 시간복잡도를 갖는다는 점을 새롭게 알게 되었다.
  • 또한, deque 자료형에 reverse() 함수가 존재한다는 점도 이번에 배운 흥미로운 부분이었다.

3. 회고

  • 매번 느끼는 점이지만, 문제를 정확하게 읽고 해석하는 것의 중요성을 다시금 깨달았다.
  • 이번 주에는 입출력 조건을 놓쳐서 문제를 잘못 풀이하는 실수를 범했다.
  • 이와 더불어,주어진 조건을 조합하는 능력뿐만 아니라, 이를 효율적으로 구현하는 능력도 중요하다는 점을 체감했다.
    다음과 같은 프로세스를 자주 연습하고 습관화해야겠다고 다짐했다.
    1. 필수 기능을 먼저 체크하고, 주기능을 중심으로 전체 틀 구성
    2. 주요 기능을 보완하는 부가적인 요소들을 하나씩 채워나가기
    3. 입출력 조건을 정확하게 반영하여 코드 작성
    4. 코드 완성 후 비효율적인 부분을 개선해 최적화
      ⇒ 이러한 프로세스를 반복적으로 연습하고, 문제를 해결할 때마다 적용해보려고 한다.

🚀주간 목표 문제 수: 5개

푼 문제


백준 #15681. 트리와 쿼리: DP, 골드5

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

🚩플로우 (선택)

풀이 아이디어
1. 트리의 특성과 서브트리 크기

  • 트리는 방향성이 없는 연결 그래프이다.
  • 한 정점을 루트로 설정하면, 모든 정점은 부모-자식 관계로 표현할 수 있다.
  • 서브트리란 특정 정점과 그 아래의 자식 노드들로 이루어진 부분 트리이다.

2. 핵심 풀이 아이디어

  • **DFS(깊이 우선 탐색)**으로 트리를 순회하며 서브트리의 크기를 미리 계산해둔다.
  • 한 번의 DFS로 모든 정점의 서브트리 크기를 계산하면, 쿼리에서 O(1)로 바로 답을 출력할 수 있다.

3. DFS를 이용한 풀이

  1. 트리 그래프를 구성한다.
    • 주어진 간선 정보를 통해 그래프 형태로 트리를 구성한다.
    • 방향성이 없는 그래프이므로, 간선을 양방향으로 연결한다.
  2. DFS로 서브트리 크기를 계산한다.
    • 각 정점의 서브트리 크기를 배열 size[]에 저장한다.
    • 서브트리의 크기는 해당 노드 + 자식 노드들의 서브트리 크기이다.
  3. 쿼리에 대해 미리 계산한 값을 출력한다.

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

  1. 입력 받기
    • N: 트리의 정점 개수
    • R: 루트 노드
    • Q: 쿼리 개수
    • N-1개의 간선 정보를 통해 트리를 구성한다.
  2. 트리 구성
    • 각 정점을 리스트에 담아 양방향 연결 그래프 형태로 저장한다.
    • 예: graph[u].append(v)graph[v].append(u)
  3. DFS로 서브트리 크기 계산
    • DFS 탐색을 통해 각 노드의 서브트리 크기를 구한다.
    • 이미 방문한 노드는 건너뛰고, 방문하지 않은 노드는 자식으로 추가해 재귀 호출한다.
  4. 쿼리 처리
    • 쿼리에서 요청한 정점의 서브트리 크기를 size[] 리스트에서 바로 찾아 출력한다.

🚩제출한 코드

import sys
input = sys.stdin.readline
sys.setrecursionlimit(10**6)  # 재귀 깊이 제한 설정(10만)

# 입력 처리
N, R, Q = map(int, input().split())  # 정점 수, 루트 번호, 쿼리 수
graph = [[] for _ in range(N + 1)]  # 트리 그래프

# 간선 정보 입력
for _ in range(N - 1):
    u, v = map(int, input().split())
    graph[u].append(v)
    graph[v].append(u)

# 서브트리 크기 기록 배열
size = [0] * (N + 1)

# DFS를 이용한 서브트리 크기 계산
def dfs(current):
    size[current] = 1  # 자기 자신 포함
    for node in graph[current]:
        if size[node] == 0:  # 아직 방문하지 않은 경우
            dfs(node)
            size[current] += size[node]  # 자식 서브트리 크기 추가

# 루트에서 시작해 서브트리 크기 계산
dfs(R)

# 쿼리 처리
for _ in range(Q):
    U = int(input())
    print(size[U])

💡TIL

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

  • 트리 문제는 처음에는 어렵게 느껴졌지만, 트리의 특성을 이해하고 나니 한결 쉬워졌다.
  • DFS를 활용한 트리 순회와 정점별 서브트리 크기 저장 방식을 활용한 점이 인상적이었다.
  • 배운 점: 트리 문제에서 한 번의 DFS로 필요한 정보를 미리 계산해두면, 이후의 연산을 매우 효율적으로 처리할 수 있다.

백준 #1949. 우수마을: DP / 골드2

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

🚩플로우 (선택)

풀이 아이디어

  1. 트리 구조 구성
    • 마을 사이의 연결 관계를 인접 리스트로 표현한다.
    • 방향성이 없는 그래프이므로 양방향으로 저장한다.
  2. DFS로 트리 탐색 및 DP 구성
    • 각 마을을 우수 마을로 선정했을 때와 하지 않았을 때의 값을 DP로 관리한다.
    • dp[node][0]: 현재 마을을 우수 마을로 선택하지 않았을 때의 최대 주민 수
    • dp[node][1]: 현재 마을을 우수 마을로 선택했을 때의 최대 주민 수
  3. DP 전이 관계
    • dp[node][1] = people[node] + dp[child][0]
      • 현재 마을을 우수 마을로 선택했다면, 자식 마을은 반드시 우수 마을이 아니어야 한다.
    • dp[node][0] = max(dp[child][0], dp[child][1])
      • 현재 마을을 우수 마을로 선택하지 않았다면, 자식 마을은 우수 마을이거나 아닐 수 있다.

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

  1. 입력 받기
    • N: 마을 수
    • people: 각 마을의 주민 수 (1번부터 N번까지)
    • town: 트리 형태의 마을 연결 정보
  2. DP 및 방문 처리 초기화
    • dp[node][0], dp[node][1] 초기화
    • 방문 처리 리스트 visited[] 초기화
  3. DFS 탐색으로 DP 수행
    • 트리의 루트(1번 마을)부터 DFS를 통해 트리를 순회한다.
    • 자식 노드의 DP 값을 현재 노드에 반영한다.
  4. 결과 출력
    • 루트 마을에서 우수 마을로 선정했을 때와 하지 않았을 때 중 더 큰 값을 출력한다.

🚩제출한 코드

import sys

input = sys.stdin.readline
sys.setrecursionlimit(10 ** 6)  # 재귀 깊이 설정 (10만)

# 1. 입력 받기
N = int(input())  # 마을 개수
people = [0] + list(map(int, input().split()))  # 각 마을 주민 수 (인덱스 맞추기 위해 0 추가)

# 2. 마을 연결 (트리 구성)
town = [[] for _ in range(N + 1)]

for _ in range(N - 1):
    u, v = map(int, input().split())
    town[u].append(v)
    town[v].append(u)

# 3. DP 테이블 초기화 및 방문 리스트
dp = [[0, 0] for _ in range(N + 1)]  # dp[i][0]: 우수 마을 X, dp[i][1]: 우수 마을 O
visited = [False] * (N + 1)


# 4. DFS를 통한 서브트리 탐색 및 DP 계산
def dfs(current):
    visited[current] = True  # 현재 노드 방문 처리
    dp[current][1] += people[current]  # 현재 마을을 우수 마을로 선정한 경우 (자기 자신 포함)

    for child in town[current]:
        if not visited[child]:
            dfs(child)  # 자식 노드로 DFS 진행
            dp[current][1] += dp[child][0]  # 자식 노드가 우수 마을이 아닌 경우 주민 수 더하기
            dp[current][0] += max(dp[child][0], dp[child][1])  # 자식 노드에서 우수 마을이거나 아닌 경우 중 최대값 선택


# 5. DFS 호출 (루트 노드 1부터 시작)
dfs(1)

# 6. 결과 출력
print(max(dp[1][0], dp[1][1]))

💡TIL

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

  • 처음에 단순하게 DFS로 마을 주민 수를 누적하는 방식으로 접근했는데, 인접 마을이 동시에 우수 마을이 될 수 없다는 조건 때문에 어려움을 겪었다.
  • 트리 구조에서의 DP(트리 DP)가 새로운 방식이었지만, 자식 노드의 값을 부모 노드에 누적하는 방식이 DP의 기본 원리와 유사했다.
  • 앞으로 트리 문제를 풀 때는 DFS + DP 패턴을 더 적극적으로 활용해야겠다.

백준 #2563. 색종이: 구현 / 실버5

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

🚩플로우 (선택)

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

  1. 도화지 초기화
    • 가로, 세로 100x100 크기의 2차원 리스트를 0으로 초기화한다.
    • paper = [[0] * 100 for _ in range(100)]
  2. 색종이 위치 입력 및 도화지 채우기
    • 색종이의 수 n을 입력받는다.
    • 각 색종이의 왼쪽 아래 좌표 (x, y)를 입력받는다.
    • 해당 위치부터 10x10 크기로 2차원 리스트에서 1로 채운다.
    • 이미 1로 채워진 경우는 건너뛴다.
  3. 검은 영역 계산
    • 도화지 전체를 순회하며 1의 개수를 모두 더한다.
    • 최종적으로 검은색 영역의 넓이를 출력한다.

🚩제출한 코드

import sys
input = sys.stdin.readline

n = int(input())  # 색종이의 수
paper = [[0] * 100 for _ in range(100)]  # 100x100 도화지 초기화

# 색종이를 하나씩 붙이기
for _ in range(n):
    x, y = map(int, input().split())

    # 10x10 크기의 영역을 1로 채운다.
    for i in range(x, x + 10):
        for j in range(y, y + 10):
            paper[i][j] = 1

# 도화지에서 1의 개수를 모두 더하여 넓이 계산
answer = 0
for row in paper:
    answer += sum(row)

print(answer)

💡TIL

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

  • 처음에는 색종이가 겹치는 부분을 직접 계산하려고 했지만, 겹침을 하나하나 고려하는 방식은 어렵고 실수가 많았다.
  • 2차원 리스트를 사용해 직접 도화지에 색종이를 붙이는 방식이 더 직관적이고 간단했다. 특히 2차원 리스트를 1로 채우고, 1의 개수를 세는 방식이 참신하게 느껴졌다.
  • 코딩테스트 문제를 풀 때면 일반적인 시각에서 보면서 접근해야 할 때도 있고, 반대로 또 코딩에서만 활용할 수 있는 쪽을 통해서 접근할 때도 있다. 이번 문제는 후자였던 것 같다. 후자인 경우의 사고력을 키워야 할 것 같다.

백준 #25206. 너의 평점은: 구현 / 실버5

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

🚩플로우 (선택)

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

  1. 입력 받기
    • 20과목의 정보를 한 줄씩 입력받는다.
    • 각 줄에서 과목명, 학점, 등급을 공백으로 구분해 받는다.
  2. 전공평점 계산
    • 각 과목별 (학점 × 과목 평점)을 누적하여 더한다.
    • P 등급 과목은 계산에서 제외한다.
  3. 평점 출력
    • (총 평점 ÷ 총 학점)을 구해 소수점 6자리까지 출력한다.

🚩제출한 코드

import sys
input = sys.stdin.readline

# 1. 과목 평점 테이블 (딕셔너리 사용)
rating = {
    "A+": 4.5, "A0": 4.0,
    "B+": 3.5, "B0": 3.0,
    "C+": 2.5, "C0": 2.0,
    "D+": 1.5, "D0": 1.0,
    "F": 0.0
}

# 2. 변수 초기화
total_score = 0  # 총 평점 (학점 * 과목 평점의 합)
total_credit = 0  # 총 학점

# 3. 과목별 입력 및 평점 계산
for _ in range(20):
    subject, credit, grade = input().split()
    credit = float(credit)  # 학점은 소수점으로 변환

    if grade != 'P':  # P 등급 과목 제외
        total_score += credit * rating[grade]
        total_credit += credit

# 4. 전공 평점 계산 및 출력
GPA = total_score / total_credit
print("{:.6f}".format(GPA))  # 소수점 6자리 출력

💡TIL

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

  • 소수점 아래 6자리만큼 출력하고 싶다면 format 함수 또는 **포맷팅**을 이용하면 된다.

    print("{:.6f}".format(GPA))  # format 사용
    print("%.6f" % GPA)  # % 포맷팅 사용
  • 변수 이름 고민하는 게 어렵다. 미국에서는 ‘학점’을 credit으로 쓴다고 한다.


백준 #5430. AC: 구현 / 골드5

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

🚩플로우 (선택)

풀이 아이디어
위의 시간 초과 문제를 해결하기 위해서는 아래 2가지 방식의 접근을 해야 한다.

  1. 리버스 최적화 (Lazy Reverse)

    • 배열을 실제로 뒤집는 대신 뒤집혔는지 여부를 나타내는 플래그를 사용한다.
    • 'D' 연산 시 앞에서 제거할지, 뒤에서 제거할지 플래그로 결정한다.
  2. deque 활용

    • 배열을 처음부터 deque로 변환해 사용한다.
    • D 연산은 popleft()pop()으로 O(1)에 수행된다.

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

  1. 입력 받기
    • 명령어 p, 배열 크기 n, 배열 nums를 입력받는다.
    • 배열은 deque로 변환해 사용한다.
    • 배열이 빈 경우([]) 예외처리한다.
  2. 명령어 처리
    • R: reverse 상태를 토글한다. (True ↔ False)
    • D: 배열이 비어 있으면 error를 출력하고 종료한다.
      • 비어 있지 않으면 reverse 상태에 따라 popleft() 또는 pop()을 실행한다.
  3. 최종 출력
    • reverse 상태에 따라 배열을 뒤집는다.
    • 명령어 처리가 끝난 배열을 출력한다.

🚩제출한 코드

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

def AC():
    p = input().strip()  # 수행할 함수
    n = int(input())  # 배열 크기
    nums = input().strip()  # 배열 입력
    
    # 배열 처리
    if nums == "[]": 
        nums = deque()
    else:
        nums = deque(map(int, nums[1:-1].split(",")))
    
    reverse = False  # 뒤집기 플래그
    for command in p:
        if command == "R":
            reverse = not reverse # R이 나올 때마다 뒤집기 상태 토글
        elif command == "D":
            if not nums:  # 비어있는 경우 에러 출력
                print("error")
                return
            if reverse:  # 뒤집힌 상태라면 뒤에서 제거
                nums.pop()
            else:  # 그렇지 않다면 앞에서 제거
                nums.popleft()
    
    # 최종 출력
    if reverse:
        nums.reverse()  # 필요시 뒤집기
    print("[" + ",".join(map(str, nums)) + "]")

# 테스트 케이스 처리
T = int(input())  # 테스트 케이스 수
for _ in range(T):
    AC()

💡TIL

회고

  • 처음에는 배열을 매번 뒤집는 방식으로 풀었으나 시간 초과가 발생했다.

  • deque와 뒤집기 플래그를 사용해 문제를 효율적으로 해결할 수 있었다. 이 과정에서 deque 자료형에 reverse 함수가 있다는 것을 알게 되었다.

  • 1 2 3 형식이 아닌 [1,2,3] 형식으로 입력이 될 때 어떤 식으로 입력을 받아야 할 지 고민할 수 있는 좋은 문제였다. []으로 입력이 들어올 때에도 예외처리를 고려해야 한다는 것을 알게 되었다.

  • 입출력 조건에 맞게 코드를 구성해야 한다는 것을 알게 됐다.

    • 단순히 list()로 형변환하여 출력하였으나 문제의 출력 조건은 형변환 결과대로 나오면 안 되고 각각의 숫자가 붙어서 나와야 했기에 직접 다시 join 함수로 만들어주어야 했다.
  • 매번 반성하는 점이지만 문제를 잘 읽고 해석하는 것이 중요하다는 것을 다시한번 깨달았다. 이번에는 입출력 조건을 빠뜨렸기 때문이다.

    동시에 주어진 조건들을 잘 조합하는 것도 중요하지만 이를 효율적으로 짤 수 있도록 해야 하는 것 같다.

    1. 꼭 필요한 기능을 먼저 체크하고 주기능 위주로 큰 틀 구성하기
    2. 주기능을 보충하는 설명들을 위주로 채워나가면서,
    3. 입출력 조건에 맞게 코드를 구성하기
    4. 코드를 다 구성한 후 비효율적인 부분 개선시키기

    ⇒ 이 부분을 최대한 여러 번 연습해보고 연습할 때마다 써먹으려고 노력해야겠다.

배운 점

  1. 리스트 vs deque
    • 리스트는 **슬라이싱으로 뒤집으면 O(N)**이지만, deque는 **O(1)**로 효율적이다.
  2. 뒤집기 플래그 활용
    • 플래그를 사용해 실제로 배열을 뒤집지 않아도 뒤집힌 상태를 표현할 수 있다.
  3. 문자열 파싱 및 리스트 변환
    • [1,2,3] 형태를 map(int, nums[1:-1].split(","))로 쉽게 변환할 수 있다.

Copy link
Member

@YoonYn9915 YoonYn9915 left a comment

Choose a reason for hiding this comment

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

이번 과제문제를 하면서 아래 두 가지 부분에서 어려움이 있어서 구현 실패했는데, 블로그 및 민정님 PR에 작성한 플로우 참고하며 이해했습니다!

일주일쯤 지나서 까먹을 만할때 다시 풀어봐야 겠어요. 이번주 고생하셨습니다 👍

  1. 현재 노드를 우수 마을로 선정한 경우와 선정하지 않은 경우, 두 가지 경우의 최댓값을 저장하기 위해 dp배열을 2차원으로 설정한다.
  2. 현재 노드가 우수 마을이 아닌 경우를 구현하는 점화식 관련 코드
dp[current][0] += max(dp[child][0], dp[child][1]) 

@Mingguriguri Mingguriguri merged commit b8ae06a into main Jan 7, 2025
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.

2 participants