「一本通 5.2 练习 4」叶子的颜色

GitHub跳转原题关系图返回列表

树形 DP:答案与根无关,任取度数大于 1 的节点为根,状态记录根到该点路径上最后一个有色节点的颜色(黑/白/尚无),自底向上在给该点着色与沿用上方颜色之间取最小值。

OJ: roj

题目 ID: 1583

难度:提高

标签:动态规划树形 DP

创建: 2026-09-30 20:18

更新: 2026-10-07 13:50

形式化题目

给定一棵无根树 T=(V,E)T = (V, E),∣V∣=m|V| = m,其中前 nn 个节点恰好是全部叶子。每个叶子 u∈{1,…,n}u \in \{1, \dots, n\} 有一个指定颜色 cu∈{0,1}c_u \in \{0, 1\}(00 表示黑,11 表示白)。

选择一个度数大于 1 的节点作为根 rr,再给任意若干个节点着黑或白,得到着色集合 S⊆VS \subseteq V。要求对每个叶子 uu:

  1. 根到 uu 的简单路径 P(r,u)P(r, u) 上至少有一个有色节点(叶子自身着色也算);
  2. P(r,u)P(r, u) 上最后一个有色节点的颜色恰好等于 cuc_u。

求满足上述要求的最少着色节点数 ∣S∣|S|。

样例:m=5,n=3m = 5, n = 3,c1=0,c2=1,c3=0c_1 = 0, c_2 = 1, c_3 = 0,边为 1-4,2-5,4-5,3-51\text{-}4, 2\text{-}5, 4\text{-}5, 3\text{-}5。下图给出以 5 为根时一种最优着色(5 着黑、2 着白,其余不着色):

graph TD
    n5(("5(根,黑)")) --- n4["4"]
    n5 --- n2["2(白)"]
    n5 --- n3["3"]
    n4 --- n1["1"]

观察三条根到叶路径:路径 5→4→15 \to 4 \to 1 上只有 5 有色,最后一个有色节点是 5(黑),匹配 c1=0c_1 = 0;路径 5→35 \to 3 同理匹配 c3=0c_3 = 0;路径 5→25 \to 2 上 5 和 2 都有色,最后一个是 2(白),匹配 c2=1c_2 = 1。4 号点没有着色却完全合法——判定只看每条路径的最后一个有色节点,而不是路径上有没有中间色点。因此该方案用 2 个着色点满足全部约束,输出 2。

正解

思路

从判定模型说起。 着色方案是否合法,对每片叶子只看一件事:根到它的路径上最后一个有色节点(下称"责任节点")是否存在、颜色是否为 cuc_u。最朴素的做法是枚举着色集合及其配色再逐叶验证,2m2^m 级别完全不可行;但它提示我们:叶子真正关心的只是"路径上的最后颜色",路径更早的历史没有意义。

把"最后颜色"做成状态。 树上"向上最近的特殊点"类问题的标准套路是自底向上树形 DP。固定节点 uu,uu 子树内叶子的合法性只依赖两件事——根到 uu 路径上最后一个有色节点的颜色,以及子树内部如何继续着色;uu 之上的全部历史都不影响子树(无后效性)。于是定义

dp[u][s]=在 u 的子树内合法着色所需的最少新增节点数,dp[u][s] = \text{在 } u \text{ 的子树内合法着色所需的最少新增节点数},

其中 s∈{黑, 白, 尚无}s \in \{\text{黑},\ \text{白},\ \text{尚无}\} 是根到 uu 路径上最后一个有色节点的颜色;"尚无"表示这条路径上还一个色点都没有,它只能从根一路向下继承,直到某处着色或叶子被迫着色——这一状态不能省,否则会漏掉"整条路径无色"的非法方案。

转移。 对叶子(要求责任节点颜色为 cuc_u):ss 已经等于 cuc_u 时不花钱(0);ss 是另一种颜色时只能给叶子着上 cuc_u 盖住它(1);ss 为"尚无"时连"路径含色点"都没满足,也必须给叶子着色(1)。

对内部节点 uu,任意合法方案在 uu 处只有两类互斥选择:

  • 不给 uu 着色:子树入状态原样沿用,代价 ∑vdp[v][s]\sum_{v} dp[v][s];
  • 给 uu 着色:付出 1 个点,uu 成为子树内一切叶子的"上方最后色点",孩子的状态全部换成 uu 的颜色,代价 1+∑vdp[v][X]1 + \sum_{v} dp[v][X],XX 枚举黑、白两种取值。

