신비로운 유적 탐험 문서의 문제 1 풀이 코드다. if __name__ == "__main__": 아래가 자체 테스트다.
"""신비로운 유적 탐험 (프로그래머스 1834) 풀이와 검증. 노드 번호는 문제처럼 1부터 센다."""
import random
import time
from itertools import permutations
def best_matching(w):
"""w[i][j] ≥ 0인 표에서, 행과 열을 겹치지 않게 짝지어 얻는 값의 합의 최댓값(헝가리안 방법)."""
if not w or not w[0]:
return 0
if len(w) > len(w[0]):
w = [list(col) for col in zip(*w)] # 행이 열보다 많지 않게 돌린다
n, m = len(w), len(w[0])
INF = float("inf")
u, v = [0] * (n + 1), [0] * (m + 1) # 행과 열의 잠재값
match = [0] * (m + 1) # 열 j와 짝인 행 (0이면 없음)
way = [0] * (m + 1)
for i in range(1, n + 1): # 행을 하나씩 짝에 넣는다
match[0] = i
j0 = 0
minv = [INF] * (m + 1)
used = [False] * (m + 1)
while True: # 가장 싸게 늘리는 길을 찾는다
used[j0] = True
i0, delta, j1 = match[j0], INF, 0
for j in range(1, m + 1):
if not used[j]:
cur = -w[i0 - 1][j - 1] - u[i0] - v[j] # 값을 비용(−값)으로 바꿔 최소화
if cur < minv[j]:
minv[j], way[j] = cur, j0
if minv[j] < delta:
delta, j1 = minv[j], j
for j in range(m + 1):
if used[j]:
u[match[j]] += delta
v[j] -= delta
else:
minv[j] -= delta
j0 = j1
if match[j0] == 0:
break
while j0: # 찾은 길을 따라 짝을 바꾼다
j1 = way[j0]
match[j0] = match[j1]
j0 = j1
return sum(w[match[j] - 1][j - 1] for j in range(1, m + 1) if match[j])
def children_lists(n, g):
adj = [[] for _ in range(n + 1)]
for p, q in g:
adj[p].append(q)
adj[q].append(p)
kids = [[] for _ in range(n + 1)]
order, seen, stack = [], [False] * (n + 1), [1]
seen[1] = True
while stack: # 뿌리 1부터 내려가며 부모-자식을 정한다
v = stack.pop()
order.append(v)
for c in adj[v]:
if not seen[c]:
seen[c] = True
kids[v].append(c)
stack.append(c)
return kids, order[::-1] # 자식이 먼저인 순서
def solution(n1, g1, n2, g2, table=None):
kids1, order1 = children_lists(n1, g1)
kids2, order2 = children_lists(n2, g2)
dp = [[0] * (n2 + 1) for _ in range(n1 + 1)] # dp[u][v]: u와 v를 뿌리로 맞출 때 공통부분 크기
for u in order1:
for v in order2:
w = [[dp[c][d] for d in kids2[v]] for c in kids1[u]]
dp[u][v] = 1 + best_matching(w)
if table is not None:
table.extend(dp)
return dp[1][1]
def canon(kids, v, keep):
"""keep 안의 v 아래 나무를 순서 없는 꼴로 적는다."""
return "(" + "".join(sorted(canon(kids, c, keep) for c in kids[v] if c in keep)) + ")"
def rooted_subtrees(n, g):
kids, _ = children_lists(n, g)
parent = {c: v for v in range(1, n + 1) for c in kids[v]}
shapes = {}
for mask in range(1 << (n - 1)): # 뿌리 1은 늘 넣는다
keep = {1} | {i + 2 for i in range(n - 1) if mask >> i & 1}
if all(parent[x] in keep for x in keep if x != 1):
shapes[canon(kids, 1, keep)] = len(keep)
return shapes
def by_shapes(n1, g1, n2, g2):
"""뿌리를 포함한 부분 나무의 모양을 모두 적어 겹치는 가장 큰 것을 찾는다(작은 입력만)."""
s1, s2 = rooted_subtrees(n1, g1), rooted_subtrees(n2, g2)
return max(s1[k] for k in s1 if k in s2)
def random_tree(n, rng):
g = [[rng.randint(1, i - 1), i] for i in range(2, n + 1)]
perm = list(range(2, n + 1))
rng.shuffle(perm)
relabel = {1: 1, **{i: perm[i - 2] for i in range(2, n + 1)}}
return [[relabel[a], relabel[b]] if rng.random() < 0.5 else [relabel[b], relabel[a]] for a, b in g]
if __name__ == "__main__":
g1 = [[3, 1], [5, 7], [8, 7], [2, 3], [3, 6], [1, 5], [4, 3]]
g2 = [[1, 5], [5, 6], [3, 7], [3, 1], [7, 4], [7, 2], [9, 8], [5, 9]]
tb = []
assert solution(8, g1, 9, g2, tb) == 7 == by_shapes(8, g1, 9, g2)
assert (tb[3][5], tb[3][3], tb[5][5], tb[5][3], tb[1][1]) == (3, 2, 3, 3, 7) # 6단계 표
# 헝가리안 방법을 순열 전부와 비교한다
rng = random.Random(1834)
for _ in range(2000):
r, c = rng.randint(0, 5), rng.randint(0, 5)
w = [[rng.randint(0, 9) for _ in range(c)] for _ in range(r)]
if r <= c:
want = max((sum(w[i][p[i]] for i in range(r)) for p in permutations(range(c), r)), default=0)
else:
want = max(sum(w[p[j]][j] for j in range(c)) for p in permutations(range(r), c))
assert best_matching(w) == want, w
# 큰 쪽을 욕심으로 고르면 틀리는 표 (7단계)
assert best_matching([[3, 2], [2, 0]]) == 4 and best_matching([[2, 3], [3, 3]]) == 6
for _ in range(1500):
n1, n2 = rng.randint(1, 9), rng.randint(1, 9)
a, b = random_tree(n1, rng), random_tree(n2, rng)
assert solution(n1, a, n2, b) == by_shapes(n1, a, n2, b), (a, b)
# 노드 100개: 별 모양 둘, 한 줄 둘, 무작위 둘
star = [[1, i] for i in range(2, 101)]
line = [[i, i + 1] for i in range(1, 100)]
t0 = time.perf_counter()
assert solution(100, star, 100, star) == 100
assert solution(100, line, 100, line) == 100 and solution(100, star, 100, line) == 2
solution(100, random_tree(100, rng), 100, random_tree(100, rng))
t = time.perf_counter() - t0
assert t < 20, t
print(f"노드 100개 네 번: {t:.2f}s")
print("ALL CHECKS PASSED")