Skip to content

Conversation

@Mingguriguri
Copy link
Collaborator

@Mingguriguri Mingguriguri commented Nov 11, 2024

주 목표 문제 수: 3개

푼 문제


백준 #3665. 최종 순위: 위상정렬 / 골드1

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

🚩플로우 (선택)

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

  1. 초기 설정
    1. 입력을 통해 테스트 케이스 개수(t)를 받아 반복한다.
    2. 각 테스트 케이스마다 팀 수(n)와 작년 순위 정보(info)를 받아서 초기 그래프(graph)와 진입차수(indegree) 리스트를 초기화한다.
  2. 작년 순위를 기반으로 그래프와 진입차수 설정
    1. 작년 순위에 따라 팀들 간의 상대적인 순위를 그래프로 표현한다.
    2. graph[i]에는 i번 팀보다 뒤에 있는 팀들의 리스트를 추가하고, 각 팀의 진입차수를 기록한다.
  3. 올해 순위 변경 정보를 기반으로 그래프 수정
    1. 순위가 변경된 쌍의 개수(m)를 받아 각 쌍마다 간선 방향을 바꾼다.
    2. 이때 기존에 ba보다 뒤에 있는 경우에는 순위가 변경된 경우이므로, 기존 간선을 제거하고 새 순서로 간선 추가한다.
    3. 이런 경우가 아닌 경우에는 간선 방향을 다시 반대로 바꿔준다.
  4. 위상정렬 수행
    1. topology_sort 함수에서 위상정렬을 수행하여 올해의 순위를 결정한다.
    2. 진입차수가 0인 팀들을 queue에 넣어가며 순서대로 처리하고 queue가 빌 때가지 반복한다.
      1. 큐에 노드를 꺼내 최종 순위 리스트인 ranking 에 추가한다.
      2. 해당 노드와 연결된 다른 노드들의 진입차수를 1씩 감소한다.
      3. 만약 진입차수가 0이 된 노드가 있다면 이를 큐에 삽입한다.
      4. 만약 진입차수가 음수가 되는 경우가 있다면 잘못된 정보로 간주하고 IMPOSSIBLE을 출력할 수 있도록 표시한다.
  5. 결과 출력
    • 진입차수가 음수가 되는 경우가 있거나 정렬 결과와 입력된 팀 수가 일치하지 않으면 IMPOSSIBLE을 출력한다.
    • 그게 아니라면, 확실한 순사가 결정된 경우이므로 ranking 리스트를 문자열로 바꾸어 출력한다.

🚩제출한 코드

import sys
from collections import deque

input = sys.stdin.readline

'''
1. 일반적인 위상정렬 구현
2. 예외조건 추가 : 일관성이 없는 정보일 경우(IMPOSSIBEL) / 발표된 정보만 가지고 확실한 순위를 만들 수 없는 경우(?)
'''

t = int(input())    # 테스트케이스 개수
for _ in range(t):
    # 각 테스트 케이스에 대해 팀의 수와 작년 순위 정보를 입력받음
    n = int(input())  # 팀의 수
    info = list(map(int, input().split()))  # 작년에 i등한 순서 정보

    # 초기 그래프와 진입차수 초기화
    graph = [[] for _ in range(n + 1)]
    indegree = [0] * (n + 1)  # 진입차수 0으로 초기화

    # 작년 순위 정보를 바탕으로 그래프 생성
    for i in range(1, n + 1):
        for j in range(i + 1, n + 1):
            graph[i].append(info[j - 1])
            indegree[info[j - 1]] += 1

    # 순위가 변경된 팀 쌍의 수
    m = int(input())  # 상대적인 등수가 바뀐 쌍의 수 m
    for _ in range(m):
        a, b = map(int, input().split()) # 순위가 변경된 두 팀

        # 간선 방향을 바꾸기 위한 처리
        a_idx = info.index(a) + 1
        b_idx = info.index(b) + 1

        # 기존에 b가 a보다 뒤에 있는 경우: 간선을 제거하고 새 순서로 간선 추가
        if a in graph[b_idx]:
            # 더 높았던 순위에서 제거
            graph[b_idx].remove(a)
            indegree[b] += 1
            # 새로 갱신된 순위 반영
            graph[a_idx].append(b)
            indegree[a] -= 1
        else:  # 반대 경우: 간선 방향을 다시 바꿔야 하는 경우
            graph[a_idx].remove(b)
            indegree[b] -= 1
            graph[b_idx].append(a)
            indegree[a] += 1


    # 위상 정렬 수행
    def topology_sort():
        ranking = []  # 최종 순위를 저장할 리스트
        queue = deque()

        # 큐에 진입차수가 0인 팀(노드)을 모두 추가
        for i in range(1, len(indegree)):
            if indegree[i] == 0:
                queue.append(i)

        # 진입 차수가 0인 정점이 없어 큐가 비어 있다면, 순위를 결정할 수 없다는 의미로 "IMPOSSIBLE" 출력
        if not queue:
            print("IMPOSSIBLE")
            return

        able = True  # 순위 결정 가능 여부

        # 큐가 빌 때까지 반복
        while queue:
            # 큐에서 노드를 꺼내고 최종 순위에 추가
            now = queue.popleft()
            ranking.append(now)

            # 해당 노드와 연결된 다른 노드들의 진입차수를 1씩 감소
            for i in graph[info.index(now) + 1]:
                indegree[i] -= 1
                # 진입차수가 0이 된 노드를 큐에 삽입
                if indegree[i] == 0:
                    queue.append(i)
                # 진입차수가 음수가 되는 경우는 잘못된 정보로 간주
                elif indegree[i] < 0:
                    able = False
                    break

        # 출력 조건을 확인하여 결과 출력
        if not able or n != len(ranking):  # 전체 노드 개수와 결과의 노드 개수가 다를 경우
            print("IMPOSSIBLE")
        else:
            # 확실한 순서가 결정된 경우 순위를 출력
            print(" ".join(map(str, ranking)))

    topology_sort()

