Skip to content

Conversation

@learntosurf
Copy link
Collaborator

@learntosurf learntosurf commented Mar 8, 2025

🌱WIL

  • 유니온 파인드라는 개념을 처음 알게 되어 신기했다. 스터디 때 푸는 문제들이 혼자 힘으로 풀기는 어렵지만, 배워가는 것들을 잘 정리해두어야 겠다.
  • 이번주는 매일 문제를 풀었다. 매일 1문제씩 풀고 정리하는데도 시간이 꽤 걸리는 것 같다. 실버 난이도 정도의 문제들을 계속 많이 풀어보면 좋을 것 같다.

🚀주간 목표 문제 수: 5개

푼 문제


백준 #1976. 여행가자: 유니온파인드 / 골드4

정리한 링크: 바로가기

🚩플로우 (선택)

문제

  • 한국에는 $N$개의 도시가 있으며, 임의의 두 도시 사이에 길이 있을 수도 있고 없을 수도 있다.
  • 주어진 $N$x$N$ 행렬을 통해 각 도시간의 연결 여부를 확인할 수 있다. 1이면 연결되어 있고, 0이면 연결되어 있지 않다.
  • 동혁이와 친구들은 여행 계획에 따라 특정 도시들을 방문하려고 한다. 한 도시에서 다른 도시로 직접 이동하지 못하더라도, 경유지를 거쳐 이동할 수 있다.
  • 여행 경로가 가능한지(YES, NO)를 판별하는 프로그램을 작성해야 한다.
  • 도시 수 $N$ (1 ≤ N ≤ 200), 여행 계획에 포함된 도시의 수 $M$ (1 ≤ M ≤ 1,000)

시간복잡도

  • $N$, $M$의 최대값을 보아 $O(N^2)$까지 허용될 가능성이 높다.
  • 그래프 탐색을 수행하는 방식에 따라 $O(N^2)$~$O(NM)$ 정도의 복잡도를 가진 알고리즘이 적합하다.
  • 특정 두 도시가 연결되어 있는지를 판별하기 위해 BFS/DFS를 이용한 그래프 탐색을 사용할 수 있다. 하지만 $O(N^2)$의 행렬이므로 매번 탐색하면 비효율적이다.
  • Union-Find 알고리즘모든 도시를 같은 연결 요소로 그룹화하여, 여행 경로의 모든 도시가 같은 그룹인지 확인한다. 연결 요소를 미리 계산하면 $O(N^2)$의 전처리 이후, 경로 검사는 $O(M)$으로 효율적이다.

알고리즘

  • Union-Find (Disjoint Set)을 활용하여 여행 경로의 모든 도시가 같은 연결 요소(집합)에 속해 있으면 가능, 아니면 불가능으로 판단한다.

🚩제출한 코드

import sys 
input = sys.stdin.readline

N = int(input().strip()) # 도시의 수 
M = int(input().strip()) # 여행 계획에 속한 도시의 수

# 유니온 파인드 부모 배열
parent = [i for i in range(N)]  

# find 연산: 경로 압축을 이용하여 루트 노드 찾기
def find(x):
    if parent[x] != x:
        parent[x] = find(parent[x])
    return parent[x]

# union 연산: 두 집합을 합치기
def union(x, y):
    root_x = find(x)
    root_y = find(y)
    if root_x != root_y:
        parent[root_y] = root_x  # y의 루트를 x로 연결

# 도시 연결 정보 처리
for i in range(N):
    row = list(map(int, input().split()))
    for j in range(N):
        if row[j] == 1:  # 두 도시가 연결된 경우
            union(i, j)

# 여행 계획 입력
plan = list(map(int, input().split()))

# 여행 경로가 같은 집합인지 확인
root = find(plan[0] - 1)  # 첫 번째 도시의 루트
for city in plan[1:]:
    if find(city - 1) != root:
        print("NO")
        exit()

print("YES")

