매출 하락 최소화 문서의 문제 1 풀이 코드다. if __name__ == "__main__": 아래가 자체 테스트다.

"""매출 하락 최소화 (프로그래머스 72416) 풀이와 검증."""
import random
import time
from itertools import combinations


def solution(sales, links, table=None):
    n = len(sales)
    children = [[] for _ in range(n + 1)]
    for a, b in links:
        children[a].append(b)
    order, stack = [], [1]
    while stack:                               # 뿌리부터 내려가는 순서
        v = stack.pop()
        order.append(v)
        stack.extend(children[v])
    go = [0] * (n + 1)                         # go[v]: v가 간다
    stay = [0] * (n + 1)                       # stay[v]: v가 안 간다
    for v in reversed(order):                  # 자식이 먼저
        base = sum(min(go[c], stay[c]) for c in children[v])
        go[v] = sales[v - 1] + base
        if not children[v]:
            stay[v] = 0                        # 팀장이 아니면 v 때문에 채울 팀이 없다
        elif any(go[c] <= stay[c] for c in children[v]):
            stay[v] = base                     # 팀원 중 누군가 이미 가는 편이 싸다
        else:
            stay[v] = base + min(go[c] - stay[c] for c in children[v])   # 한 명은 억지로 보낸다
        if table is not None:
            table[v] = (go[v], stay[v])
    return min(go[1], stay[1])


def by_subsets(sales, links):
    n = len(sales)
    teams = {}
    for a, b in links:
        teams.setdefault(a, {a}).add(b)
    best = None
    for k in range(n + 1):
        for comb in combinations(range(1, n + 1), k):
            s = set(comb)
            if all(team & s for team in teams.values()):
                cost = sum(sales[v - 1] for v in comb)
                best = cost if best is None else min(best, cost)
    return best


if __name__ == "__main__":
    ex = ([14, 17, 15, 18, 19, 14, 13, 16, 28, 17],
          [[10, 8], [1, 9], [9, 7], [5, 4], [1, 5], [5, 10], [10, 6], [1, 3], [10, 2]])
    assert solution(*ex) == 44 == by_subsets(*ex)
    assert solution([5, 6, 5, 3, 4], [[2, 3], [1, 4], [2, 5], [1, 2]]) == 6
    assert solution([5, 6, 5, 1, 4], [[2, 3], [1, 4], [2, 5], [1, 2]]) == 5
    assert solution([10, 10, 1, 1], [[3, 2], [4, 3], [1, 4]]) == 2
    # 6단계 추적: 예시 1의 (간다, 안 간다)
    tb = {}
    solution(*ex, table=tb)
    assert tb[10] == (17, 14) and tb[5] == (33, 17) and tb[9] == (28, 13) and tb[1] == (44, 45)
    assert tb[7] == (13, 0) and tb[3] == (15, 0)
    rng = random.Random(72416)
    for _ in range(1500):
        n = rng.randint(2, 10)
        links = [[rng.randint(1, v - 1), v] for v in range(2, n + 1)]
        sales = [rng.randint(0, 20) for _ in range(n)]
        assert solution(sales, links) == by_subsets(sales, links), (sales, links)
    # 30만 명, 한 줄로 이어진 조직도
    n = 300_000
    links = [[v - 1, v] for v in range(2, n + 1)]
    sales = [rng.randint(0, 10_000) for _ in range(n)]
    t0 = time.perf_counter()
    solution(sales, links)
    t = time.perf_counter() - t0
    assert t < 10, t
    print(f"300k chain: {t:.2f}s")
    print("ALL CHECKS PASSED")