💡TIL

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

  • IMPOSSIBLE을 출력해야 하는데 자꾸 IMPOSSIBE을 출력해서 틀렸다고 나왔다.. 오타를 꼭 주의하자!
  • 그리고 출력한 후 마지막 줄에 print()를 추가했더니 ‘출력 형식이 잘못되었습니다’라고 나왔다. 이런 경우도 주의하도록 하자..!
  • ?나 IMPOSSIBLE이 나와야 하는 조건을 생각해내는게 어려웠다. “애매모호한 경우”, “일관성이 없는 경우”라는 텍스트를 봤을 때 “진입차수 0이 여러 개인 경우”이구나, “사이클이 생겨서 그래프 개수가 일하지 않겠구나”를 떠올리지 못했다. 그래서 힌트를 보고 풀게 되었다. 이 정도 정보만을 가지고 조건을 확장하여 생각하는 연습이 필요할 것 같다.
  • 처음에 간선 정보를 바꿀 때 예외의 경우에 대해 만들지 못해 애를 먹었다. 즉, 일관성이 없는 경우에 대한 간성 정보를 어떻게 처리해야 할 지 몰라 그냥 원래 해야 하는 것의 정반대로 수행했는데 그게 맞는 풀이였다. 그게 왜 맞는지 좀 더 고민해봐야겠다. 어찌보면 당연한 거겠지만..

백준 #2644. 촌수계산: DFSBFS / 실버2

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

🚩플로우 (선택)

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

  1. 입력 변수를 초기화하고 그래프를 연결한다.
    1. 전체 사람 수 n, 촌수를 계산할 두 사람의 번호 ab를 입력받고, 부모-자식 관계의 개수 m줄만큼 x, y를 입력받아 graph를 연결한다.
  2. 방문 여부를 기록할 visited 리스트를 초기화한다.
  3. 결과를 담을 변수 result는 -1로 설정하여 촌수를 찾지 못한 경우를 대비한다.
  4. 백트래킹을 시작한다.
    1. 시작 노드는 a로 지정하고 탐색을 시작한다.
    2. 각 노드에 대해 방문 여부를 체크하고, 방문한 노드는 level+1을 넘겨줘서 촌수를 계산한다.
    3. 만약 탐색 중인 노드가 b라면, result 변수에 level을 업데이트하고 재귀 호출을 종료한다.
  5. 탐색한 결과인 result를 출력한다.
  • BFS
  1. 입력 변수를 초기화하고 그래프를 연결한다.
    1. 전체 사람 수 n, 촌수를 계산할 두 사람의 번호 ab를 입력받고, 부모-자식 관계의 개수 m줄만큼 x, y를 입력받아 graph를 연결한다.
  2. 방문 여부를 기록할 visited 리스트를 초기화하고, 시작 노드 a부터 탐색할 수 있도록 BFS 큐를 설정한다.
    1. 큐에는 시작 노드와 dist 값을 (a, 0) 형태로 추가한다.
  3. BFS 탐색을 시작한다.
    1. 큐에서 노드와 거리 dist를 꺼내고, 현재 노드가 목적지 b와 같은지 확인한다.
    2. 현재 노드가 b일 경우, 최단 거리 dist를 반환하여 촌수 계산을 완료한다.
    3. 현재 노드가 b가 아닐 경우, 해당 노드와 연결된 모든 이웃 노드를 확인한다.
  4. 이웃 노드 중 방문하지 않은 노드는 방문 처리 후 큐에 (neighbor, dist + 1)로 추가하여 다음 탐색 대기로 설정한다.
  5. 큐가 비어 탐색이 끝날 때까지 목적지 b에 도달하지 못한 경우, 두 사람이 연결되지 않았음을 의미하므로 -1을 반환한다.
  6. 탐색 결과를 출력한다.

🚩제출한 코드

DFS

# 2644-촌수계산. DFS(백트래킹) 풀이
import sys

input = sys.stdin.readline

n = int(input())                    # 전체 사람 수
a, b = map(int, input().split())    # 촌수 계산해야 하는 두 사람의 번호
m = int(input())                    # 부모 자식들간의 관계의 개수
graph = [[] for _ in range(n + 1)]  # 그래프
visited = [False for _ in range(n + 1)]  # 방문여부
for _ in range(m):
    x, y = map(int, input().split())
    # 그래프 연결
    graph[x].append(y)
    graph[y].append(x)

result = -1  # 기본값은 -1로 설정하여 찾지 못한 경우에 대비

def backtracking(v, level):
    global result
    # 재귀함수 마치는 조건: a,b 촌수를 계산이 되었을 때
    if v == b:
        result = level
        return

    # 탐색
    for i in graph[v]:
        if not visited[i]:
            visited[i] = True
            backtracking(i, level + 1)  # 자식 노드 방문 level + 1을 넘겨줌으로써 촌수 계산
            visited[i] = False          # 방문했다면 부모노드 다시 방문 기록 지움


visited[a] = True # 초기 노드 방문 체크
backtracking(a, 0)
print(result)

BFS

# 2644-촌수계산. BFS 풀이
import sys
from collections import deque

input = sys.stdin.readline

