설명을 매우 잘해주신 분이 있어서 참고했습니다.
인천 앞바다에는 1부터 n
까지 서로 다른 번호가 매겨진 등대 n
개가 존재합니다. 등대와 등대 사이를 오가는 뱃길이 n-1
개 존재하여, 어느 등대에서 출발해도 다른 모든 등대까지 이동할 수 있습니다. 등대 관리자 윤성이는 전력을 아끼기 위하여, 이 중 몇 개의 등대만 켜 두려고 합니다. 하지만 등대를 아무렇게나 꺼버리면, 뱃길을 오가는 배들이 위험할 수 있습니다. 한 뱃길의 양쪽 끝 등대 중 적어도 하나는 켜져 있도록 등대를 켜 두어야 합니다.
예를 들어, 아래 그림과 같이 등대 8개와 7개의 뱃길들이 있다고 합시다. 이 경우 1번 등대와 5번 등대 두 개만 켜 두어도 모든 뱃길은 양쪽 끝 등대 중 하나가 켜져 있으므로, 배들은 안전하게 운항할 수 있습니다.
등대의 개수 n
과 각 뱃길이 연결된 등대의 번호를 담은 이차원 배열 lighthouse
가 매개변수로 주어집니다. 윤성이가 켜 두어야 하는 등대 개수의 최솟값을 return 하도록 solution 함수를 작성해주세요.
n
≤ 100,000lighthouse
의 길이 = n – 1
lighthouse
배열의 각 행 [a, b]
는 a
번 등대와 b
번 등대가 뱃길로 연결되어 있다는 의미입니다.
a
≠ b
≤ n
n | lighthouse | result |
---|---|---|
8 | [[1, 2], [1, 3], [1, 4], [1, 5], [5, 6], [5, 7], [5, 8]] | 2 |
10 | [[4, 1], [5, 1], [5, 6], [7, 6], [1, 2], [1, 3], [6, 8], [2, 9], [9, 10]] | 3 |
입출력 예 #1
입출력 예 #2
일단 근본적으로 생각해봐야 할 것은, 켜두어야 하는 등대의 최솟값을 구해야 한다는 것이다. 이것을 이분법적으로 생각해보면 등대를 최소로 켜둔 상황에서 특정 등대는 켜지거나 꺼지거나 둘 중 하나이다.
이를 분할하면, 특정 노드를 포함한 서브트리에서 특정 노드가 켜졌을 때(on
) 최소로 켜진 등대 개수와 꺼졌을 때(off
) 최소로 켜진 등대 개수중 최소를 답으로 가지면 된다. 이 과정을 루트노드에 대해서 수행하면 된다.
특정 노드가 on
일 때, 특정 노드를 포함하는 서브트리에서 켜야하는 최소 등대 개수
특정 노드가 켜져있다면 이와 연결된 자식 노드들은 켜지던 꺼지던 상관이 없다. 그러므로 자식노드(c
)로 dfs
를 수행한 결과(dfs(c)
)인 c_on
, c_off
중 최솟값을 골라서 합친다.
특정 노드가 off
일 때, 특정 노드를 포함하는 서브트리에서 켜야하는 최소 등대 개수
특정 노드가 꺼져있다면 이와 연결된 자식 노드들은 무조건 켜져있어야 한다.
그러므로, 자식 노드(c
)로 dfs
를 수행한 결과(dfs(c)
)인 c_on
을 합친다.
따라서, 위의 모든 과정을 루트노드에 대해서 DFS를 재귀적으로 수행하면, 최소로 켜야하는 등대의 개수를 얻을 수 있다.
이때, 루트 노드는 어떻게 정하는지도 생각해봐야 한다.
분명 문제에서 모든 등대는 서로 다른 등대로 이동할 수 있는 뱃길이 반드시 존재한다고 되어있다.
이 말을 풀어보면 모든 등대는 서로 연결되어 있으니 어떤 노드를 루트 노드로 선택해도 상관없다는 말이다.
어떤 노드를 루트노드로 선택해도 상관 없다 → 등대 연결 정보는 양방향이다.
특정 노드에서 연결된 모든 자식 노드를 확인해야 한다 → DFS
따라서, 양방향 그래프를 하나 만들고 이를 토대로 DFS를 수행한다.
import sys
sys.setrecursionlimit(10 ** 6)
def solution(n, lighthouse):
graph = [[] for _ in range(n + 1)] # 양방향 그래프
check = [0] * (n + 1) # 방문 처리
# 양방향 그래프 만들기
for a, b in lighthouse:
graph[a].append(b)
graph[b].append(a)
# DFS
def dfs(x):
check[x] = 1 # 방문처리
# 리프 노드라면
if not graph[x]:
return 1, 0 # on, off
# 리프 노드가 아니라면
# 방문하지 않은 자식 노드를 리스트로 만든다.
child = [nxt for nxt in graph[x] if not check[nxt]]
# 점등, 소등
on, off = 1, 0
# 자식노드를 탐색하면서
for c in child:
c_on, c_off = dfs(c) # 해당 자식노드를 켜거나 끌 경우
on += min(c_on, c_off) # 나(특정 노드)를 켤 경우
off += c_on # 나(특정 노드)를 끌 경우 자식은 반드시 켜야한다.
return on, off
# 뱃길은 모두 이어져있으므로 시작 노드는 상관이 없다.
on, off = dfs(1)
return min(on, off) # 최소로 켜는 등대 개수를 반환한다.
먼저 알고리즘의 과정은 다음과 같다.
dfs
를 수행한다. → dfs(x)
check[x] = 1
return 1, 0
dfs(c)
on += min(c_on, c_off)
off += c_on
return on, off
문제의 예시#1을 그림으로 살펴보자.
(방문한 자식 노드는 파란 테두리, 루트 노드는 빨간 테두리, 그리고 켜진 노드는 노란 배경으로 설정한다.)
먼저 초기 등대의 연결 상태이다. 루트 노드를 1로 설정하겠다.
(자식 방문 순서는 2, 3, 4, 5로 설정한다.)
1번 등대를 켜고 2를 방문했다.
이때는 2를 키지 않아도 된다. 양 끝에 등대 하나만 켜지면 되고 1이 이미 켜졌기 때문이다.
나머지 2, 3을 방문해도 똑같이 켜지 않아도 된다.
다음으로 5를 방문했고 등대를 켜지 않았을 경우이다.
자식 노드들은 반드시 켜져야한다(양 끝 등대 중 적어도 하나는 켜져있어야 하므로)
만약에 5를 켠다면 어떻게 될까?
이때는 자식 노드를 켜지 않아도 되므로, 방문만 한다.
자 그러면 위의 예시는 1을 켤 경우였고, 이번에는 1을 꺼보도록 하자.
1을 끄게 되면 자식 노드인 2는 반드시 켜져야 한다.
나머지 자식 노드인 3과 4도 똑같다.
다음으로는 5번 노드를 방문한 경우이고 켜지 않았을 때 이다.
5번 노드가 켜져있지 않으므로, 5번 노드의 자식 노드들은 반드시 켜져야 한다.
자 그러면 5번 노드가 꺼졌을 때는 살펴봤으니, 켜졌을 때를 살펴보자.
5번 노드가 켜져있으므로 5번 노드를 루트노드로 하는 6, 7, 8은 켜지 않아도 된다.
결과적으로 1번과 5번만 켰을 때, 모든 배들이 안전하게 운행할 수 있게 된다.
계산 필요..
재귀에 대해서 더 공부해야겠다.
계속해서 절차지향적 사고 재귀를 풀어나가려 하니까 코드 구현이 쉽지가 않다.
수학적 귀납법을 생각해야 하는데, 여전히 갈 길이 멀다..