Day 034-Q3 — 木の彩色数え上げ (Broken Profile DP + Tree DP)

2026-05-17 赤色 Master / Phase 8+ ★★★★★★★★★ 木 DP + 順列

問題

$N$ 頂点の木を $K$ 色で塗る。以下2条件を共に満たす塗り方を $10^9 + 7$ で割った余りで求めよ。

  1. 隣接辺制約: 辺で結ばれた2頂点は異なる色
  2. 連結成分制約: 同じ色の頂点集合の誘導部分グラフは連結

つまり「各色が連結な部分木を形成する塗り方」を数える。

制約

$2 \le N \le 3000$
$2 \le K \le N$
辺は $N-1$ 本

入出力例

入力例 1 (パス)

4 3
1 2
2 3
3 4

出力: 18

パス $1{-}2{-}3{-}4$ を3色で連続区間に分割

入力例 2 (星型)

3 2
1 2
1 3

出力: 2

中心1を色A、葉2,3を色B → 葉どうし非連結 → NG。中心と片葉を同色にする2通りのみ

概念図: 木の分割 → 色割り当て

「各色 = 連結な部分木」⟺ 「木を $m$ 個の連結部分木に分割」 × 「$K$ 色から $m$ 色を選ぶ順列」

元の木 (N=6) 1 2 3 4 5 6 m=3 個に分割 → 各色を割当 1 2 3 4 5 6 辺を 2 本切断 → 3 連結成分 答え = $\sum_{m=1}^{K} f(m) \cdot P(K, m)$

インタラクティブ: 辺の切断 → 成分数

パス $1{-}2{-}3{-}4$ の辺ボタンをクリックして切断する/しないを切り替えてみよう。

1
2
3
4

成分数: 1  |  切断辺: 0 本

ヒント (段階的開示)

ヒント1: 方向性
「各色が連結部分木」⟺ 「木を $K$ 個までの連結部分木に分割し、各成分に異なる色」。
ヒント2: アプローチ
木を $m$ 個に分割する方法数を $f(m)$ とすると、答えは $\sum_{m=1}^{K} f(m) \cdot P(K, m)$。$f(m)$ は木 DP で求める。
ヒント3: 木 DP 遷移
dp[v][m] = 頂点 $v$ の部分木を $m$ 個の連結部分木に分割する方法。子 $c$ をマージするとき:
  • 辺 $(v,c)$ を切る → 成分数 $s_v + s_c$
  • 辺 $(v,c)$ を繋ぐ → 成分数 $s_v + s_c - 1$

模範解答 (Python)

import sys
from sys import stdin

def main():
    MOD = 10**9 + 7
    data = stdin.read().split()
    idx = 0
    N, K = int(data[idx]), int(data[idx+1]); idx += 2

    adj = [[] for _ in range(N+1)]
    for _ in range(N-1):
        u, v = int(data[idx]), int(data[idx+1]); idx += 2
        adj[u].append(v); adj[v].append(u)

    # BFS で親子関係を確定
    root = 1
    parent = [0] * (N+1)
    order = []
    visited = [False] * (N+1)
    queue = [root]; visited[root] = True; head = 0
    while head < len(queue):
        v = queue[head]; head += 1
        order.append(v)
        for u in adj[v]:
            if not visited[u]:
                visited[u] = True
                parent[u] = v
                queue.append(u)

    children = [[] for _ in range(N+1)]
    for v in order[1:]:
        children[parent[v]].append(v)

    size = [1] * (N+1)
    for v in reversed(order):
        for c in children[v]:
            size[v] += size[c]

    # dp[v][m] = v の部分木を m 個の連結部分木に分割する方法
    dp = [[0] * (N+1) for _ in range(N+1)]
    for v in range(1, N+1):
        dp[v][1] = 1

    for v in reversed(order):
        for c in children[v]:
            new_dp = [0] * (N+1)
            sv_max = size[v] - size[c]
            for sv in range(1, sv_max + 1):
                if dp[v][sv] == 0: continue
                for sc in range(1, size[c] + 1):
                    if dp[c][sc] == 0: continue
                    val = dp[v][sv] * dp[c][sc] % MOD
                    new_dp[sv + sc]     = (new_dp[sv + sc] + val) % MOD       # 切る
                    new_dp[sv + sc - 1] = (new_dp[sv + sc - 1] + val) % MOD   # 繋ぐ
            for m in range(N+1):
                dp[v][m] = new_dp[m]

    f = dp[root]

    # P(K, m) = K * (K-1) * ... * (K-m+1)
    ans = 0
    perm = 1
    for m in range(1, min(K, N) + 1):
        if m == 1:
            perm = K % MOD
        else:
            perm = perm * (K - m + 1) % MOD
        ans = (ans + f[m] * perm) % MOD

    print(ans)

main()

Step-by-Step 解説

1問題変換
「各色 = 連結部分木」を「分割」 + 「色順列」の積に分解。$P(K,m) = K!/(K-m)!$
2木 DP の遷移
子 $c$ をマージするとき:
  • 切る: 成分数 $s_v + s_c$
  • 繋ぐ: 合併で成分数 $s_v + s_c - 1$
3葉→根の処理順
BFS 順を逆転 (reversed(order)) で葉→根を保証。
4答え集計
$\text{ans} = \sum_{m=1}^{\min(K,N)} f(m) \cdot K (K-1) \cdots (K-m+1)$

計算量

木 DP: $O(N^2)$ (全辺マージで部分木サイズの積)
答え集計: $O(K)$
合計: $O(N^2)$  ←  $N=3000$ で約 $9 \times 10^6$

よくあるミス

ミス原因正しい書き方
繋ぐ場合の -1 忘れ合併で成分数が1減ることを失念new_dp[sv + sc - 1]
f[0] を使う0 個分割が無意味$m$ は 1 から開始
$P(K,m)$ の順序$m > K$ で負になるmin(K, N) の範囲で打ち切り
DP 更新順序親が子より先に更新reversed(order) で葉→根

次のステップ

  • 発展: 各色の使用頂点数がちょうど $k_i$ 個という制約
  • 応用: グリッドグラフでの Broken Profile DP (連結領域の分割)

自己評価

自分の回答

気づき・メモ