n = int(input())                    # 전체 사람 수
a, b = map(int, input().split())    # 촌수 계산할 두 사람의 번호
m = int(input())                    # 부모-자식 관계의 개수
graph = [[] for _ in range(n + 1)]  # 그래프 초기화
visited = [False for _ in range(n + 1)]  # 방문 여부 초기화
for _ in range(m):
    x, y = map(int, input().split())
    graph[x].append(y)
    graph[y].append(x)


def bfs(start, end):
    queue = deque([(start, 0)]) # (노드, 거리) 형태로 초기화
    visited[start] = True       # 시작 노드 방문 표시

    while queue:
        v, dist = queue.popleft()

        # 목표 노드에 도달 시 거리 반환
        if v == end:
            return dist

        for neighbor in graph[v]:
            if not visited[neighbor]:
                visited[neighbor] = True
                queue.append((neighbor, dist + 1))  # 거리 1 증가하여 큐에 추가

    return -1  # 연결되지 않았을 경우 -1 반환


print(bfs(a, b))

💡TIL

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

  • 백트래킹으로 풀기 위해서는 꼭 종료 조건을 설정해야 한다.
  • 촌수 계산 문제를 처음엔 잘못 이해해서, 3촌 이후부터는 -1을 출력해야 하는 줄 알고 level > 3일 때 종료되도록 불필요한 조건을 넣어 초반에 틀렸었다. 앞으로는 종료 조건을 설정할 때 문제 내용을 다시 확인하자.
  • 재귀 호출 시 level을 매개변수로 넘기지 않고 global 키워드를 이용해 변수를 더하고 감소시키는 방식으로 관리했다가 원하는 촌수가 누락되는 문제를 겪었다. 깊이를 구할 때는 전역 변수보다는 매개변수로 전달하는 방식이 더 안정적이라는 걸 깨달았다.

백준 #7562. 나이트의 이동: DFSBFS / 실버1

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

🚩플로우 (선택)

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

  1. 나이트가 이동할 수 있는 방향을 미리 directions 리스트에 정의한다.
  2. 테스트 케이스 개수 t를 입력받아 아래 과정들을 수행한다.
  3. 각 테스트케이스를 처리하기 위해 t만큼 반복한다.
    1. 체스판의 한 변의 길이인 l, 현재 위치(cur_x, cur_y)와 이동하려는 목표 위치(fin_x, fin_y)를 입력받는다.
  4. 체스판을 만들기 위해 한 변의 길이 l 의 크기만큼 chess_map을 만든다. chess_map의 1칸은 -1로 설정한다. 이 칸을 이동할 때마다 이전 칸의 값을 더하여 이동하는 횟수를 저장하도록 한다. 처음에는 아무곳에도 이동하지 않았으므로 -1로 설정한다.
  5. BFS 탐색을 시작한다.
    1. queue에 시작 위치를 추가하고 chess_map에 시작 위치 값을 0으로 설정한다.
    2. queue가 빌 때까지 반복하며, 현재 위치에서 모든 가능한 방향으로 이동을 시도한다.
      1. 이동 위치가 chess_map의 범위를 벗어나지 않는지 확인하고 방문하지 않은 위치의 경우 이동 횟수를 갱신하고 queue에 추가한다.
      2. 목표 위치(fin_x, fin_y)에 도달하면 탐색을 중지하고 이동 횟수를 반환한다.
  6. BFS 탐색을 통해 반환받은 목표 위치까지의 최소 이동 횟수를 출력한다.

🚩제출한 코드

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

# 1. 나이트가 이동할 수 있는 8가지 방향을 정의
directions = [(0, 0), (-1, -2), (1, -2), (-2, -1), (2, -1), (-1, 2), (1, 2), (-2, 1), (2, 1)]

# 5. BFS 탐색 함수 정의
def bfs(start_x, start_y):
    # 시작 노드를 큐에 추가하고 해당 위치의 값을 0으로 설정
    queue = deque([(start_x, start_y)])
    chess_map[start_x][start_y] = 0

    # 큐가 빌 때까지 탐색
    while queue:
        x, y = queue.popleft()
        
        # 현재 위치에서 모든 방향으로 이동 시도
        for dx, dy in directions:
            nx, ny = x + dx, y + dy
            
            # 5.2.1 체스판의 범위를 넘어가지 않는지 확인
            if 0 <= nx < l and 0 <= ny < l:
                # 아직 방문하지 않은 위치라면
                if chess_map[nx][ny] == -1:
                    # 현재 위치에서 1 이동한 값으로 갱신하고 큐에 추가
                    chess_map[nx][ny] = chess_map[x][y] + 1
                    queue.append((nx, ny))
                
                # 5.2.2. 목표 지점에 도달한 경우, 이동 횟수 반환
                if nx == fin_x and ny == fin_y:
                    return chess_map[nx][ny]
                    
    return -1  # 목표 지점에 도달하지 못한 경우

# 2. 테스트 케이스 개수 입력
t = int(input())
for _ in range(t):
    # 3. 체스판의 크기, 시작 위치, 목표 위치 입력
    l = int(input())  # 체스판의 한 변의 길이 l
    cur_x, cur_y = map(int, input().split())  # 시작 위치
    fin_x, fin_y = map(int, input().split())  # 목표 위치

    # 4. 체스판 초기화 (-1로 초기화하여 미방문 표시)
    chess_map = [[-1] * l for _ in range(l)]

    # 6. BFS 탐색을 통해 결과 출력
    print(bfs(cur_x, cur_y))