💡TIL

  • Union-Find (Disjoint set): 서로소 집합을 관리하는 자료구조이다.
    • Find 연산: 특정 노드가 속한 집합의 대표 노드를 찾는 연산이다. (경로 압축 사용)

      def find(x):
          if parent[x] != x:
              parent[x] = find(parent[x])  # 경로 압축 (루트까지 거슬러 올라감)
          return parent[x]
      • find(x): x가 속한 그룹의 대표 노드를 찾는다.
    • Union 연산: 두 개의 노드를 같은 집합으로 합치는 연산이다.

      def union(x, y):
          root_x = find(x)
          root_y = find(y)
          
          if root_x != root_y:
              parent[root_y] = root_x  # y의 대표 노드를 x의 대표 노드로 설정
      • union(x, y): xy가 속한 집합을 합친다.
      • root_x, root_y가 다르면 root_yroot_x의 자식으로 설정한다.
    • 초기화: parent[i]=i → 처음에는 모든 노드가 자기 자신을 대표하는 집합이다.

  • 2차원 행렬을 순회하며 1이 있으면 union(i, j)를 수행하여 같은 집합으로 묶는다. 이 과정이 끝나면 모든 연결된 도시들이 같은 그룹으로 묶인다.
  • Union-Find 알고리즘을 처음 알게 되었다. 이 문제는 BFS/DFS로도 풀 수 있지만, 해당 알고리즘을 알면 훨씬 빠르고 간단한 코드를 작성할 수 있다. 처음 접했을 때 잘 정리를 해두어야 하는 것 같다.
  • Union-Find의 원리
    • 초기에 모든 노드는 자기 자신을 대표하는 독립 집합이다.
    • union(x,y) 연산을 통해 같은 그룹으로 묶는다.
    • 모든 도시가 같은 집합인지 확인할 때 find()를 사용한다.
  • 그래프의 연결 여부를 판별하는 문제일 때 알고리즘을 떠올려 볼 수 있다.

백준 #1717. 집합의 표현: 유니온파인드 / 골드4

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

🚩플로우 (선택)

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

🚩제출한 코드

import sys
input = sys.stdin.readline
sys.setrecursionlimit(10**6)

n, m = map(int, input().split())
parent = [i for i in range(n + 1)]
rank = [0] * (n + 1)  # 트리 깊이 관리

result = []

def find(x):
    if parent[x] != x:
        parent[x] = find(parent[x])  # 경로 압축
    return parent[x]

def union(x, y):
    root_x = find(x)
    root_y = find(y)

    if root_x != root_y:
        if rank[root_x] > rank[root_y]:  # 더 랭크가 높은 루트로 합침
            parent[root_y] = root_x
        elif rank[root_x] < rank[root_y]:
            parent[root_x] = root_y
        else:
            parent[root_y] = root_x
            rank[root_x] += 1  # 같은 랭크면 한쪽의 랭크 증가


for _ in range(m):
    op, a, b = map(int, input().split())

    if op == 0:  # 합집합 연산
        union(a, b)

    elif op == 1:  # 같은 집합 여부 확인
        if find(a) == find(b):
            result.append("YES")
        else:
            result.append("NO")

sys.stdout.write("\n".join(result) + "\n")

💡TIL

백준 #1326. 폴짝폴짝: BFS/DFS / 실버2

정리한 링크: 바로가기

🚩플로우 (선택)

문제

  • 개구리가 징검다리에서 점프할 때, 해당 징검다리 숫자의 배수만큼 떨어져 있는 곳으로만 이동 가능하다.
  • 시작점 $a$에서 목표점 $b$까지 최소 점프 횟수를 구하는 문제이다.
  • 이동할 수 없는 경우 -1을 출력해야 한다.
  • 징검다리의 개수 , 각 징검다리에 쓰여 있는 $N$개의 정수 (1 ≤ N, 값 ≤ 10,000)
  • 시작점 $a$, 도착점 $b$ (1 ≤ a, b ≤ N)