取三者最小即得 dp[u][s]dp[u][s]:第二类选择中 XX 取黑、白各算一个候选,因此每个状态实际在"沿用上方"“着黑”"着白"三个候选里取最小。孩子子树互不相交,各自的最优可以相加(最优子结构),且这三个候选不重不漏地覆盖了所有方案。

根的选取。 题目只允许度数大于 1 的节点作根,而最优值与根的选取无关。直观上,着色集合本身与根无关,换根只是改变每片叶子"最后一个有色节点"落在谁头上;我们用对照实验确认了这一性质:m⩽10m \leqslant 10 的随机树逐个合法根与 2m2^m 暴力枚举结果全部一致,m⩽40m \leqslant 40 的随机树枚举全部合法根得到的 DP 答案两两相等,全部 10 组真实数据也通过。因此任取一个度数大于 1 的节点作根求解即可。

答案为 dp[root][尚无]dp[\text{root}][\text{尚无}]。下面用样例展示自底向上每个节点的三元组(根取 5,孩子为 2,3,42, 3, 4,节点 4 的孩子为 1;行按计算顺序,列是三种入状态,单元格为 dp[u][s]dp[u][s]):

节点 结构 继承黑 继承白 尚无色
1(叶, c1=0c_1 = 0) 匹配则 0 0 1 1
2(叶, c2=1c_2 = 1) 匹配则 0 1 0 1
3(叶, c3=0c_3 = 0) 匹配则 0 0 1 1
4 子:1 0 1 1
5 子:2, 3, 4 1 2 2

观察两处关键:节点 4 在"继承黑"列取 0——不给 4 着色、沿用上方黑色即可满足 c1=0c_1 = 0;根节点 5 的"尚无色"列取 2,来自"给 5 着黑"分支:1+∑vdp[v][黑]=1+(1+0+0)=21 + \sum_v dp[v][\text{黑}] = 1 + (1 + 0 + 0) = 2(三棵子树分别是 2 号点需补着色、3 号点已满足、4 号点沿用黑色),这正是样例最优解——给 5 着黑、2 着白,共 2 个点。

代码

Python 版:

python
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-09-30 20:18
# update_at: 2026-09-30 20:25

from collections import deque
import sys

BLACK, WHITE, NONE = 0, 1, 2  # dp 状态:根到 u 路径上最后一个有色节点的颜色,NONE = 上方还没有有色节点


def min_coloring(adj: list[list[int]], color: list[int], root: int) -> int:
    """以 root 为根做树形 DP,返回让所有叶子 c_u 都成立的最少着色数。"""
    parent = [0] * len(adj)
    parent[root] = -1
    order = [root]
    q = deque([root])
    while q:  # 迭代建先父后子的顺序:深链也不爆栈
        u = q.popleft()
        for v in adj[u]:
            if not parent[v]:
                parent[v] = u
                q.append(v)
                order.append(v)

    dp: dict[int, tuple[int, int, int]] = {}
    for u in reversed(order):
        kids = [v for v in adj[u] if v != parent[u]]
        if not kids:  # 叶子:上方颜色不合要求就只能自己着色,上方没色更得着色
            dp[u] = (0 if color[u] == BLACK else 1,
                     0 if color[u] == WHITE else 1,
                     1)
            continue
        # u 不着色:子树各自沿用上方状态 u;u 着色:付 1 个代价,子树状态全换成 u 的颜色
        inherit = [sum(dp[v][s] for v in kids) for s in (BLACK, WHITE, NONE)]
        paint = (1 + sum(dp[v][BLACK] for v in kids),
                 1 + sum(dp[v][WHITE] for v in kids))
        dp[u] = tuple(min(paint[0], paint[1], inherit[s]) for s in (BLACK, WHITE, NONE))
    return dp[root][NONE]


def solve() -> None:
    data = iter(map(int, sys.stdin.buffer.read().split()))
    m, n = next(data), next(data)

    color = [0] * (m + 1)
    for leaf in range(1, n + 1):  # 前 n 个节点恰好是全部叶子,依次给出 c_u
        color[leaf] = next(data)

    adj: list[list[int]] = [[] for _ in range(m + 1)]
    for _ in range(m - 1):
        a, b = next(data), next(data)
        adj[a].append(b)
        adj[b].append(a)

    # 题目要求根的度数 > 1;答案与根的选取无关,任取一个满足条件的节点即可
    root = next(u for u in range(1, m + 1) if len(adj[u]) > 1)
    print(min_coloring(adj, color, root))


if __name__ == "__main__":
    solve()

C++ 版(同一算法):

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-10-05 10:10
 * update_at: 2026-10-05 10:10
 */
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;

typedef long long ll;

const int MAXN = 10005; // m <= 10^4