💡TIL

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

  • 최단 이동 경로를 구하는 문제는 BFS가 짱이다.

  • 매번 1칸씩 이동하는 문제를 풀다가 나이트 방향대로 이동하는 문제를 푸니까 새로웠다.

    (0, 0), (-1, -2), (1, -2), (-2, -1), (2, -1), (-1, 2), (1, 2), (-2, 1), (2, 1)

  • 마지막 예제인 현재 위치와 목표 위치가 같은 경우에 자꾸 None이 나왔다. 그 이유는 코드에 방문하지 않았을 경우에, 목표지점을 발견했다면 반환하도록 코드를 짰기 때문이었다. 그래서 현재 자신의 위치를 이미 방문했다고 판단하여 아예 탐색을 하지 않고, 나머지 위치를 모두 탐색하였지만 목표지점이 발견되지 않아 반환할 게 없었던 것이다. 즉, 목표지점인지 파악하는 조건문을 방문하지 않았는지의 조건 안에다가 넣으면 안 되고, 1) 방문하지 않았는지 판단하는 조건 2) 목표지점인지 도달했는지 파악하는 조건을 각각 배치해야 한다.

    while queue:
        x, y = queue.popleft()
        for dx, dy in direction:
            nx, ny = x + dx, y + dy
            if 0 <= nx < l and 0 <= ny < l:
                if graph[nx][ny] == -1: # 아직 방문하지 않았다면
                    graph[nx][ny] = graph[x][y] + 1 # 현재 위치에서 값 추가
                    queue.append((nx, ny))
                if nx == fin_x and ny == fin_y:  # 목표 지점이라면
                    return graph[nx][ny]

백준 #18352. 특정 거리의 도시 찾기: DFSBFS / 실버2

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

🚩플로우 (선택)

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

  1. 도시의 개수 n, 도로의 개수 m, 최단거리 k, 출발도시 번호 x를 입력받고, 방문여부를 판단하는 visited 리스트와 그래프로 표현할 graph 리스트, 정답을 저장할 answer 리스트를 초기화한다. 이때 visited 리스트는 방문 여부를 판단할 뿐만 아니라 거리를 저장하는 역할로써 사용한다.
  2. graph를 단방향으로 연결한다.
  3. 출발도시인 x부터 BFS 탐색을 시작한다.
    1. 시작 도시에서 최단거리 k인 모든 도시 번호를 저장할 리스트 answer를 초기화한다.
    2. queue에 시작 위치를 추가하고 visited에 시작 위치 값을 0으로 설정한다.
    3. queue가 빌 때까지 아래 과정을 반복한다.
    4. 첫번째 노드를 pop하고, 해당 노드와 연결되어 있으며, 방문하지 않고, 첫번째로 시작한 노드가 아닌 연결된 노드라면, queue에 추가하고, 방문 표시를 한다. 이때 방문 표시는 현재 노드의 거리에 +1을 해준다.
    5. 동시에 만약 연결된 노드의 거리(visited[connected_node])가 최단거리 k와 같다면 answer 리스트에 추가한다.
    6. 탐색이 끝나면 answer 리스트를 반환한다.
  4. answer리스트가 존재하지 않는 경우는 최단 거리 k인 도시가 하나도 존재하지 않는 경우이기 때문에 -1을 출력하고, 존재하는 경우에는 오름차순으로 정렬한 후 한 줄에 하나씩 출력한다.

🚩제출한 코드

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

def bfs(start):
    queue = deque([start])
    answer = []             # 시작 도시에서 최단거리 k인 모든 도시 번호를 저장할 리스트
    visited[start] = 0      # 시작 노드 방문 표시 (시작노드는 거리 0)

    while queue:
        current = queue.popleft()
        
        # 현재 노드와 연결된 노드 탐색
        for node in graph[current]:
            if visited[node] == -1: # 아직 방문하지 않았다면 방문 처리하고 큐에 추가
                visited[node] = visited[current] + 1 # 현재 노드의 거리 + 1
                queue.append(node)
                
                # 연결된 노드의 거리가 k와 같을 경우 answer에 추가
                if visited[node] == k:
                    answer.append(node)
    return answer

# 도시의 개수 n, 도로의 개수 m, 거리 정보 k, 출발도시 번호 x
n, m, k, x = map(int, input().split())
graph = [[] for _ in range(n+1)] # 그래프
visited = [-1] * (n+1)

# 그래프 연결
for _ in range(m):
    a, b = map(int, input().split())
    graph[a].append(b)

# 출발도시 번호 x부터 BFS 탐색
answer = bfs(x)
if answer: 
    for ans in sorted(answer):
        print(ans)
else: 
    print(-1) # 최단거리 k인 도시가 하나도 존재하지 않으면 -1 출력

💡TIL

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

  • 출력할 때에는 오름차순으로 정렬 후 출력하라고 문제에 나와있었다. 문제 조건만 보는 게 아니라 출력 조건도 잘 살펴봐야겠다.
  • 예시 답변이 모두 정답임에도 불구하고 “틀렸습니다”가 계속 반복해서 나왔다. 그 이유는 시작하는 노드를 다시 재방문하게 되는 경우를 고려하지 않았고, 시작하는 노드의 방문 표시를 하지 않았기 때문이다. 시작하는 노드도 방문 표시해줘야 한다는 점 잊지 말자!

백준 #25195. Yes or yes: DFSBFS / 골드4

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

