1 条题解
-
0
一、题意理解
我们有一棵以 为根的有根树,每个节点 有一个参数 。
一次操作:选择一个白色节点 ,将 到根的路径上距离 小于 的所有节点染黑(包括 本身)。
目标:用最少的操作次数染黑整棵树。
二、样例分析
样例:
n=4 父节点:2的父亲是1,3的父亲是2,4的父亲是1 k = [1, 2, 2, 1]树结构:
1 / \ 2 4 | 3k值:
- 节点1: k=1
- 节点2: k=2
- 节点3: k=2
- 节点4: k=1
操作方案
-
选择节点3(白色),k=2,覆盖节点3、2、1(距离3:0<2,距离2:1<2,距离1:2不小于2?等等,距离计算是到节点i的距离,不是到根的距离。题目说:i到根的链上所有与节点i距离小于k_i的点。这里“与节点i距离”指的是在路径上到i的步数。)
仔细读题:"i 到根的链上(包括节点 i 与根)所有与节点 i 距离小于 k_i 的点"
距离是沿着链到 i 的步数:
- 对于节点3:链是 3-2-1
- 节点3:距离0 < 2 → 染黑
- 节点2:距离1 < 2 → 染黑
- 节点1:距离2 < 2?否 → 不染黑 所以操作3只染黑3和2。
- 对于节点3:链是 3-2-1
-
选择节点4:链4-1
- 节点4:距离0 < 1 → 染黑
- 节点1:距离1 < 1?否 → 不染黑 所以操作4只染黑4。
-
选择节点1:链1
- 节点1:距离0 < 1 → 染黑
总共3次操作。
三、关键观察
一次操作选择节点 ,会染黑 到根路径上距离 小于 的点,即从 往上走最多 步能到达的点(包括 本身)。
换句话说,操作 能覆盖从 往上 个节点(包括 )。
四、问题转化
我们要选择若干个节点,使得每个节点都被某个操作覆盖。
对于节点 ,能被哪些 覆盖?
- 必须在 的子树中(因为操作时 必须白色,但这里其实不要求白色?不对,题目要求操作时 是白色,但我们可以按任意顺序执行,所以顺序重要。但最小操作次数与顺序无关,因为一个节点操作一次就永久变黑,我们可以从深到浅操作。)
实际上,我们可以从叶子往根考虑,这是一个贪心覆盖问题。
五、贪心策略
我们从叶子往根处理,当遇到一个节点还没有被覆盖时,我们必须选择它的某个后代(或它自己)来覆盖它。
为了最大化覆盖范围,我们应选择能覆盖到最上面的操作节点。
设 表示在 的子树中,已经进行的操作能覆盖到 上方多少距离(即从 往上还能额外覆盖多少层)。如果 ,说明 已被覆盖。
初始 表示未被覆盖。
当我们发现 未被覆盖(即 )时,我们必须进行一次操作。为了覆盖 ,我们可以在 的某个后代 操作, 的 要足够大,能覆盖到 。
但为了减少操作次数,我们应选择能覆盖尽可能多上方节点的操作。
实际上,我们可以维护 表示在 的子树中,如果我们在子树内操作,能覆盖到 上方的最远距离(即 ),其中 是 到 的步数。
算法步骤:
- DFS 后序遍历树。
- 维护 $f[u] = \max(0, \max_{v \in subtree(u)} (k_v - dist(u,v)))$。
- 同时维护 表示从 的祖先的操作能覆盖到 下方的最远距离(即从上面传下来的覆盖能力)。
- 当 且 时,说明 未被上方覆盖,但子树内有节点能覆盖 ,此时我们选择那个能覆盖最远的节点(即 对应的节点)进行操作,答案加1,并更新 值。
更简单的做法(常见解法):
设 表示在 的子树中,已选的操作能覆盖到 上方的最大距离(初始0)。 设 表示在 的子树中,某个操作节点能覆盖到 上方的最大距离(即 )。
DFS 过程:
- 计算 对 取最大值,再与0取max。
- 计算 对 取最大值。
- 如果 且 ,说明需要新操作,答案+1,并令 。
这样 表示当前节点被子树内操作覆盖后,还能往上覆盖多少层。
六、正确性解释
- 表示如果在 的子树内进行操作,能覆盖到 上方的最远距离。
- 表示 是否已被覆盖,并且能往上覆盖多远。
- 当 时, 未被覆盖,必须在其子树内进行一次操作,我们选择能覆盖最远的那个操作(即 对应的操作),然后 记录这个覆盖能力。
七、算法步骤
- 建树,读入 。
- DFS 后序遍历:
- $down[u] = \max(0, k_u - 1, \max_{v \in children(u)} (down[v] - 1))$
- 如果 且 :
- 输出 。
八、样例验证
样例: 树:1(2,4), 2(3), 3(), 4() k: 1:1, 2:2, 3:2, 4:1
DFS:
- 节点3: down= max(0, 2-1, []) = 1, up=0 → up=0且down>0 → ans=1, up=1
- 节点4: down= max(0, 1-1, []) = 0, up=0 → 不操作
- 节点2: down= max(0, 2-1, 1-1=0) = 1, up= max(1-1=0) = 0 → up=0且down>0 → ans=2, up=1
- 节点1: down= max(0, 1-1, 1-1=0, 0-1=-1) = 0, up= max(1-1=0, 0-1=-1) = 0
修正: 如果 :
- 如果 ,用 down 对应操作,ans+1, up=down
- 否则(down=0),必须在 操作,ans+1, up = k_u - 1
九、修正算法
设 表示 被覆盖后(或被祖先覆盖)能往上覆盖的最远距离。 设 表示 的子树中能覆盖到的最远祖先距离(即 )。
DFS:
- $best[u] = \max( k_u, \max_{v \in children(u)} (best[v] - 1) )$
- 如果 且 ,则需要在 的子树内选择一个节点操作,选择能提供 的那个节点,然后 ,ans+1
- 将 传递给孩子们:对每个孩子 ,
这样 表示 被覆盖后还能往上覆盖多少,初始0。
十、最终代码(C++)
#include <bits/stdc++.h> using namespace std; const int N = 1e5 + 5; struct edge { int to, next; // 子节点to,下一个子节点的索引 } e[N << 1]; // 存储树的边 int cnt, head[N]; // cnt:边计数器;head[u]:u的子节点链表头 int n, k[N], ans, fa[N]; // fa:父节点;ans:操作次数 // 添加边:u -> v(u是v的父节点) inline void add(int u, int v) { e[++cnt].to = v; e[cnt].next = head[u]; head[u] = cnt; } // 后序遍历处理节点u inline int dfs(int u) { int ret = 0; // 子节点能覆盖到u的最大范围 // 递归处理所有子节点 for (int i = head[u]; i; i = e[i].next) { int v = e[i].to; ret = max(ret, dfs(v)); // 取子节点的最大覆盖范围 } // 子节点无法覆盖u,需操作u if (ret <= 0) { ans++; // 操作次数+1 return k[u] - 1; // u的操作能覆盖父节点方向k[u]-1步 } // 子节点可覆盖u,更新父节点的k值(扩大覆盖范围) k[fa[u]] = max(k[fa[u]], k[u] - 1); return ret - 1; // 覆盖范围向上递减1 } int main() { scanf("%d", &n); // 读入2~n号节点的父节点,构建树 for (int i = 2, x; i <= n; i++) { scanf("%d", &fa[i]); add(fa[i], i); // 父节点->子节点建边 } // 读入每个节点的k值 for (int i = 1; i <= n; i++) { scanf("%d", &k[i]); } // 从根节点开始处理 dfs(1); printf("%d\n", ans); return 0; }
十一、总结
本题的关键在于:
- 理解操作覆盖的范围:从操作节点 往上 个节点。
- 使用贪心策略,从根到叶子传递覆盖信息,当节点未被覆盖时,在子树内选择能覆盖最远的操作。
- 通过 DFS 维护两个值:子树能提供的最佳覆盖能力和当前节点的覆盖状态。 4.该解法时间复杂度 ,可以处理 的数据范围。
- 1
信息
- ID
- 4807
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 10
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者