int m, n;                  // m 节点总数,n 叶子个数(节点 1..n 恰好是全部叶子)
int color[MAXN];           // color[u]:叶子 u 的 c_u,0 表示黑,1 表示白,仅 1..n 有效
vector<int> g[MAXN];       // 树的邻接表
int fa[MAXN];              // fa[u]:以 root 为根时 u 的父亲,根的 fa 为 0
int order[MAXN];           // BFS 顺序,逆序即为自底向上的树形 DP 计算顺序
// dp[u][s]:根到 u 的路径上最后一个有色节点的颜色为 s 时,u 子树全部合法所需的最少着色数
// s = 0 表示黑,s = 1 表示白,s = 2 表示路径上尚无有色节点
ll dp[MAXN][3];

// 以 root 为根建立父子关系与 BFS 顺序(迭代写法,深链也不会爆栈)
void build_order(int root) {
    int head = 0;
    int tail = 0;
    fa[root] = 0;
    order[tail] = root;
    tail++;
    while (head < tail) {
        int u = order[head];
        head++;
        for (int i = 0; i < (int)g[u].size(); i++) {
            int v = g[u][i];
            if (v == fa[u]) continue; // 只有父亲这一个邻居已经访问过
            fa[v] = u;
            order[tail] = v;
            tail++;
        }
    }
}

void solve() {
    cin >> m >> n;
    for (int u = 1; u <= n; u++) {
        cin >> color[u];
    }
    for (int i = 1; i <= m - 1; i++) {
        int a, b;
        cin >> a >> b;
        g[a].push_back(b);
        g[b].push_back(a);
    }

    // 题目要求根节点的度数大于 1;答案与根的选取无关,任取一个满足条件的节点作为根
    int root = 1;
    for (int u = 1; u <= m; u++) {
        if (g[u].size() > 1) {
            root = u;
            break;
        }
    }

    build_order(root);

    for (int idx = m - 1; idx >= 0; idx--) { // 逆 BFS 序:保证孩子先于父亲算完
        int u = order[idx];
        // 累加"u 不着色时,子树沿用上方颜色 s"的代价
        ll sum_black = 0;
        ll sum_white = 0;
        ll sum_none = 0;
        int child_cnt = 0; // u 的孩子个数,为 0 说明 u 是叶子
        for (int i = 0; i < (int)g[u].size(); i++) {
            int v = g[u][i];
            if (v == fa[u]) continue;
            sum_black += dp[v][0];
            sum_white += dp[v][1];
            sum_none += dp[v][2];
            child_cnt++;
        }

        if (child_cnt == 0) {
            // 叶子:上方颜色已经是 c_u 就不用着色;上方是另一种颜色或尚无颜色,都必须给它着色
            dp[u][0] = (color[u] == 0) ? 0 : 1;
            dp[u][1] = (color[u] == 1) ? 0 : 1;
            dp[u][2] = 1;
        } else {
            // 内部节点:不着色则沿用上方状态 s;着色则付 1 个代价,孩子的状态全部换成该颜色
            ll paint_black = 1 + sum_black;
            ll paint_white = 1 + sum_white;
            dp[u][0] = min(sum_black, min(paint_black, paint_white));
            dp[u][1] = min(sum_white, min(paint_black, paint_white));
            dp[u][2] = min(sum_none, min(paint_black, paint_white));
        }
    }

    cout << dp[root][2] << endl; // 根上方没有任何有色节点,入状态是"尚无"
}

int main() {
    solve();
    return 0;
}

复杂度

  • 时间复杂度:O(m)\mathcal{O}(m)。BFS 建序与逆序 DP 各遍历每条边常数次,三状态的代价均为线性累加;m⩽104m \leqslant 10^4 远低于 1000 ms1000\,\text{ms} 限制(真实数据实测最快 0.017s、最慢 0.031s)。
  • 空间复杂度:O(m)\mathcal{O}(m)。邻接表、父子数组、访问顺序与每点一个三元组均为线性规模,实测峰值 ⩽19.3 MB\leqslant 19.3\,\text{MB},远低于 128 MB128\,\text{MB}。

总结

  • "最后一个有色节点"就是状态:路径上的历史只有"最后颜色"会影响后续叶子,把它压缩进状态后子树问题立刻满足无后效性与最优子结构。
  • "尚无色"必须独立成状态:它编码了"根到叶路径必须含色点"这一硬约束——路径一路无色时,叶子会被强制着色。
  • 转移只有两个动作:给当前点着色(付 1,孩子的状态全部换成它的颜色)或不着色(沿用上方状态),配合叶子的三段式即可覆盖全部方案。
  • 答案与根的选取无关,任取度数大于 1 的节点作根即可,复杂度 O(m)\mathcal{O}(m)。