🚩플로우 (선택)

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

  1. sys.setrecursionlimit(10**6)을 통해 깊은 재귀 호출이 가능하도록 설정한다.
  2. 초기화를 한다.
    1. 입력받은 n, m에 따라 각 노드의 간선 정보를 인접 리스트 graph에 저장한다.
    2. 팬클럽 곰곰이가 존재하는 정점 수 s와 각 팬클럽 노드 정보를 리스트 gomgom에 저장한다.
  3. 1번 노드에서부터 DFS 탐색을 시작한다.
  4. DFS 함수
    1. 함수 내에서 현재 노드를 방문하지 않았고, 팬클럽이 없는 경우 방문처리한다. 하지만 팬클럽인 노드라면 False를 반환하여 더이상 탐색을 진행하지 않는다.
    2. 탐색 종료 조건을 설정한다. 현재 노드의 인접 리스트가 비어 있다면 더 이상 이동할 곳이 없으므로 True를 반환하여 탐색을 종료한다.
    3. 현재 노드의 인접 리스트를 순회하면서, 방문하지 않았고 팬클럽이 없는 노드에 대해 재귀호출한다.
    4. 재귀호출의 결과가 True라면 경료가 유효하므로 해당 결과를 반환한다.
    5. 탐색 후에는 다시 방문여부를 초기화하여 다른 경로를 탐색할 수 있도록 처리한다.
  5. DFS 함수의 호출 결과가 True라면, 팬클럽 곰곰이를 만나지 않고 이동할 수 있는 경우이므로 yes를 출력한다. False라면, 어떤 경로든 곰곰이를 만나므로 Yes를 출력한다.

🚩제출한 코드

import sys
sys.setrecursionlimit(10**6) # 재귀 한도

input = sys.stdin.readline

def dfs(current):
    if not visited[current]:
        if current not in gomgom: # 해당 노드에 팬클럽 곰곰이가 없다면, 방문 표시
            visited[current] = True
        else: 
            # 시작부터 곰곰이 있기 때문에 True가 나올 수가 없기에 False 반환
            return False

    if not graph[current]:  # 더이상 탐색할 노드가 없다면, 팬클럽을 만나지 않아도 되기 때문에 True 반환
        return True

    for node in graph[current]:
        if not visited[node]:
            visited[node] = True
            if node not in gomgom: # 인접 노드에 팬클럽이 없다면, 재귀호출
                if dfs(node): # 팬클럽을 만나지 않고 도착할 수 있는 경우 True 반환
                    return True
            visited[node] = False # 다른 경로를 탐색하기 위해 방문취소

    return False

# 정점의 개수 n, 간선의 개수 m
n, m = map(int, input().split())
graph = [[] for _ in range(n+1)]
visited = [False] * (n+1)
for _ in range(m):
    u, v = map(int, input().split())
    graph[u].append(v) # u에서 v로의 단방향 간선 추가

# 팬클럽 곰곰이 정보
s = int(input())                            # 곰곰이가가 존재하는 정점의 개수 s
gomgom = list(map(int, input().split()))    # 공곰이가 존재하는 정점의 번호 목록

# 1번 정점에서 dfs 탐색 시작
if dfs(1):  # 팬클럽 곰곰이를 만나지 않고 이동할 수 있는 경우
    print("yes")
else: # 어떻게 이동하는 곰곰이를 만나게 되는 경우
    print("Yes")

💡TIL

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

  • 사이클이 존재하지 않을 경우에는 그래프를 연결할 때 한 쪽만 연결해주면 된다.

  • 다른 분의 코드를 보고 코드를 보고 파이썬에서도 main 함수 코드를 만들 수 있다는 것을 알게 되었다. 파이썬은 main 함수가 따로 존재하지 않는다. 하지만 if __name__ == '__main__': 으로 시작하면 main 함수의 역할을 대신할 수 있다. 즉, python 파일을 재사용ㄱ ㅏ능ㅇ한 모듈 또는 독립 실행형 프로그램으로 사용할 수 있다.

    출처: https://www.entity.co.kr/entry/51-Python-Main-%ED%95%A8%EC%88%98-%EB%B0%8F-%EB%A9%94%EC%86%8C%EB%93%9C-def-Main%EC%9D%98-%EC%9D%B4%ED%95%B4

  • 재귀 깊이 한도는 그냥 하는 건 줄 알았는데 다른 분의 블로그를 보면서 왜 쓰는지 알게 되었다. 파이썬의 기본 재귀 깊이 제한은 1000이다. 정점의 개수가 1000이 넘어간다면 꼭 재귀 깊이 한도를 설정해주어야 한다.

  • 다른 분의 코드를 보고 내 코드가 너무 쓸데없이 조건이 긴 것 같다는 생각이 들었다. 문제를 푸는 데는 성공했지만 더 나은 코드를 만들기 위해 노력해야겠다.

  • 더이상 탐색할 필요가 없으면 exit(0)를 이용해 프로그램 자체를 종료시키자.


백준 #2573. 빙산: DFSBFS / 골드4

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