알고리즘

  • 개구리는 특정 위치($i$)에서 배수(arr[i])만큼 떨어진 곳으로 점프할 수 있다. 최소 점프 횟수를 찾는 문제이므로, 최단 거리 탐색 알고리즘이 필요하다.

    BFS(Breadth-First Search)

  • BFS vs. DFS

    • BFS: 현재 노드와 인접한 노드를 먼저 모두 탐색하고, 그 다음으로 그 노드에서 인접한 노드를 탐색한다. 따라서, 목적지에 도달하면 그 거리가 최단거리임이 보장된다.
    • DFS: 현재 노드에서 연결된 모든 노드를 먼저 탐색하기 때문에, 목적지에 도달했더라도 최단거리임을 보장할 수 없다. 모든 탐색이 종료된 후에 최단거리임을 알 수 있다.
  • BFS에서는 큐(Queue)를 활용해 방문할 노드를 탐색하면서 최소 횟수를 기록한다.

시간복잡도

❌ O(N²) 방식이 비효율적인 이유

  • 한 위치($i$)에서 $N$개의 모든 위치를 탐색하는 방식 (즉, 모든 노드에서 모든 노드를 탐색하는 방식)은 $O(N²)$ 시간이 걸린다.
  • $N$이 최대 10,000이므로, $O(N²)$ = 100,000,000(1억 번 연산) → 시간 초과 발생

⭕ BFS를 이용한 O(N) 방식

  • BFS는 한 번 방문한 위치는 다시 방문하지 않고, 탐색할 노드의 수가 최대 $N$이므로 $O(N)$이 된다.
  • queue를 활용하여 한 번 방문한 곳을 다시 방문하지 않도록 하면, 각 노드는 한 번씩만 방문하게 되어 $O(N)$에 가까운 성능을 유지할 수 있다.
  • 최대 $N$=10,000이므로, $O(N)$=10,000 (1만 번 연산)
  1. 입력값을 받아 징검다리 개수 $N$과 각 징검다리에 적힌 값 arr를 저장한다.
  2. 시작 위치 $a$와 목표 위치 $b$를 입력받는다.
  3. BFS 탐색을 위한 queue를 생성하고, (현재 위치, 현재까지의 점프 횟수) 형태로 저장한다.
  4. 방문한 위치를 체크하기 위해 visited 배열을 생성한다.
  5. BFS 탐색을 수행한다.
  6. 탐색이 종료되었는데도 목표 위치 $b$에 도달하지 못하면 -1을 반환한다.

🚩제출한 코드

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

N = int(input())
arr = [0] + list(map(int, input().split())) # 1-based index 사용
a, b = map(int, input().split())

def bfs(N, arr, a, b):
    queue = deque([(a, 0)]) # (현재 위치, 점프 횟수)
    visited = [False] * (N + 1) # 방문 여부 체크 
    visited[a] = True # 시작점 방문 처리 

    while queue:
        pos, jump = queue.popleft()

        # 목표 지점에 도달하면 점프 횟수 반환 
        if pos == b:
            return jump

        step = arr[pos] # 현재 위치에서 이동할 수 있는 거리
        if step == 0: 
            continue # 이동할 수 없는 경우는 다음 탐색을 진행

        # 양방향 탐색 (오른쪽 방향)
        next_pos = pos + step  # 첫 번째 점프
        while next_pos <= N:
            if not visited[next_pos]:  # 방문하지 않은 곳만 탐색
                if next_pos == b:  # 목표 지점에 도달하면 즉시 반환
                    return jump + 1
                visited[next_pos] = True
                queue.append((next_pos, jump + 1))  # 점프 횟수 증가
            next_pos += step  # 다음 배수 위치 탐색

        # 양방향 탐색 (왼쪽 방향)
        next_pos = pos - step  # 왼쪽 방향 점프
        while next_pos >= 1:
            if not visited[next_pos]:  # 방문하지 않은 곳만 탐색
                if next_pos == b:  # 목표 지점에 도달하면 즉시 반환
                    return jump + 1
                visited[next_pos] = True
                queue.append((next_pos, jump + 1))  # 점프 횟수 증가
            next_pos -= step  # 다음 배수 위치 탐색


    # 목표 지점에 도달하지 못하면 -1 반환           
    return -1 

print(bfs(N, arr, a, b))

💡TIL