🚩플로우 (선택)

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

  1. 이차원 배열의 행과 열의 개수를 nm에 저장한다.
  2. 빙산 높이 정보를 저장할 2차원 배열을 생성하여 iceberg에 저장한다.
  3. 연도(시간)을 year 변수에 0으로 초기화한다.
  4. 빙산이 분리될 때까지 무한 반복한다.
    1. year를 1 증가시킨다.
    2. 빙산 높이를 감소하는 함수 (get_iceberg_height) 를 호출한다.
      1. 감소된 빙산 높이를 저장할 새로운 배열 new_height를 초기화한다.
      2. 이중 for문을 돌며 빙산이 있는 칸의 상하좌우를 탐색한다.
      3. 만약 범위 안에 있고, 상하좌우에 0(바다)이 있다면, water_count를 1증가시킨다.
      4. 바다에 접한 개수 water_count 만큼 빙산 높이를 감소시키고 최소 0으로 유지한다.
      5. 새로 만든 new_height 를 반환하고 iceberg에 업데이트한다.
    3. 현재 빙산의 덩어리 개수를 get_iceberg_height를 호출하여 확인한다.
      1. 방문여부를 기록할 visited 배열을 초기화한다.
      2. 빙산 덩어리 수인 iceberg_count를 0으로 초기화한다.
      3. 이중 for문을 돌면서, 빙산이 있고 아직 방문하지 않은 경우 새로운 덩어리로 간주한다.
      4. iceberg_count를 1증가시킨다.
      5. 이후 BFS를 이용해 연결된 빙산들을 모두 방문처리한다.
      6. queue를 초기화하고 첫 위치 방문여부를 True로 설정한다.
      7. queue가 있는 동안 아래 과정을 반복한다.
      8. 상하좌우를 탐색하면서 빙산이 범위 안에 있고, 아직 방문하지 않았고, 빙산인지 판단하여 방문처리를 한다. 이를 queue에 추가한다.
      9. 탐색이 다 끝났다면 빙산 덩어리 수 iceberg_count를 반환한다.
    4. 빙산 덩어리의 개수 iceberg_count가 조건에 맞는지 확인한다.
      1. iceberg_count가 2개 이상이면 연도를 출력하고 종료한다.
      2. 모든 빙산이 녹을 때까지 iceberg_count가 0이라면 0을 출력하고 종료한다.

🚩제출한 코드

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

# 이차원 배열의 행과 열 개수 입력
n, m = map(int, input().split())

# 빙산 높이 정보를 저장할 2차원 배열 생성
iceberg = [list(map(int, input().split())) for _ in range(n)]

# 빙산 높이 감소를 계산하는 함수
def get_iceberg_height(iceberg):
    # 감소된 빙산 높이를 저장할 새로운 배열 초기화
    new_height = [[0] * m for _ in range(n)]
    for i in range(n):
        for j in range(m):
            if iceberg[i][j] > 0:  # 빙산이 있는 칸만 처리
                water_count = 0  # 인접한 바다 칸 개수 초기화

                # 상하좌우 네 방향 탐색
                for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
                    ni, nj = i + dx, j + dy
                    if 0 <= ni < n and 0 <= nj < m and iceberg[ni][nj] == 0:
                        water_count += 1

                # 바다에 접한 개수만큼 빙산 높이를 감소시키며 최소 0으로 유지
                new_height[i][j] = max(iceberg[i][j] - water_count, 0)
    return new_height

# BFS를 통해 빙산 덩어리 수를 계산하는 함수
def count_icebergs(iceberg):
    visited = [[False] * m for _ in range(n)]  # 방문 여부를 기록할 배열 초기화
    iceberg_count = 0  # 빙산 덩어리 수 초기화

    for i in range(n):
        for j in range(m):
            # 빙산이 있고 아직 방문하지 않은 경우 새로운 덩어리로 간주
            if iceberg[i][j] > 0 and not visited[i][j]:
                iceberg_count += 1
                # BFS로 연결된 빙산을 모두 방문 처리
                queue = deque([(i, j)])
                visited[i][j] = True
                while queue:
                    x, y = queue.popleft()
                    for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
                        nx, ny = x + dx, y + dy
                        # 빙산이 연결된 인접 노드를 방문 처리
                        if 0 <= nx < n and 0 <= ny < m and not visited[nx][ny] and iceberg[nx][ny] > 0:
                            visited[nx][ny] = True
                            queue.append((nx, ny))
    return iceberg_count

# 연도(시간) 초기화
year = 0

# 빙산이 분리될 때까지 반복
while True:
    year += 1

    # 빙산 높이 감소 계산 및 업데이트
    iceberg = get_iceberg_height(iceberg)

    # 현재 빙산 덩어리 수 계산
    mountain = count_icebergs(iceberg)

    # 빙산 덩어리가 2개 이상으로 분리된 경우
    if mountain >= 2:
        print(year)
        exit(0)

    # 빙산이 모두 녹아 덩어리가 없는 경우
    if mountain == 0:
        print(0)
        exit(0)

💡TIL

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

  • 문제 해결을 위한 큰 틀을 미리 정리하는 것이 중요하다는 것을 깨달았다. 처음에는 문제에서 필요한 요소들을 정리하지 않고 무작정 코드를 작성하기 시작했는데, 결국 흐름이 정리되지 않아 코드가 복잡해졌다. 앞으로는 문제의 핵심을 파악하고 필요한 단계를 정리한 후에 코드를 작성해야겠다고 느꼈다.

  • 상하좌우 탐색을 동시에 수행하는 구조를 다루는 데 익숙하지 않아서 시행착오가 있었다. 매년 빙산의 높이를 바다와 인접한 개수만큼 줄이고, 그다음에 BFS로 덩어리를 탐색해야 하는 문제였기에 한 번에 여러 작업을 처리하는 방법을 익히는 데 도움이 되었다.

  • BFS를 통해 연결된 덩어리 수를 세는 방법을 알게 되었다. 이 문제는 매년 빙산의 덩어리 개수를 확인해야 하는데, BFS로 한 덩어리씩 탐색하면서 방문 처리를 통해 덩어리를 세는 방법을 배울 수 있었다.

  • 2차원 리스트 입력을 간편하게 받는 방식을 새로 알게 되었다. 기존에는 반복문을 통해 2차원 리스트를 하나씩 추가했는데, 한 줄로 리스트를 생성할 수 있어 코드가 훨씬 간결해졌다. 잊지 말고 나중에 유용하게 써먹어야겠다.

    # 이전에 쓰던 방식
    iceberg = [] # 빙산
    for _ in range(n):
        temp = list(map(int, input().split()))
        iceberg.append(temp)
        
    # 새로 알게 된 방식
    iceberg = [list(map(int, input().split())) for _ in range(n)]

백준 #7569. 토마토: DFSBFS / 골드5

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

🚩플로우 (선택)

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

  1. bfs() 함수: 3차원 BFS 탐색 함수

    1. 큐를 생성하고, 초기 상태에서 익은 토마토들의 위치를 큐에 추가한다. (층, 행, 열, 일수)가 들어간다.
    2. 큐 빌 때까지 아래 과정을 반복한다.
      1. 큐가 빌 때까지 반복하면서 각 위치에서 익지 않은 토마토에 익은 토마토의 영향을 전파한다. 이때 일수를 1씩 증가시키면서 최대 일수를 추적한다.
      2. 모든 토마토가 익은 후의 일수를 반환한다.
  2. all_ripe() 함수: 모든 토마토가 있었는지 확인하는 함수

    3중 for문으로 전체 배열을 순회하며 익지 않은 토마토가 하나라도 있으면 False를, 그렇지 않으면 True를 반환한다.

  3. 메인 코드 흐름:

    1. H, N, M을 입력받아 3차원 배열 tomatoes를 생성한다.
    2. 상하좌우앞뒤의 6방향을 나타내는 directions 리스트를 설정한다.
    3. 초기 상태에서 all_ripe()를 호출해 모든 토마토가 이미 익었으면 0을 출력하고 종료한다.
    4. 그렇지 않으면 bfs()를 호출하여 최소 일수를 계산하고, 모든 토마토가 익었는지 최종적으로 확인한다.
    5. 모든 토마토가 익었으면 일수를 출력하고, 아니면 -1을 출력한다.

🚩제출한 코드

import sys
from collections import deque

input = sys.stdin.readline

# 상자의 가로 m, 세로 n, 높이 h
m, n, h = map(int, input().split())
tomatoes = []

for _ in range(h):
    layer = [list(map(int, input().split())) for _ in range(n)]
    tomatoes.append(layer)


# 상하좌우앞뒤 방향
directions = [(0, 0, 1), (0, 0, -1), (0, -1, 0), (0, 1, 0), (1, 0, 0), (-1, 0, 0)] # 상하좌우앞뒤

def bfs():
    queue = deque() # (층, 행, 열, 일수)

    # 초기 익은 토마토 위치 큐에 추가
    for z in range(h):
        for x in range(n):
            for y in range(m):
                if tomatoes[z][x][y] == 1:
                    queue.append((z, x, y, 0))  # (층, 행, 열, 일수)

    max_day = 0  # 익는 데 걸린 최대 일수 추적

    while queue:
        z, x, y, day = queue.popleft()
        max_day = max(max_day, day)  # 가장 오래 걸린 일수 갱신

        for dz, dx, dy in directions:
            nz, nx, ny = z + dz, x + dx, y + dy
            if 0 <= nz < h and 0 <= nx < n and 0 <= ny < m:
                if tomatoes[nz][nx][ny] == 0:  # 익지 않은 토마토일 때
                    tomatoes[nz][nx][ny] = 1
                    queue.append((nz, nx, ny, day + 1))  # 익는 데 하루 추가


    return max_day

def all_ripe():
    # 모든 토마토가 익었는지 확인
    for i in range(h):
        for j in range(n):
            for k in range(m):
                if tomatoes[i][j][k] == 0:
                    return False
    return True

# 초기 상태 확인
if all_ripe():
    print(0)
    exit(0)
else:
    days = bfs()

    # 모든 토마토가 익었는지 다시 확인
    if all_ripe():
        print(days)
    else:
        print(-1)

💡TIL

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

  • 어려워서 주어진 시간 내에 혼자 처리하지 못하고 결국 도움을 받아 해결하였다.. 접근방식 자체가 어려웠던지라 많이 배우게 된 문제였다. 더불어 너무 어렵게 생각해서는 안 되겠다는 생각이 들었다. 무엇보다 수도코드를 설정하고 접근하면 너무 쉽게 풀릴텐데 수도코드를 처음에 짜는 게 어려운 것 같다. 훈련이 필요한 것 같다.

  • 처음에 3차원으로 입력받는 것이 헷갈려서 아래와 같이 짰는데 입력받는 것을 잘못해서 예제가 제대로 출력이 되지 않았다. 근데 당연히 입력에는 문제가 없을 것이라고 판단하여 로직만 계속 만지작 거렸다.

    temp = [list(map(int, input().split())) for _ in range(n)]
    tomatoes = []
    print(f"temp: {temp}")
    for i in range(h):
        tomatoes.append(temp)
        print(tomatoes)

    하지만 3차원 리스트를 한번에 받으려면 아래와 같이 진행하면 된다는 것을 알게 되었다.

    for _ in range(h):
        layer = [list(map(int, input().split())) for _ in range(n)]
        tomatoes.append(layer)

    n줄만큼 입력하고 나면 h번만큼 반복하여 tomatoes에 추가된다.

    BFS 문제에서, 특히 다차원 배열을 다룰 때는 각 차원의 입력을 제대로 다루는 것이 중요하다는 것을 느꼈다.

  • 새로운 BFS 유형을 경험해보게 된 것 같다.


백준 #27961. 고양이는 많을수록 좋다 : 그리디 / 브론즈1

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

🚩플로우 (선택)

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

  1. 목표 고양이 수 n을 입력받는다.
  2. n이 0인 경우 0을 출력하고 종료한다.
  3. 최소 행동 횟수 madoka와 현재 고양이 수 cat을 각각 1로 초기화한다.
  4. cat이 목표 고양이 수 n보다 같거나 커질 때까지 계속해서 복제 마법(cat *= 2)을 사용하고, 행동 횟수 madoka를 증가시킨다.
  5. catn과 같아지거나 이를 초과하면 반복을 종료하고 madoka를 출력한다.