1회차

  • BFS 탐색 구현 방법을 고민했다.
    • queue에서 pos, jump를 꺼낸다.
    • pos==b라면 jump를 반환한다. (최소 점프 횟수)
    • 현재 위치에서 arr[pos]의 배수만큼 이동 가능한 위치를 탐색한다.
    • 방문하지 않은 위치만 queue에 추가하고 visited를 업데이트한다.

2회차

  • 문제에서 점프의 방향을 제한하지 않았다. 좌측 방향으로 점프를 하는 케이스까지 고려해야 한다.

    ⇒ 문제에서 이동 방향이 조건으로 명시되어 있지 않다면 왼, 오 모두 이동할 수 있다.

  • BFS 알고리즘의 구현 방법을 익힐 수 있는 문제였다. 코드를 복잡하게 짠 것 같아 다른 풀이들과 비교해보면서 최적화된 코드를 확인하고 개념을 다시 복습해보면 좋을 것 같다.

  • 문제를 풀기 전에 가능한 시간복잡도를 고려하고, 적절한 알고리즘을 선택하는 것이 아직은 익숙치 않다. 문제를 풀 때마다 생각해보면서 습관을 길러볼 것.

백준 #4963. 섬의 개수: BFS/DFS / 실버2

정리한 링크: 바로가기

🚩플로우 (선택)

문제

  • 지도가 주어지고, 1(땅)과 0(바다)로 이루어져 있다.
  • 가로, 세로, 대각선으로 연결된 1들은 같은 섬이다.
  • 지도에서 섬의 개수를 세어야 한다.
  • 지도의 너비 $w$, 높이 $h$ (1 ≤ w, h ≤ 50) (지도의 크기 $w$x$h$ 행렬)
  • (0, 0)이 입력되면 종료한다.

시간복잡도

  • 지도 크기는 최대 50x50=2500 칸이다.
  • 각 칸을 탐색하면서 연결된 칸을 탐색하는 문제이므로 그래프 탐색 (DFS/BFS)를 사용해야 한다.
  • 한번 탐색된 정점은 다시 탐색하지 않기 때문에 $O(w\times h)$개의 정점을 탐색하게 된다. 한 정점에서 8가지 방향으로 이동할 수 있기 때문에 8 x $O(w\times h)$의 시간복잡도가 소요된다.
    • 상수 배수를 무시하면 $O(w\times h)$이다.

알고리즘

  • 그래프 탐색 문제

  • 한 지점에서 상하좌우, 대각선으로 인접한 곳으로 이동한다. 연결된 모든 지점을 이동한다.

    2차원 배열에서 상하좌우 또는 상하좌우, 대각선으로 이동하는 인접탐색을 진행한다면 BFS/DFS 떠올릴 것!

  1. 입력을 받아서 2차원 리스트(지도)로 저장
  2. 모든 칸을 탐색하며 1(땅)을 찾으면, DFS 또는 BFS를 사용하여 연결된 모든 땅을 방문 처리한다.
    • DFS(재귀)를 사용하여 연결된 땅을 모두 탐색한다.
      • 인접한 노드를 큐에 넣기 전에 (1)지도 밖으로 벗어나지는 않았는지, (2)바다이거나 이미 방문된 곳은 아닌지 조건을 확인해야 한다.
      • visited 배열 없이 1→0으로 바꾸어 탐색을 처리 할 수 있다
    • 또는 BFS(큐)를 사용하여 연결된 땅을 큐에 넣고 탐색한다.
  3. 새로운 섬을 찾을 때마다 카운트를 증가시킨다.
  4. 여러개의 케이스를 처리하고, (0, 0) 입력이 들어오면 종료한다.

🚩제출한 코드

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

# 8방향 (상, 하, 좌, 우, 대각선 4개)
dx = [-1, 1, 0, 0, -1, -1, 1, 1]
dy = [0, 0, -1, 1, -1, 1, -1, 1]

def bfs(x, y, graph, w, h):
    queue = deque([(x, y)])
    graph[y][x] = 0  # 방문 처리 (1 → 0 변경)

    while queue: # 큐가 빌 때까지 반복
        cx, cy = queue.popleft() # 현재 좌표 꺼내기 
        for i in range(8):  # 8방향 탐색
            nx, ny = cx + dx[i], cy + dy[i] # 새로운 좌표 계산
            if 0 <= nx < w and 0 <= ny < h and graph[ny][nx] == 1:
                graph[ny][nx] = 0  # 방문 처리
                queue.append((nx, ny)) # 큐에 추가 (다음에 탐색할 곳)

while True:
    w, h = map(int, input().split())
    if w == 0 and h == 0:
        break  # 종료 조건
    island = [list(map(int, input().split())) for _ in range(h)]

    count = 0 # 섬의 개수 
    for y in range(h):
        for x in range(w):
            if island[y][x] == 1:  # 새로운 섬 발견
                count += 1
                bfs(x, y, island, w, h) # BFS 실행

    print(count)

💡TIL

  • 방향벡터를 사용해서 상하좌우, 대각선 이동을 시뮬레이션 하는 코드를 잘 기억해두어야 할 것 같다.
  • 해당 문제가 BFS/DFS 풀이가 모두 가능해서 두 풀이 모두 잘 익혀두면 좋을 것 같다.

백준 #27737. 버섯 농장: BFS/DFS / 실버1

정리한 링크: 바로가기

🚩플로우 (선택)

문제

  • $N$x$N$칸으로 이루어진 나무판이 있다. 각 칸은 0(버섯이 자랄 수 있다) 또는 1(버섯이 자랄 수 없다)로 이루어져 있다.

  • 버섯 포자는 0칸에만 심을 수 있다.

  • 각 버섯 포자는 포자가 심어진 칸을 포함해 최대 $K$개의 연결된(=상하 좌우로 적어도 한 변을 공유하는) (버섯이 자랄 수 있는) 칸에 버섯을 자라게 하다.

    • 포자는 한 번 심으면 $K$개의 칸을 덮을 수 있다.
    • 한 칸에 여러 개의 포자를 겹쳐서 심을 수 있다. 겹쳐 심으면 $x$ x $K$개의 연결된 칸을 덮을 수 있다.
  • 버섯 포자를 하나라도 사용하고 버섯이 자랄 수 있는 모든 칸에 버섯이 자랐을 때 농사가 가능하다.

  • **농사가 가능할지 판단(POSSIBLE, IMPOSSIBLE)**하고, 농사가 가능하면 버섯 포자의 최소 개수를 구한다.

  • $N$ (1 ≤ N ≤ 100), 버섯 포자의 개수 $M$ (1 ≤ M < 1,000,000), $K$ (1 ≤ K ≤ 10^8)

시간복잡도

  • 한번 탐색된 정점은 다시 탐색하지 않기 때문에 $O(N\times N)$개의 정점을 탐색하게 된다.
  • 한 정점에서 4가지 방향으로 이동을 할 수 있기 때문에 4*$O(N^2)$의 시간복잡도가 소요된다. 상수배수를 무시하면 $O(N^2)$이다.
  • $N$이 최대 100이므로, 격자 탐색은 $O(N^2)$=$O($10^4$)$ 정도로 수행 가능하다.
  • $M$이 최대 10^6이고, $K$가 10^8까지 가능하므로 브루트포스는 불가능하다. 대신, 각 연결 요소를 그룹화하고, $K$ 크기만큼 효율적으로 덮는 방법을 찾아야 한다.

알고리즘

  • 격자 탐색 문제이다. (BFS/DFS)
  • 2차원 배열에서 상하좌우로 인접한 곳을 탐색하는 문제로 BFS로 접근한다!
  1. 버섯이 자랄 수 있는 칸(0)의 연결 요소를 BFS/DFS로 그룹화한다.
    • 모든 0칸을 상하좌우로 이어진 그룹으로 묶는다.
    • 각 덩어리의 크기(연결된 0 개수)를 저장한다.
  2. 각 덩어리를 덮을 최소한의 포자 개수를 계산한다.
    • 한 번에 $K$칸을 덮을 수 있다.
      (포자 개수) = [덩어리 크기/$K$]
    • 모든 덩어리에 대해 필요한 포자 개수를 합산한다.
  3. 포자가 충분한지 확인한다.
    • (필요한 포자 개수) ≤ $M$이면 POSSIBLE
    • (필요한 포자 개수) ≥ $M$이면 IMPOSSIBLE

🚩제출한 코드

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

N, M, K = map(int, input().split())
boards = [list(map(int, input().split())) for _ in range(N)]

def bfs(x, y):
    queue = deque([(x, y)])
    boards[x][y] = 1  # 방문 처리
    areas = 1  # 현재 덩어리 크기

    directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]  # 상하좌우 이동

    while queue:
        x, y = queue.popleft()
        for dx, dy in directions:
            nx, ny = x + dx, y + dy
            if 0 <= nx < N and 0 <= ny < N and boards[nx][ny] == 0:
                boards[nx][ny] = 1  # 방문 처리
                queue.append((nx, ny))
                areas += 1  # 영역 개수 증가
    return areas

total_nums = 0
use_seed = False

# 버섯이 자랄 수 있는 칸 찾기
for i in range(N):
    for j in range(N):
        if boards[i][j] == 0:
            areas = bfs(i, j)  # BFS로 덩어리 크기 계산
            nums = math.ceil(areas / K)  # 필요한 포자 개수
            total_nums += nums 
            use_seed = True  # 포자 사용 가능 여부 확인

if not use_seed:
    print('IMPOSSIBLE')
elif total_nums <= M:
    print('POSSIBLE')
    print(M - total_nums)  # 남은 포자 개수 출력
else:
    print('IMPOSSIBLE')

💡TIL

  • 2차원 배열에서 상하좌우 또는 상하좌우, 대각선으로 이동하는 인접탐색을 진행하는 BFS/DFS 문제였다. 인접한 곳들의 개수를 구하는 법을 알아두자.
  • 문제 조건이 복잡하다면 구해야 하는 것들을 나누어 생각해야 한다. (인접한 땅 개수 구하기, 사용해야 할 포자 개수 구하기)

백준 #10026. 적록색약: BFS/DFS / 골드5

정리한 링크: 바로가기

🚩플로우 (선택)

문제

  • $N$x$N$ 크기의 그리드에서 R(빨강), G(초록), B(파랑) 중 하나로 색칠된 그림이 주어진다.

  • 같은 색으로 상하좌우 인접한 칸들은 같은 구역으로 본다.

    # 예제 입력
    5
    RRRBB
    GGBBB
    BBBRR
    BBRRR
    RRRRR
    
    # 예제 출력
    • 적록색약이 아닌 사람은 R, G, B를 각각 구분해서 구역을 센다.
      • R 2개, G 1개, B 1개 → 총 4개
    • 적록색약(빨간색과 초록색의 차이를 거의 느끼지 못하는 사람)인 사람은 R과 G를 같은 색으로 간주하여 구역을 센다.
      • R과 G를 같은 색으로 간주 → RG 2개, B 1개 → 총 3개
  • 두 경우의 구역 개수를 구한다.

시간복잡도

  • 지도 위의 모든 정점에 대해 탐색을 진행하게 된다. 단, 한번 탐색된 정점은 다시 탐색하지 않기 때문에 $O(N \times N)$개의 정점을 탐색하게 된다.
  • 한 정점에서 4가지 방향으로 이동할 수 있기 때문에 4x$O(N^2)$의 시간 복잡도가 소요된다.
  • 적록색맹인 경우, 아닌 경우 2번의 탐색이 진행되므로 최종적으로 2x4x$O(N^2)$의 시간복잡도가 소요된다.
  • 중간에 적록색약 탐색을 위해 그리드를 함께 탐색해서 값을 바꿔주는 과정이 존재한다. 이 경우 $O(N^2)$의 시간복잡도가 소요된다.
  • 총 9x$O(N^2)$이 소요되며, 상수배수를 무시하면 최종 시간복잡도는 $O(N^2)$이다.
  • $N$ ≤ 100 이므로 최대 100x100=10,000 개의 노드를 탐색해야 한다. BFS/DFS를 사용하여 $O(N^2)$의 시간복잡도로 해결이 가능하다.

알고리즘

  • 2차원 배열 상하좌우 인접 탐색은 BFS/DFS로 해결한다.
  1. $N$을 입력받고 $N$x$N$ 크기의 그리드를 저장한다.
  2. BFS/DFS를 이용하여 같은 색의 영역을 찾는다.
    • 적록색약이 아닌 경우와 적록색약인 경우를 각각 계산해야 한다.

🚩제출한 코드

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

N = int(input().strip())
grid = [list(input().strip()) for _ in range(N)]

# 방향 벡터 
dx = [-1, 1, 0, 0]
dy = [0, 0, -1, 1]

def bfs(x, y, color, visited, grid):
    queue = deque([(x, y)])
    visited[x][y] = True

    while queue:
        x, y = queue.popleft()
        for i in range(4):
            nx, ny = x + dx[i], y + dy[i]
            if 0 <= nx < N and 0 <= ny < N and not visited[nx][ny]:
                if grid[nx][ny] == color:
                    visited[nx][ny] = True
                    queue.append((nx, ny))

def count_regions(grid, is_color_blind):
    visited = [[False] * N for _ in range(N)]
    count = 0

    for i in range(N):
        for j in range(N):
            if not visited[i][j]:  # 방문하지 않은 경우 BFS 탐색 시작
                count += 1
                color = grid[i][j]

                # 적록색약 모드라면 R과 G를 동일하게 처리
                if is_color_blind and color in "RG":
                    bfs(i, j, "R", visited, grid)
                else:
                    bfs(i, j, color, visited, grid)

    return count

# 적록색약이 아닌 경우
normal_count = count_regions(grid, is_color_blind=False)

# 적록색약인 경우 (R과 G를 동일하게 처리한 grid 생성)
for i in range(N):
    for j in range(N):
        if grid[i][j] == 'G':
            grid[i][j] = 'R'

color_blind_count = count_regions(grid, is_color_blind=True)

print(normal_count, color_blind_count)

💡TIL

  • 적록색약의 탐색을 위해 그리드에서 G를 R로 변경하는 과정이 추가되는데, 이는 그리드를 한 번 순회하면서 수행되므로 $O(N^2)$이다.

  • 방향벡터를 사용해서 상하좌우 이동하는 코드 잘 기억해두어야 한다.

    dx = [-1, 1, 0, 0]
    dy = [0, 0, -1, 1]
    
    for i in range(4):
        nx = x + dx[i]
        ny = y + dy[i]
  • BFS에서 인접한 노드를 queue에 넣기 전에는 항상 두 조건을 확인한다.

    • (1) 주어진 배열 범위를 벗어나지는 않았는가?
    • (2) 이미 방문된 곳은 아닌가? 현재 탐색중인 구역과 같은 색인가?
      • 방문 배열을 별도로 만들수도 만들지 않을수도 있다.
    • 두 조건을 모두 만족한다면, 그 지점을 방문 표기하고 queue에 삽입한다.

백준 #9195. 1,2,3 더하기: DP / 실버3

정리한 링크: 바로가기

🚩플로우 (선택)

문제

  • 정수 $n$이 주어졌을 때, $n$을 1, 2, 3의 합으로 나타내는 방법의 수를 구한다.
  • 테스트 케이스의 개수 $T$, 정수 $n$ (1 ≤ n < 11)

시간복잡도 알고리즘

  • 각 숫자(1, 2, 3)을 더하면서 $n$을 만들 수 있는 모든 경우를 시도하면 약 $3^n$번의 연산이 필요하다. 최대 $n$이 11이므로 3^11≈177,147 연산이 필요하다. 1억 번 이하라 가능은 하지만, 더 좋은 방법이 있다.

  • 규칙을 찾기 위해 작은 값부터 나열해본다.

  • $n$=4의 경우를 보면,

    • $n$=3을 만드는 방법 뒤에 +1 붙이기
    • $n$=2을 만드는 방법 뒤에 +2 붙이기
    • $n$=1을 만드는 방법 뒤에 +3 붙이기
    • 즉, 숫자 $n$을 만드는 경우의 수는 아래의 경우 총 3가지로 정의된다.
      • ($n$-1)을 만드는 방법 뒤에 숫자 +1 붙이기
      • ($n$-2)을 만드는 방법 뒤에 숫자 +2 붙이기
      • ($n$-3)을 만드는 방법 뒤에 숫자 +3 붙이기
    • 이로부터 DP[n] = DP[n−1]+DP[n−2]+DP[n−3] 의 점화식을 도출할 수 있다.
  • 동적계획법(DP): 전 단계의 합을 활용해서 다음 단계의 답을 구할 수 있을 때 활용한다.

  • 점화식이 성립되었으므로, 작은 값부터 차례대로 DP 배열을 채워나가면 된다.

    • 점화식 정의: DP[n] = DP[n−1]+DP[n−2]+DP[n−3]

    • 초기값 설정: DP[1]=1, DP[2]=2, DP[3]=4

      • 이외 모든 DP는 0으로 초기화 해둔다.
    • DP 테이블 채우기: 점화식을 이용해 최대 $n$까지 미리 계산해 둔다.

      for i in range(4, 11):
          DP[i] = DP[i-3] + DP[i-2] + DP[i-1]
  • 점화식대로 $n$까지의 DP를 탐색하기 위해서는 **$O(N)$**의 시간복잡도가 소요된다.

  1. DP 탐색을 위한 초기 값을 설정한다.
  2. 세운 점화식에 맞게 DP 탐색을 진행한다.
  3. 문제의 입력을 받고, 구해놓은 DP 값에 맞게 답을 출력한다.

🚩제출한 코드

import sys 
input = sys.stdin.readline

DP = [0] * 11
DP[1] = 1
DP[2] = 2
DP[3] = 4

for i in range(4, 11):
    DP[i] = DP[i-1] + DP[i-2] + DP[i-3]

T = int(input())
for _ in range(T):
    n = int(input())
    print(DP[n])

💡TIL

  • 전의 단계의 값을 다음 단계의 문제풀이에 사용할 수 있을 경우 DP를 떠올린다.
  • DP 문제는 규칙성을 찾는 것이 중요하다. 작은 단계부터 시작해서 규칙을 찾아보자.

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.

일주일 간 7문제를 푸시다니 대단하십니다! 저도 꾸준히 풀 수 있도록 본받도록 할게요...ㅎ
또 각 문제별로 문제, 알고리즘, 시간복잡도를 자세히 잘 정리하시는 것 같습니다!
여러모로 배워갑니당 고생하셨어요

Comment on lines +34 to +39
Copy link
Collaborator

Choose a reason for hiding this comment

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

저는 아래 코드처럼 매번 If 문 안에서 조회하게 짰는데 이렇게 코드 짜는 게 훨씬 효율적인 것 같네요!

plan = list(map(int, input().split()))
result = "YES"
for i in range(1, m):
    if parent[plan[i]-1] != parent[plan[0]-1]:
        result = "NO"
        break

Comment on lines +36 to +42
Copy link
Collaborator

Choose a reason for hiding this comment

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

문제 조건에 한번에 출력이라는 게 없다면 result 리스트가 아니라 바로바로 출력해도 좋을 것 같습니다!

if find(a) == find(b):
    print("YES")
else:
    print("NO")

Comment on lines +20 to +27
Copy link
Collaborator

Choose a reason for hiding this comment

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

혹시 26-27줄에 같은 랭크 부분은 왜 추가가 된 건지 알 수 있을까요?

저는 이 부분을 아래 코드처럼 작성하였습니다. 참고해주세요!

if p_a > p_b: # 값이 더 작은 쪽을 부모로 설정
    parent[p_a] = p_b
else:
    parent[p_b] = p_a

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에 유니온 파인드에 대한 개념과 여행가자 문제에 대해 잘 작성해 주셔서 이해하기 쉬웠습니다.
한 주 수고하셨습니다!

@Mingguriguri Mingguriguri merged commit 1f2926d into AlgorithmStudy-Allumbus:main Mar 10, 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.

3 participants