🚩제출한 코드

import sys
input = sys.stdin.readline

n = int(input()) # 목표 고양이 수

if n == 0:
    print(0)
    exit(0)

madoka = 1  # 마도카를 위한 최소의 행동 횟수
cat = 1     # 현재 고양이 수

while cat < n:
    cat *= 2
    madoka += 1

print(madoka)

💡TIL

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

  • 그리디 알고리즘은 매우 오랜만에 풀어봤다. 코드는 간단해 보였지만 최적의 방식을 찾는 데 꽤 오래 걸렸다. 특히 제한 시간과 메모리 제한을 함께 고려하면서 최적화를 진행하는 것이 중요하다는 것을 다시 느꼈다. 이중 반복문에서 헤어나지 못한 초기 시도가 특히 그랬다. 앞으로는 조건에 따라 이중 반복문을 안 쓰는 방향으로 최적의 답을 찾을 수 있도록 방향을 잡아나가야겠다.
  • 또한 그리디 알고리즘 문제일수록, 예제를 넘어 조건에 따라 다양한 테스트 케이스를 적용해 봐야 한다는 것을 느꼈다. 특히 이번 문제에서는 “홀수”에 대한 테스트가 부족해 정확한 로직을 찾지 못한 점이 아쉬웠다.
  • 코테 스터디 중 다수가 금방 문제의 아이디어를 떠올리셨던 것 같고, 권장시간 1시간 15분도 너무 많이 남는다고 하셨다. 난 아이디어를 떠올리기까지 최적의 방식이 아닌 그냥 해결법만 떠올리고 우왕자왕 했던 것이 반성이 됐다.

백준 #14917. 거스름돈: 그리디 / 실버5

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

🚩플로우 (선택)

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

  • 그리디
    "가능한 한 5원짜리 동전을 우선 사용하고, 나머지를 2원짜리로 채운다."
  1. 거스름돈 액수 n을 입력받는다.
  2. 거스름돈을 줄 수 없는 경우(1, 3)는 예외로 -1을 출력한다.
  3. 5로 나누어 떨어진다면 5로 나눈 몫을 출력한다.
  4. 그 외의 경우, n이 5보다 크고 (n-5)를 한 값이 5의 배수거나 짝수일 경우는 5를 뺀다. 그게 아닐 경우 2를 뺀다. 이 과정을 n이 0보다 클 동안 반복하며, 한 번 반복할 때마다 cnt를 1 증가시킨다.
  5. cnt를 출력한다.
  • DP
    1~9원까지는 규칙성이 없으나 10원부터는 i원은 [i-5]원 값에 +1한 값과 같다.

따라서 점화식은 다음과 같다.

dp[i] = dp[i-5] + 1
  1. 거스름돈 액수 n을 입력받는다.
  2. dp 배열을 초기화한다.
  3. 5로 나누어 떨어진다면 5로 나눈 몫을 출력한다.
  4. 그 외의 경우, n이 5보다 크고 (n-5)를 한 값이 5의 배수거나 짝수일 경우는 5를 뺀다. 그게 아닐 경우 2를 뺀다. 이 과정을 n이 0보다 클 동안 반복하며, 한 번 반복할 때마다 cnt를 1 증가시킨다.
  5. cnt를 출력한다.

🚩제출한 코드

그리디

import sys
input = sys.stdin.readline

n = int(input()) # 거스름돈 액수

# 예외처리
if n == 1 or n == 3:
    print(-1)
    exit(0)
    
# 5로 나누어 떨어질 경우
if n % 5 == 0:
    print(n // 5)
    
else:
    cnt = 0
    while n > 0:
        if (n >= 5) and ((n-5) % 5 == 0 or (n-5) % 2 == 0):
            n -= 5
        else:
            n -= 2

        cnt += 1

    print(cnt)

DP

import sys
input = sys.stdin.readline

n = int(input()) # 거스름돈 액수

dp = [0] * (n+1)
#           0  1 2  3 4 5 6 7 8 9
dp[:10] = [-1,-1,1,-1,2,1,3,2,4,3]

for i in range(10,n+1):
    dp[i] = dp[i-5] + 1

print(dp[n])

💡TIL

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

  • 어제 [[백준] #27961. 고양이는 많을수록 좋다 (파이썬/Python)](https://www.notion.so/27961-Python-139dded9d6ea803da5bedeed2571d5ca?pvs=21) 문제를 풀면서 반성했던 것을 떠올리면서 풀이에 임했다. 내가 작성한 코드에 대한 테스트를 계속 수행해보고, 의심 해보면서 풀이를 작성했더니 한 번에 성공했다! 코린이로서 뿌듯하다.
  • 그리디 문제를 풀 때는 최대한 큰 단위부터 채우고 테스트를 반복하여 규칙을 찾아야 한다는 점을 다시 확인했다.
  • 그리디 문제는 계속 의심해야 한다는 것과 코딩테스트 시험에서는 최대한 마지막에 풀어야 한다는 것을 알게 되었다. 또한 DP 방식으로 접근하는 것처럼 가장 먼저 ‘나열’해보고 그 후에 규칙을 찾는 식으로 접근해도 된다는 것을 알게 되었다.
  • ‘안 될 게 뭐 있어?’라는 마인드를 가지고 규칙성을 찾는 방법과, 여러 풀이 방식을 비교하면서 효율적인 코드를 작성하는 연습을 해야겠다.

@Mingguriguri Mingguriguri self-assigned this Nov 11, 2024
@Mingguriguri Mingguriguri merged commit 80efba6 into main Nov 11, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants