<think>题目要求在一棵树上选择一条简单路径(从s到t不经过重复节点),使得路径上黑色节点数量不超过k,并且经过的节点总数尽可能多。注意,路径上的节点数就是路径长度(节点数=边数+1)。
由于n最大为1000,我们可以考虑使用树形DP或者枚举直径端点等做法。但是注意到路径上黑色节点数的限制,我们可以考虑使用树形DP,记录路径上黑色节点的数量。
一种常见的做法是定义DP状态为:
dp[u][c]表示在以u为根的子树中,从u出发向下的一条路径(一端是u)上恰好有c个黑色节点时,该路径的最大长度(节点数)。
但是题目要求的是任意两点之间的路径,所以不仅仅是向下的路径,还可能经过LCA后折向另一棵子树。因此,我们需要在每一个节点u处,考虑所有经过u的路径,即两条来自不同子树的路径拼接而成。
具体思路:
1. 首先,我们预处理每个节点的颜色(0或1)。
2. 进行DFS,在DFS过程中,对于每个节点u,我们维护一个数组dp[u][c],表示在u的子树中,从u出发向下的一条路径(到某个叶子节点)上恰好有c个黑色节点时,该路径的最大节点数(包括u)。
3. 同时,在DFS过程中,我们尝试更新答案:对于当前节点u,我们考虑所有以u为LCA的路径。即,我们遍历u的每个子树,然后合并当前子树和之前已经遍历过的子树的信息。具体来说,我们可以用一个临时数组(或者直接双重循环)来组合两条路径(一条来自当前子树,一条来自之前子树),形成一条经过u的路径,并检查黑色节点总数不超过k,然后更新最大节点数。
然而,需要注意的是,k最大1000,n最大1000,如果对每个节点u,我们进行O(k^2)的合并,那么总复杂度是O(n*k^2),即1000*1000*1000=1e9,在C++中可能会超时(尤其树退化成链时,深度为1000,但实际我们合并时是每层子树合并,最坏情况是O(n^2 * k^2)?需要优化)。
因此,我们需要优化合并过程。一种常见的优化是,在合并子树时,我们只保留有效状态(即黑色节点数不超过k),并且使用一个辅助数组tmp,记录在当前已经合并的子树中,从u出发向下,黑色节点数为c时的最大路径长度。然后对于新来的子树,我们枚举该子树中的状态(黑色节点数c1,路径长度len1),然后与tmp中状态(黑色节点数c2,路径长度len2)组合,得到一条经过u的路径,黑色节点数为c1 + c2 - color[u](因为u被计算了两次,需要减去一次)?不对,实际上两条路径在u处拼接,每条路径都包含u,所以拼接后的路径上u只算一次。因此,黑色节点数应该是 c1 + c2 - (color[u]==1?1:0) (如果color[u]是黑色,则两条路径都包含了u的黑色,所以多算了一次,需要减去一次;如果是白色,则没有影响)。但是注意,我们dp[u][c]的定义中,c已经包含了u节点的颜色。所以两条路径拼接后,总的黑色节点数为 c1 + c2 - (color[u]==1?1:0)。
另外,路径长度:两条路径的节点数之和减去1(因为u重复计算了一次),即 len1 + len2 - 1。
所以,在合并子树时,我们枚举当前子树的状态(c1, len1)和之前子树的状态(c2, len2),然后计算总黑色节点数:c_total = c1 + c2 - (color[u]==1?1:0),如果c_total<=k,则更新答案ans = max(ans, len1+len2-1)。
但是,这样合并的复杂度是O(k^2)的,而子树个数最多是O(n)的,所以总复杂度是O(n*k^2),在n=1000,k=1000时,最坏情况是1000*1000*1000=1e9,在C++中可能勉强通过(因为常数较小,且实际树结构可能不是最坏),但题目中n<=1000,k<=1000,且子任务3中n,k都是1000,所以我们需要优化。
优化合并过程:我们可以对每个子树,先枚举其状态(c1, len1),然后我们希望在之前子树的状态中,对于每个c2,我们只关心在黑色节点数不超过某个值的情况下的最大路径长度。但是注意,我们合并时需要满足:c1 + c2 - (color[u]==1?1:0) <= k,即c2 <= k - c1 + (color[u]==1?1:0) (注意,如果color[u]是黑色,则c1+c2-1<=k -> c2<=k+1-c1;如果白色,则c2<=k-c1)。因此,我们可以维护一个辅助数组tmp,表示在之前子树中,黑色节点数为c时的最大路径长度。然后对于当前子树的状态(c1, len1),我们只需要查询tmp数组中c2在[0, up_bound]的最大路径长度,其中up_bound由上述不等式确定。这样,我们可以在O(k)的时间内完成当前子树状态的合并(因为查询区间最大值,我们可以用前缀最大值和后缀最大值来优化)。
具体步骤:
1. 初始化tmp数组,大小为k+1(因为黑色节点数不超过k,我们只关心0~k),初始化为负无穷(或0?但路径至少包含u,所以长度至少为1,但初始时没有子树,所以可以初始化为0,但注意合并时路径长度要减去重复的u,所以需要记录)。
实际上,我们定义tmp[c]表示在已经合并的子树中,从u出发向下,黑色节点数为c时,最大路径长度(节点数)。初始时,tmp[color[u]] = 1(因为u自己,黑色节点数就是color[u]的值,0或1,路径长度为1)。然后其他为负无穷(表示不可达)。
2. 然后,对于u的每个子树v:
a. 首先,对子树v进行DFS,得到dp[v]数组(大小为k+1)。
b. 然后,我们枚举v子树中的状态:对于c1(从0到k),如果dp[v][c1]有值(不为负无穷),则考虑该状态。
c. 接着,枚举之前子树的状态(即tmp数组):对于c2(从0到k),如果tmp[c2]不为负无穷,则计算拼接后的路径:黑色节点数c_total = c1 + c2 - (color[u]==1?1:0) (注意,这里c1和c2都包含了u的黑色?不对!在dp[v][c1]中,c1是从v开始的路径上的黑色节点数(不包括u!)?还是包括u?
这里有一个关键:我们在dp[u][c]的定义中,c是包括u节点的。但是,在子树v中,dp[v][c1]表示的是从v开始(不包括u)的路径,所以它不包含u。因此,我们需要调整。
为了避免混淆,我们重新定义dp[u][c]:表示在u的子树中,从u开始(包括u)向下的一条路径,黑色节点数为c,该路径的最大节点数(包括u)。
那么,在计算dp[u]时,我们首先初始化:dp[u][color[u]] = 1(只有u自己)。然后,对于每个子树v,我们更新dp[u]:对于v中的状态,我们考虑从u到v再延伸,那么路径为u->v->...,所以黑色节点数为 color[u] + (v子树中的黑色节点数),即状态转移为:对于dp[v][c_v],那么新的路径黑色节点数为 c = color[u] + c_v,路径长度为 1 + dp[v][c_v]。
但是,注意:这样定义的话,在合并两条路径时,两条路径都包含u,所以拼接后u被重复计算。因此,拼接后的路径黑色节点数为:c1 + c2 - color[u](因为u被计算了两次,减去一次)。路径长度为:len1 + len2 - 1。
所以,在合并子树时,我们使用tmp数组来保存已经合并的子树的状态(即dp[u]的当前状态,注意dp[u]在合并过程中会更新)。然后对于新的子树v,我们先用dp[u](即tmp)和dp[v]进行组合来更新答案,然后再用dp[v]来更新dp[u](即tmp)。
具体步骤:
1. 初始化:tmp[color[u]] = 1,其他为负无穷(表示不可达)。
2. 对每个子树v:
a. 对v进行DFS,得到dp[v]。
b. 在dp[v]中,我们考虑的是从v开始(不包括u)的路径,所以dp[v][c_v]对应的路径黑色节点数为c_v(不包括u),那么从u到v再到这条路径,黑色节点数就是color[u] + c_v,路径长度就是1+dp[v][c_v](因为多了一个节点u)。
c. 但是,在合并两条路径时,我们考虑的是以u为根的两条不同子树的路径。因此,我们枚举当前子树v的状态(c1, len1)和当前tmp的状态(c2, len2):
- 注意:当前tmp的状态对应的是之前子树合并后的状态,即从u出发到之前子树的一条路径,黑色节点数为c2(包括u),路径长度为len2(包括u)。
- 当前子树v的状态:我们定义一个新的数组,对于v的状态,我们将其转化为从u出发(经过v)的路径:即黑色节点数为 c1' = color[u] + c1(其中c1是dp[v][c1]对应的黑色节点数,注意dp[v][c1]不包括u,所以加上u),路径长度为 len1' = 1 + dp[v][c1](因为加上u)。
- 然后,将当前子树v的这条路径(黑色节点数c1', 长度len1')与之前子树中的任意一条路径(黑色节点数c2, 长度len2)拼接,拼接后的黑色节点数为 c1' + c2 - color[u](因为u被重复计算了一次),路径长度为 len1' + len2 - 1。
- 如果拼接后的黑色节点数<=k,则更新答案。
d. 然后,用子树v的状态更新tmp:对于子树v的每个状态(c1),我们得到新的黑色节点数c1'=color[u]+c1,新的长度len1'=1+dp[v][c1],然后更新tmp[c1'] = max(tmp[c1'], len1')。同时,我们也要更新dp[u](因为dp[u]就是tmp,所以这里用tmp记录dp[u])。
3. 注意:在合并子树时,我们还需要考虑单独一条路径(即只走当前子树v,或者只走之前子树)的情况,这些情况在更新dp[u]时已经包含,并且这些情况在更新答案时,我们可以在枚举所有节点时考虑(因为路径可以是一条链,不一定需要分叉,但一条链的情况实际上在dp[u]中已经记录了,而答案更新时我们只考虑分叉的情况?不对,答案可能是一条链(即不经过u分叉)?但是一条链的情况实际上就是dp[u]中的状态,所以我们在最后答案中应该取所有节点u的dp[u][c](c<=k)的最大值?不,因为答案可能是一条不分叉的链,所以我们在每个节点u处,除了考虑分叉的路径(两条链拼接),还要考虑不分叉的路径(即dp[u][c]本身)。
因此,我们在每个节点u处,先初始化答案:ans = max(ans, dp[u][c] for c<=k) (即不分叉的路径)。然后,在合并子树时,考虑分叉路径(经过u的两条路径拼接)。
但是,注意:在合并子树时,我们还没有更新dp[u](即tmp)之前,tmp中保存的是之前子树的状态,而当前子树v的状态我们还没有加入tmp。所以,我们先用当前子树v的状态和之前子树的状态(tmp)组合得到分叉路径,然后才用v的状态更新tmp(这样避免v的状态和自己组合)。
所以,算法框架如下:
```python
ans = 0
dp[u] = [ -inf ] * (k+1)
dp[u][color[u]] = 1 # 只有u自己
ans = max(ans, 1) # 至少包含u的路径
for each child v of u:
# 递归处理子树v
dfs(v, u)
# 新建一个数组 new_dp 用于保存当前子树v的状态(转化为从u出发)
# 实际上,我们可以不新建,而是直接使用dp[v]来计算转化后的状态
# 枚举v的状态
for c1 in range(0, k+1):
if dp[v][c1] == -inf: continue
# 转化为从u出发:黑色节点数 = color[u] + c1, 路径长度 = 1 + dp[v][c1]
new_c = color[u] + c1
new_len = 1 + dp[v][c1]
if new_c <= k:
# 不分叉的路径(只走v子树)也可以在这里更新,但已经在dp[u]中更新?不,我们还没有更新dp[u],所以这里先更新答案:不分叉的路径实际上在更新dp[u]后会被考虑,所以这里先不更新
# 然后,将当前子树v的状态(new_c, new_len)与之前子树的状态(dp[u])组合
# 组合:枚举之前子树的状态
for c2 in range(0, k+1):
if dp[u][c2] == -inf: continue
total_c = new_c + c2 - color[u]
if total_c <= k:
total_len = new_len + dp[u][c2] - 1 # 因为u重复
ans = max(ans, total_len)
# 然后,更新dp[u]:用new_c, new_len更新dp[u][new_c] = max(dp[u][new_c], new_len)
# 注意:这里更新dp[u]要放在组合之后,避免同一子树内两条路径拼接(题目要求简单路径,不能重复节点,同一子树的路径不能拼接)
# 所以,我们先组合,再更新
# 然后,我们更新dp[u]:对于c1,更新dp[u][new_c] = max(dp[u][new_c], new_len)
for c1 in range(0, k+1):
if dp[v][c1] == -inf: continue
new_c = color[u] + c1
new_len = 1 + dp[v][c1]
if new_c <= k:
if new_len > dp[u][new_c]:
dp[u][new_c] = new_len
# 注意:在更新dp[u]后,我们还要考虑不分叉的路径(即dp[u][c]),但我们在更新dp[u]后,已经包含了不分叉的路径(包括之前子树和当前子树)。而且,我们在上面已经用分叉路径更新了ans,所以最后只需要在函数外取全局ans的最大值。
```
但是,这个双重循环是O(k^2)的,而子树个数最多是O(n),所以总复杂度O(n*k^2),在n=1000,k=1000时,最坏情况是1000 * 1000 * 1000 = 1e9,在C++中可能超时(虽然常数小,但1e9在C++中可能1秒过不了,需要优化)。
优化合并:我们希望在合并两个子树时,对于当前子树v的每个状态(new_c, new_len),我们快速查询之前子树的状态中,满足条件(即拼接后总黑色节点数<=k)的最大长度。具体地,对于当前子树v的一个状态(new_c, new_len),我们需要找到之前子树中满足 c2 <= k - (new_c - color[u]) 的状态中,dp[u][c2]的最大值(注意:总黑色节点数 total_c = new_c + c2 - color[u] <= k => c2 <= k - new_c + color[u])。
所以,我们可以维护一个数组max_dp,其中max_dp[i]表示在dp[u]中,黑色节点数从0到i的最大值(即前缀最大值)。这样,对于当前状态new_c,我们只需要查询 max_dp[up_bound],其中up_bound = k - new_c + color[u](注意,如果up_bound<0,则跳过;如果up_bound>k,则取k)。然后,用 new_len + max_dp[up_bound] - 1 更新答案。
但是注意:这里我们要求c2在[0, up_bound]范围内,所以前缀最大值数组max_dp[i] = max(dp[u][0], dp[u][1], ..., dp[u][i])。
然后,在更新dp[u]时,我们也要更新这个前缀最大值数组。但是,由于dp[u]在更新过程中,我们每加入一个子树v,dp[u]会更新,所以我们需要在更新dp[u]后重新计算前缀最大值。
具体步骤:
1. 初始化dp[u]:dp[u][color[u]]=1,其他为负无穷。然后,构建一个前缀最大值数组max_dp,长度为k+1,其中max_dp[i] = max_{j=0}^{i} dp[u][j]。
2. 对于每个子树v:
a. 递归处理v,得到dp[v]。
b. 对于v的每个状态c1(0~k):
new_c = color[u] + c1
new_len = 1 + dp[v][c1]
if new_c <= k:
# 计算之前子树中满足条件的最大长度:up_bound = k - new_c + color[u]
# 注意:如果color[u]为1,则up_bound = k - new_c + 1;如果为0,则up_bound = k - new_c。
up_bound = k - new_c + color[u]
if up_bound < 0:
# 没有满足条件的,跳过
else:
if up_bound > k: up_bound = k
candidate = new_len + max_dp[up_bound] - 1
ans = max(ans, candidate)
c. 然后用v的状态更新dp[u]:对于每个c1,new_c=color[u]+c1, new_len=1+dp[v][c1],如果new_c<=k,则dp[u][new_c] = max(dp[u][new_c], new_len)
d. 更新dp[u]后,重新计算前缀最大值数组max_dp:max_dp[0]=dp[u][0],然后max_dp[i]=max(max_dp[i-1], dp[u][i]) for i from 1 to k。
3. 同时,在更新dp[u]后,我们还要考虑不分叉的路径:即dp[u][c]本身,所以我们在更新dp[u]后,用dp[u][c](c<=k)更新ans:ans = max(ans, dp[u][c])。但注意,在合并子树时,分叉路径已经更新了ans,而不分叉的路径在dp[u]中,所以我们在最后遍历所有节点u和所有c来更新ans?实际上,在更新dp[u]后,我们可以立即用dp[u][c]更新ans,但这样可能会重复更新(分叉路径和不分叉路径是分开更新的)。我们可以在每个节点u处,在更新完所有子树后,用dp[u][c]更新ans。但是,分叉路径已经在合并子树时更新了,所以这里只需要在合并子树前,先考虑不分叉路径?实际上,在初始化dp[u]时,我们就已经考虑了不分叉路径(只有u自己)。然后,在合并子树时,我们更新了dp[u](即不分叉路径也更新了),所以最后我们只需要在合并子树后,再取一次dp[u][c]的最大值?但是,分叉路径的更新已经单独计算了,所以这里只需要取不分叉路径的最大值,即dp[u][c]的最大值。但是,注意:分叉路径的拼接是在两个子树之间,而dp[u][c]只记录不分叉路径(从u出发向下的一条链)。所以,我们需要在合并子树之前,先更新不分叉路径的ans?实际上,我们可以在初始化dp[u]后更新一次ans(即只有u),然后在更新dp[u]的过程中,每次更新dp[u][c]时,都可以更新ans(即不分叉路径)。但是,在合并子树时,我们更新dp[u]是在组合之后,所以组合时我们用的是之前的dp[u](即不包括当前子树的状态),所以不会重复。
因此,我们可以在每个节点u处:
ans = max(ans, 1) # 只有u
然后对于每个子树v,先组合(用当前子树v的状态和之前子树的状态组合),然后更新dp[u](加入当前子树v的状态),并同时更新ans:ans = max(ans, dp[u][c] for all c) # 这里dp[u][c]已经更新,所以包含了不分叉路径。
但是,这样更新ans(在加入子树v后,用dp[u][c]更新ans)会包括不分叉路径,但注意:不分叉路径在dp[u]中,所以我们在整个过程中,ans取所有情况的最大值。
所以,算法框架:
ans = max(ans, 1) # 初始化
dp[u][color[u]] = 1
max_dp = 初始化前缀最大值数组(大小为k+1)
for each child v of u:
dfs(v, u)
# 用v的状态和之前的dp[u](即max_dp)组合
for c1 in range(0, k+1):
if dp[v][c1] is not -inf:
new_c = color[u] + c1
new_len = 1 + dp[v][c1]
if new_c <= k:
up_bound = k - new_c + color[u]
if up_bound < 0:
# 跳过
else:
if up_bound > k: up_bound = k
candidate = new_len + max_dp[up_bound] - 1
ans = max(ans, candidate)
# 用v的状态更新dp[u]
for c1 in range(0, k+1):
if dp[v][c1] == -inf: continue
new_c = color[u] + c1
new_len = 1 + dp[v][c1]
if new_c <= k:
if new_len > dp[u][new_c]:
dp[u][new_c] = new_len
# 更新dp[u]后,重新计算max_dp(前缀最大值数组)
max_dp[0] = dp[u][0]
for i in range(1, k+1):
max_dp[i] = max(max_dp[i-1], dp[u][i])
# 同时,用更新后的dp[u]更新不分叉路径的ans(实际上,因为dp[u]更新了,所以不分叉路径可能变长,但我们在组合时已经考虑了之前的状态,这里只需要更新ans)
# 注意:dp[u]中存的是不分叉路径,所以我们可以在这里更新ans:ans = max(ans, max_dp[k]) 但是,max_dp[k]是0~k的最大值,所以我们可以用max_dp[k]更新,但更准确的是,我们可以在更新dp[u]的过程中记录当前dp[u]的最大值,但这里我们可以在重新计算max_dp后,用max_dp[k]更新ans:ans = max(ans, max_dp[k]) 但是,注意,分叉路径可能比不分叉路径长,而分叉路径已经在组合时更新了ans,所以这里只需要更新不分叉路径?实际上,dp[u][c]就是不分叉路径,所以ans = max(ans, max_dp[k]) 就是更新不分叉路径的最大值。但是,在组合时我们并没有更新不分叉路径,所以这里需要更新。而且,在初始化dp[u]时我们已经更新了ans=1,所以这里更新不会漏掉。
# 最后,在u处,ans已经更新了不分叉路径和分叉路径。
4. 注意:在计算前缀最大值数组max_dp时,我们只需要在更新dp[u]后重新计算一次(即每个子树v更新后)。
5. 另外,注意:在组合时,我们用的是更新前的max_dp(即不包括当前子树v的状态),这样避免同一子树内两条路径拼接(因为题目要求简单路径,不能重复节点,所以同一子树内的两条路径不能拼接,因为它们在u以下没有公共点,但u是公共点,而路径要求不重复节点,所以同一子树内的两条路径无法拼接成一条简单路径?不对,同一子树内的两条路径在u以下会分叉,但它们在u以下有公共祖先(除了u),所以会重复节点?因此,同一子树内的两条路径不能拼接。所以,我们使用更新前的dp[u](即之前子树的状态)和当前子树v的状态组合,这样保证两条路径来自不同子树,在u以下没有公共节点(除了u),因此拼接后是一条简单路径。
6. 最后,注意:树可能是一条链,所以我们需要考虑所有节点。
7. 边界:如果k=0,则不能经过任何黑色节点。
8. 初始化dp数组:所有节点初始化为负无穷(-1e9),然后dp[u][color[u]]=1。
9. 注意:节点颜色:a_i=0或1,color[u] = a[u-1](如果节点编号从1到n,输入第二行是a0, a1,...,a_{n-1},对应节点1到n)。
10. 最终答案:ans可能至少为1(一个节点),但题目要求最大节点数。
11. 注意:在更新dp[u]时,我们只考虑new_c<=k,因为超过k的状态没有用。
12. 另外,在更新ans时,我们只考虑总黑色节点数<=k的情况。
13. 最后,全局ans取所有节点处计算得到的ans的最大值?不对,因为ans是全局变量,我们在每个节点u处更新它。
14. 但是,注意:一条路径只会在其LCA处被计算一次(因为我们在LCA处拼接两条路径,或者记录不分叉路径)。所以,我们只需要在DFS过程中更新ans。
15. 另外,还有一种情况:路径可能不经过u,而在u的子树内。但是,我们在子树v中已经递归计算了,所以不需要额外处理。
16. 因此,我们只需要一个全局变量ans,在DFS过程中更新。
17. 最后,输出ans。
18. 注意:如果树为空,但n>=1,所以不会为空。
19. 实现细节:使用邻接表存图,注意无向边,DFS时记录父节点避免回走。
20. 复杂度:O(n*k),因为每个子树v,我们更新dp[u]是O(k),重新计算前缀最大值O(k),所以每个节点u的总复杂度是O(deg(u)*k),总复杂度O(n*k),因为树中所有节点的度数之和为2(n-1),所以总复杂度O(n*k),在n,k<=1000时,最大操作次数1000*1000=1e6,可以接受。
所以,我们按照这个思路实现。
注意:在组合时,我们计算up_bound = k - new_c + color[u],然后取max_dp[up_bound](注意max_dp[up_bound]是dp[u][0..up_bound]的最大值)。然后,用new_len + max_dp[up_bound] - 1更新ans。
但是,注意:new_c = color[u] + c1,而c1是dp[v][c1]中的黑色节点数(不包括u),所以new_c就是加上u后的黑色节点数(如果u是黑色,则new_c=c1+1;如果u是白色,则new_c=c1)。然后,拼接时,总黑色节点数 = new_c + (之前子树路径的黑色节点数) - color[u] = c1 + (之前子树路径的黑色节点数) [因为new_c - color[u] = c1]。
所以,总黑色节点数 = c1 + c2_prev,其中c2_prev是之前子树路径的黑色节点数(包括u)。但是,之前子树路径的黑色节点数c2_prev中已经包含了u,而c1中不包含u(因为dp[v][c1]不包含u),所以总黑色节点数 = c1 + c2_prev - 0? 不对,因为u在之前子树路径中已经计算了,而当前子树路径不包含u(除了u,但u在new_c中已经加了一次?不对,我们new_c已经包含了u,但dp[v][c1]不包含u,所以new_c = color[u] + c1,所以当前子树路径的黑色节点数包括u。而之前子树路径的黑色节点数c2_prev也包括u。所以总黑色节点数 = new_c + c2_prev - color[u] (因为u被重复计算了一次)。
因此,计算正确。
但是,在计算up_bound时,我们要求:new_c + c2_prev - color[u] <= k => c2_prev <= k - new_c + color[u]。
所以,up_bound = k - new_c + color[u]。
然后,我们取max_dp[up_bound](即c2_prev在0到up_bound的最大路径长度)。
注意:max_dp数组是dp[u]的前缀最大值,而dp[u][c2_prev]表示从u出发到之前子树的一条路径(不分叉)的最大长度(节点数)。
所以,路径拼接:总长度 = new_len + (之前子树路径长度) - 1,因为u重复。
然后,更新ans。
最后,我们还要注意:在更新dp[u]时,我们使用当前子树v的状态更新dp[u]:new_c = color[u] + c1, new_len = 1 + dp[v][c1],然后dp[u][new_c] = max(dp[u][new_c], new_len)。
另外,在初始化dp[u]时,我们只初始化了dp[u][color[u]]=1,然后max_dp[0]=dp[u][0],max_dp[1]=max(dp[u][0],dp[u][1]),等等。
然后,在更新完所有子树后,dp[u]就是最终状态。
最后,注意:全局ans的初始化:ans=0,然后DFS过程中更新。
但是,有可能树中所有节点都是黑色,而k=0,则路径只能包含0个黑色节点,所以只能选择0个节点?但至少可以选一个白色节点?如果k=0,则不能有黑色节点。所以,如果u是白色,则dp[u][0]=1;如果u是黑色,则dp[u][1]=1,但k=0时,黑色节点数不能超过0,所以dp[u][1]不能使用。所以,在初始化时,如果u是黑色,则dp[u][1]=1,然后ans=max(ans, dp[u][1]),但k=0时,我们不会取dp[u][1](因为要求黑色节点数<=0,所以只能取dp[u][0]?但是u是黑色,dp[u][0]不存在(为负无穷),所以ans不会被更新。所以,我们需要在更新ans时,只考虑黑色节点数<=k的状态。在组合时,我们只考虑总黑色节点数<=k。在更新不分叉路径时,我们只考虑dp[u][c](c<=k)来更新ans。
因此,在更新ans时,我们确保黑色节点数不超过k。
所以,代码实现如下:
注意:节点编号从1到n。
输入:第一行n,k
第二行n个整数,表示节点i的颜色a_i(0或1)
接下来n-1行,每行两个整数u,v
我们用邻接表存图:vector<vector<int>> graph(n+1)
然后,从根节点1开始DFS。
全局变量:ans = 0
注意:可能没有节点可选(k<0),但k>=0,且节点颜色非0即1,所以k>=0时,至少可以选一个白色节点(如果有白色节点)。如果整棵树都是黑色且k=0,则没有合法路径?但题目要求经过的节点数尽可能多,至少为0?但题目要求输出一个正整数,而路径至少包含一个节点?题目没有说,但按照定义,路径至少包含一个节点(起点和终点相同)。所以,如果k=0且整棵树都是黑色,则没有合法路径?但题目要求至多经过k个黑色节点,k=0时不能经过黑色节点,所以只能选白色节点。如果整棵树没有白色节点,则无法选择任何节点?但题目要求从s到t,s和t是树中的节点,所以必须选节点。因此,如果整棵树都是黑色且k=0,则没有合法路径,但题目没有说明,但根据数据范围,k>=0,且n>=1,所以至少有一个节点,如果这个节点是黑色且k=0,则不能选择。所以,我们初始ans=0,然后如果能选则更新,最后输出ans。但是,题目要求输出最多经过的节点数,如果没有任何路径满足条件(即k=0且没有白色节点),则输出0?但题目没有说,但样例输出都是正整数。我们再看题目:小杨希望自己在至多经过k个黑色节点的前提下,经过的总节点数尽可能多。所以,如果没有任何路径满足(即没有节点可选),则输出0?但题目中节点数至少为1,所以不存在0个节点的情况。但是,如果k=0且第一个节点是黑色,那么它不能选,但是我们可以选一个白色节点?如果整棵树没有白色节点,那么就没有合法路径。因此,我们需要考虑ans可能为0。
所以,在DFS之前,我们可以初始化ans=0,然后在DFS过程中,如果遇到白色节点(且k>=0)则ans至少为1。如果整棵树都是黑色且k=0,则ans保持0。
所以,最后输出ans。
但是,注意:我们更新ans时,只有满足黑色节点数<=k的路径才更新ans。所以,如果整棵树都是黑色且k=0,则ans不会被更新,输出0。
因此,代码中ans初始化为0。
最后,输出ans。
现在,我们写代码。
注意:dp数组用二维数组,大小为[n+1][k+1],初始化为负无穷(-1e9)。但是,n和k最大1000,所以总空间1000*1000=1e6,可以接受。
但是,我们使用递归DFS,递归深度最多1000,不会栈溢出。
代码步骤:
全局变量:ans = 0
dp[u]:一个大小为k+1的数组,初始化为-10**9(一个很小的负数)
color数组:用输入的a数组,节点u的颜色为a[u-1](如果节点编号从1开始,则节点1的颜色为a[0])
DFS(u, parent):
dp[u][ color[u] ] = 1 # 颜色为0或1
# 更新ans:如果color[u]<=k,则ans=max(ans,1)
if color[u] <= k:
ans = max(ans, 1)
# 初始化max_dp:大小为k+1,max_dp[i]表示dp[u][0..i]的最大值
vector<int> max_dp(k+1, -1000000000);
if (color[u] <= k) {
max_dp[color[u]] = 1;
}
// 注意:max_dp[0] = max(dp[u][0], ...) 但这里我们只初始化了color[u]的位置,其他位置还是负无穷
// 所以,我们需要重新构建max_dp:从0到k
// 先初始化max_dp[0] = dp[u][0]
// 然后,从1到k:max_dp[i] = max(max_dp[i-1], dp[u][i])
// 但是,dp[u]除了color[u]位置,其他都是负无穷,所以先构建
vector<int> temp_max(k+1, -1000000000);
temp_max[0] = dp[u][0];
for (int i=1; i<=k; i++) {
temp_max[i] = max(temp_max[i-1], dp[u][i]);
}
max_dp = temp_max; // 这样max_dp就是前缀最大值数组
for (each child v of u):
if v == parent: continue
dfs(v, u) // 递归处理v
// 先组合:用v的状态和当前的max_dp(即更新v之前dp[u]的状态)组合
for (int c1=0; c1<=k; c1++) {
if (dp[v][c1] < 0) continue; // 无效状态
int new_c = color[u] + c1; // 从u出发,经过v的路径:黑色节点数(包括u)
if (new_c > k) continue; // 如果超过k,则跳过
int new_len = 1 + dp[v][c1]; // 路径长度(包括u)
// 计算之前子树中满足条件的c2的上界: up_bound = k - new_c + color[u]
int up_bound = k - new_c + color[u];
if (up_bound < 0) {
// 没有满足条件的c2
// 但是,这里我们只考虑分叉路径,所以即使没有之前子树,我们也可以不考虑
// 所以,这里不更新ans(分叉路径至少需要两条路径,所以这里跳过)
// 注意:不分叉路径已经在dp[u]中,所以这里只考虑分叉
continue;
}
if (up_bound > k) up_bound = k;
// 查询max_dp[up_bound]:即之前子树中黑色节点数不超过up_bound的最大路径长度
if (max_dp[up_bound] < 0) continue; // 没有有效状态
int total_len = new_len + max_dp[up_bound] - 1;
ans = max(ans, total_len);
}
// 然后,用v的状态更新dp[u]
for (int c1=0; c1<=k; c1++) {
if (dp[v][c1] < 0) continue;
int new_c = color[u] + c1;
if (new_c > k) continue;
int new_len = 1 + dp[v][c1];
if (new_len > dp[u][new_c]) {
dp[u][new_c] = new_len;
}
}
// 更新dp[u]后,重新计算max_dp(前缀最大值数组)
temp_max[0] = dp[u][0];
for (int i=1; i<=k; i++) {
temp_max[i] = max(temp_max[i-1], dp[u][i]);
}
max_dp = temp_max;
// 在更新完所有子树后,用dp[u]更新不分叉路径的ans
// 因为dp[u]在更新过程中已经更新了不分叉路径,我们在组合时已经考虑了分叉路径,所以这里只需要用dp[u]更新ans(即不分叉路径)
// 但是,注意:我们已经在初始化dp[u]时更新了ans=1,然后在更新dp[u]的过程中,我们可能更新了dp[u][c](变得更长),所以这里再更新一次不分叉路径的最大值
// 实际上,我们可以在更新dp[u][new_c]时更新ans,但为了清晰,我们在更新完所有子树后,用max_dp[k]更新ans(因为max_dp[k]是dp[u]的最大值)
// 但是,dp[u][c]可能超过k?不,我们只更新new_c<=k,所以dp[u]中只有0~k的状态。
// 所以,ans = max(ans, max_dp[k]) 但是,max_dp[k]是dp[u]的最大值,所以用max_dp[k]更新ans。
// 但是,注意:分叉路径可能比不分叉路径长,而分叉路径已经在组合时更新了ans,所以这里更新不分叉路径不会超过分叉路径,但为了不漏掉,我们更新。
// 实际上,在更新dp[u]时,每次更新dp[u][new_c]时,我们都可以更新ans(不分叉路径),但为了代码清晰,我们在更新完所有子树后更新。
// 所以,在for each child循环结束后,更新:
ans = max(ans, max_dp[k]); // 但是,max_dp[k]是0~k的最大值,所以就是dp[u]的最大值
但是,注意:在更新dp[u]时,我们可能已经更新了ans(在组合时),而max_dp[k]是更新后的dp[u]的最大值,所以这里更新ans不会错。
但是,注意:在组合时,我们只更新了分叉路径,而这里更新不分叉路径(dp[u]的最大值)。
所以,整个DFS(u)中,我们更新了分叉路径和不分叉路径。
最后,输出全局ans。
但是,有一个问题:在组合时,我们用了max_dp[up_bound],而max_dp[up_bound]是dp[u]的前缀最大值,它可能来自之前子树的不分叉路径,也可能来自更早之前子树合并后的状态?是的,dp[u]就是记录从u出发向下的一条路径(不分叉)的最大长度。所以,max_dp[up_bound]就是之前子树中满足黑色节点数<=up_bound的最长路径。
然后,我们组合:当前子树v的路径(new_c, new_len)和之前子树的一条路径(长度为max_dp[up_bound])拼接,得到分叉路径。
所以,正确。
最后,注意:在更新dp[u]时,我们只更新不分叉路径(即从u出发,只走一个子树),所以dp[u][new_c] = max(原来的dp[u][new_c], new_len),而new_len是当前子树v的路径长度(包括u)。
然后,重新计算max_dp。
所以,代码实现如下。
注意:我们使用全局变量ans,在DFS过程中更新。
另外,为了避免递归爆栈(n=1000,深度最大1000,在C++中可能栈溢出?但1000层递归一般没问题),我们使用非递归DFS?或者用栈模拟递归?这里用递归。
最后,注意:如果k=0,且节点u是黑色,则dp[u][1]=1,但1>0,所以不会更新dp[u][1](因为new_c=1>0,所以跳过),所以dp[u][1]不会被设置,保持负无穷。所以,dp[u]中只有dp[u][0]和dp[u][1]?不对,u是黑色,则dp[u][color[u]]=dp[u][1]=1,所以会被设置。但是,在更新ans时,我们要求黑色节点数<=k(k=0),所以dp[u][1](黑色节点数1>0)不会用来更新ans。所以,ans不会更新。所以,如果整棵树都是黑色且k=0,则ans保持0。
所以,输出0。
但是,如果k=0,且节点u是白色,则dp[u][0]=1,然后更新ans=max(0,1)=1。
所以,正确。
我们测试样例。
样例输入:
5 1
0 0 1 1 1 # 节点1:0, 节点2:0, 节点3:1, 节点4:1, 节点5:1
1 2
2 3
2 5
1 4
树结构:1-2, 2-3, 2-5, 1-4
我们以1为根,则2和4是1的儿子,3和5是2的儿子。
我们期望输出3。
我们模拟节点1的DFS:
u=1,颜色0,dp[1][0]=1,ans=max(0,1)=1。
初始化max_dp:max_dp[0]=1,其他为负无穷(然后前缀最大值:max_dp[0]=1, max_dp[1]=1, ... max_dp[k]=1? 不对,k=1,所以max_dp[0]=1, max_dp[1]=max(1, dp[1][1]),但dp[1][1]为负无穷,所以max_dp[1]=1?不对,dp[1]只有dp[1][0]=1,dp[1][1]为负无穷。所以,max_dp[0]=1, max_dp[1]=1(因为前缀最大值,所以max_dp[1]=max(1, dp[1][1]),但dp[1][1]是负无穷,所以max_dp[1]=1?不对,max_dp[i]表示0..i的最大值,所以max_dp[1]应该等于1(因为dp[1][0]=1,dp[1][1]为负无穷,所以最大值是1)。所以,max_dp[1]=1。
然后,遍历1的儿子:先遍历2(或者4,顺序无所谓,假设先2后4)
递归处理2:
u=2,颜色0,dp[2][0]=1,ans=max(1,1)=1。
max_dp[0]=1, max_dp[1]=1(因为k=1)
遍历2的儿子:3和5
先3:
u=3,颜色1,dp[3][1]=1,ans=max(1,1)=1。
没有儿子,所以直接返回。
然后,在2处,处理3:
组合:当前子树3的状态:c1=1(因为dp[3][1]=1,所以c1=1),new_c = color[2]+c1 = 0+1=1,new_len=1+1=2。
检查:new_c=1<=k=1,继续。
up_bound = k - new_c + color[2] = 1-1+0=0。
查询max_dp[0]=1(大于0),所以candidate = 2 + 1 - 1 = 2(总长度)。
更新ans=max(1,2)=2。
然后,更新dp[2]:dp[2][1]=max(dp[2][1], 2) -> 原来dp[2][1]为负无穷,所以dp[2][1]=2。
然后重新计算max_dp:max_dp[0]=dp[2][0]=1, max_dp[1]=max(1,2)=2。
然后5:
u=5,颜色1,dp[5][1]=1。
同样,在2处处理5:
组合:c1=1,new_c=0+1=1,new_len=1+1=2。
up_bound = 1-1+0=0。
max_dp[0]=1,candidate=2+1-1=2,ans=max(2,2)=2。
更新dp[2][1]=max(2,2)=2。
重新计算max_dp:max_dp[0]=1, max_dp[1]=2。
然后,在2处,更新不分叉路径:ans = max(ans, max_dp[k]=2) -> ans=2。
(注意,在2处,分叉路径已经更新了ans=2,然后不分叉路径dp[2]的最大值为2,所以ans=2)
然后,回到1,处理子树2:
组合:当前子树2的状态:枚举c1=0和c1=1
c1=0: new_c = color[1]+0=0+0=0, new_len=1+dp[2][0]=1+1=2.
检查:new_c=0<=k=1,继续。
up_bound = k - new_c + color[1] = 1-0+0=1.
查询max_dp[1]=1(因为当前1的max_dp[1]=1,注意这是1在更新2子树之前的状态,即dp[1][0]=1,所以max_dp[1]=1)
candidate = 2 + 1 - 1 = 2,ans=max(2,2)=2。
c1=1: new_c=0+1=1, new_len=1+dp[2][1]=1+2=3.
up_bound = 1-1+0=0.
max_dp[0]=1,candidate=3+1-1=3,ans=max(2,3)=3.
然后,更新dp[1]:用子树2的状态
c1=0: new_c=0, new_len=2 -> dp[1][0]=max(1,2)=2.
c1=1: new_c=1, new_len=3 -> dp[1][1]=max(-inf,3)=3.
重新计算1的max_dp:max_dp[0]=2, max_dp[1]=max(2,3)=3.
然后,处理1的另一个儿子4:
递归处理4:
u=4,颜色1,dp[4][1]=1,ans=max(3,1)=3.
没有儿子,返回。
在1处,处理4:
组合:当前子树4的状态:c1=1(因为dp[4][1]=1),new_c=0+1=1, new_len=1+1=2.
up_bound = 1-1+0=0.
max_dp[0]=2(因为1的max_dp在更新2后:max_dp[0]=2, max_dp[1]=3,所以max_dp[0]=2),candidate=2+2-1=3,ans=max(3,3)=3.
更新dp[1]:c1=1, new_c=1, new_len=2 -> dp[1][1]=max(3,2)=3(不变).
重新计算max_dp:max_dp[0]=2, max_dp[1]=3.
然后,更新不分叉路径:ans = max(3, max_dp[1]=3) -> 3.
最后,在1处,ans=3.
所以,最终ans=3,输出3。
因此,代码正确。
但是,注意:在组合时,我们枚举了当前子树v的状态,而v的状态可能很多(0~k),所以内层循环是O(k),然后外层循环是子树个数,总复杂度O(deg(u)*k),然后总节点u的复杂度之和为O(n*k),因为所有节点的度数之和为2(n-1),所以总复杂度O(n*k),在n,k<=1000时,最大为10^6,可以接受。
代码实现:
注意:dp[u]是一个大小为k+1的数组,初始化为负无穷(比如-10^9)
颜色数组:用vector<int> color(n+1),输入时:for i=1 to n: color[i]=a[i-1]
邻接表:vector<vector<int>> graph(n+1)
全局ans:int ans=0
注意:在组合时,我们只枚举c1从0到k,并且只枚举有效状态(dp[v][c1]>=0?但初始化为负无穷,所以用<0判断无效)。但是,路径长度至少为1,所以dp[v][c1]>=1,所以我们可以用>0判断?但初始化负无穷,所以用dp[v][c1]!=-inf判断,但为了避免浮点数,我们用整数,初始化为-1000000。
代码:
#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>
using namespace std;
const int INF = -100000000;
int ans = 0;
void dfs(int u, int parent, vector<vector<int>>& graph, vector<int>& color, vector<vector<int>>& dp, int k) {
// 初始化dp[u]: 只有u自己
for (int i = 0; i <= k; i++) {
dp[u][i] = INF;
}
if (color[u] <= k) {
dp[u][color[u]] = 1;
}
if (color[u] <= k) {
ans = max(ans, 1);
} else {
// 如果color[u]>k,则不能选,但题目中color[u]只能是0或1,所以不会进入
}
// 构建max_dp:前缀最大值数组,大小为k+1
vector<int> max_dp(k+1, INF);
if (k >= 0) {
max_dp[0] = dp[u][0];
for (int i = 1; i <= k; i++) {
max_dp[i] = max(max_dp[i-1], dp[u][i]);
}
}
for (int v : graph[u]) {
if (v == parent) continue;
dfs(v, u, graph, color, dp, k);
// 先组合:用v的状态和max_dp组合
for (int c1 = 0; c1 <= k; c1++) {
if (dp[v][c1] == INF) continue;
int new_c = color[u] + c1;
if (new_c > k) continue;
int new_len = 1 + dp[v][c1];
// 计算up_bound
int up_bound = k - new_c + color[u];
if (up_bound < 0) continue;
if (up_bound > k) up_bound = k;
// 如果max_dp[up_bound]为INF,则跳过
if (max_dp[up_bound] == INF) continue;
int total_len = new_len + max_dp[up_bound] - 1;
ans = max(ans, total_len);
}
// 用v的状态更新dp[u]
for (int c1 = 0; c1 <= k; c1++) {
if (dp[v][c1] == INF) continue;
int new_c = color[u] + c1;
if (new_c > k) continue;
int new_len = 1 + dp[v][c1];
if (new_len > dp[u][new_c]) {
dp[u][new_c] = new_len;
}
}
// 更新max_dp:重新计算
max_dp[0] = dp[u][0];
for (int i = 1; i <= k; i++) {
max_dp[i] = max(max_dp[i-1], dp[u][i]);
}
}
// 用dp[u]更新不分叉路径的ans
for (int i = 0; i <= k; i++) {
if (dp[u][i] > ans) {
ans = dp[u][i];
}
}
// 或者:ans = max(ans, max_dp[k]); 但是,max_dp[k]是0..k的最大值,所以也可以
// 但上面已经用dp[u]的所有值更新了ans,所以可以省略
}
int main() {
int n, k;
cin >> n >> k;
vector<int> color(n+1);
for (int i = 1; i <= n; i++) {
cin >> color[i];
}
vector<vector<int>> graph(n+1);
for (int i = 0; i < n-1; i++) {
int u, v;
cin >> u >> v;
graph[u].push_back(v);
graph[v].push_back(u);
}
// dp[i][j] 表示节点i的子树中,从i出发的路径,黑色节点数为j的最大节点数
vector<vector<int>> dp(n+1, vector<int>(k+1, INF));
ans = 0;
dfs(1, -1, graph, color, dp, k);
cout << ans << endl;
return 0;
}
但是,注意:在组合时,我们枚举了v的状态,然后与max_dp组合。而max_dp是当前节点u在加入v之前的状态(即不包括v子树的状态)。这样保证了两条路径来自不同子树。
另外,在更新dp[u]时,我们更新了dp[u][new_c] = max(原值, new_len),然后重新计算max_dp,这样在下一个子树时,max_dp就包含了之前所有子树的状态。
最后,在DFS(1)后,输出ans。
注意:在组合时,我们可能更新ans为分叉路径,而在最后,我们用dp[u]更新不分叉路径,所以不会漏掉。
但是,有一个问题:在更新dp[u]时,我们可能用同一个子树v的路径更新了多次?不会,因为更新dp[u]时,我们只更新了不分叉路径(即一条路径),所以不会重复。
另外,注意:在组合时,我们枚举v的状态,然后与之前子树的状态组合,然后才更新dp[u](加入v的状态),所以下一个子树时,max_dp就包含了v子树的状态。
所以,正确。
但是,我们测试样例。
注意:样例输入中,节点颜色:0 0 1 1 1,对应节点1到5。
运行上面的代码,应该输出3。
如果遇到问题,可以调试。
由于题目有多个子任务,我们确保通过样例。
注意:子任务1:树的形态为一条链,我们可以测试链。
例如,链:1-2-3,颜色0,0,1,k=1,那么最长路径是3个节点(1,2,3),黑色节点数=1(节点3),满足<=1,所以输出3。
在链的情况下,我们模拟:
u=1,颜色0,dp[1][0]=1,ans=1。
然后,儿子2:递归处理2。
2:颜色0,dp[2][0]=1,ans=max(1,1)=1。
然后,儿子3:递归处理3。
3:颜色1,dp[3][1]=1,ans=max(1,1)=1。
然后,在2处处理3:
组合:c1=1(因为dp[3][1]=1),new_c=0+1=1,new_len=2。
up_bound = k - new_c + color[2] = 1-1+0=0.
max_dp[0]=1(因为2的dp[2][0]=1,所以max_dp[0]=1),candidate=2+1-1=2,ans=max(1,2)=2。
然后更新dp[2]:dp[2][1]=max(原值,2)=2。
重新计算max_dp:max_dp[0]=1, max_dp[1]=2。
然后,在2处,更新不分叉路径:ans=max(2,2)=2。
然后,回到1,处理子树2:
组合:枚举2的状态:c1=0和c1=1
c1=0: new_c=0+0=0, new_len=1+dp[2][0]=1+1=2.
up_bound = k - new_c + color[1] = 1-0+0=1.
max_dp[1]=1(因为1的max_dp在初始化时:dp[1][0]=1,所以max_dp[0]=1, max_dp[1]=1),candidate=2+1-1=2,ans=max(2,2)=2.
c1=1: new_c=0+1=1, new_len=1+2=3.
up_bound=1-1+0=0.
max_dp[0]=1,candidate=3+1-1=3,ans=3.
然后更新dp[1]:c1=0: dp[1][0]=max(1,2)=2; c1=1: dp[1][1]=max(-inf,3)=3.
重新计算max_dp:max_dp[0]=2, max_dp[1]=3.
然后,在1处,更新不分叉路径:ans=max(3,3)=3.
所以,输出3,正确。
因此,代码正确。
最后,注意:在链的情况下,我们也可以选择1-2,长度为2,黑色节点0,所以ans至少为2,而我们的算法在2处已经更新ans=2(分叉路径?不对,在2处,分叉路径是2-3和2自己?不对,2没有其他子树,所以组合时是2和3组合,得到2-3(长度为2)?不对,组合时,2的子树3:状态c1=1,new_c=0+1=1,new_len=2(表示2->3,节点2和3,长度2),然后与之前子树的状态(即2自己)组合:之前子树的状态是dp[2][0]=1(表示只有2),然后拼接:总长度=2+1-1=2(节点2重复了,所以是节点2和3,长度2?不对,应该是2->3,长度为2,但为什么是2?因为new_len=2(2->3),而之前子树的状态是长度为1(只有2),拼接后是2->3和2(合并后是2->3,但2重复了?所以应该是2->3,长度为2,但2已经包含在2->3中,所以实际上拼接后还是2->3?不对,分叉路径要求两条路径在u(这里是2)处分叉,但这里只有一条路径(3)和u自己(2),所以拼接后就是2->3,长度为2。而之前不分叉路径已经记录了2->3(长度为2),所以ans=2。
然后,在1处,组合2的状态(c1=1)时,new_len=3(表示1->2->3,长度为3),然后与之前子树的状态(1自己)组合:总长度=3+1-1=3,所以ans=3。
所以,正确。
因此,我们输出代码。
注意:在链的情况下,我们也可以选择1-2-3,长度为3,所以ans=3。
所以,最终代码。
但是,注意:题目中k可能为0,我们测试k=0的情况。
例如,链:1-2-3,颜色0,0,1,k=0,则最长路径为2(1-2),因为3是黑色,不能选。
在代码中:
u=1:颜色0,dp[1][0]=1,ans=1。
然后,儿子2:递归处理2。
2:颜色0,dp[2][0]=1,ans=1。
儿子3:颜色1,dp[3][1]=1,但1>0,所以dp[3][1]不会被使用(因为new_c=0+1=1>0,所以跳过),所以dp[3]中只有无效状态?不对,dp[3][1]被设置为1,但1>0,所以不会用来更新dp[2](因为更新dp[2]时,new_c=0+1=1>0,所以跳过)。
在2处处理3:
组合:枚举c1=1,但new_c=0+1=1>0,跳过。
更新dp[2]:c1=1,new_c=1>0,跳过。
所以,dp[2]保持dp[2][0]=1。
然后,在2处,不分叉路径ans=max(1,1)=1。
然后,回到1,处理2:
组合:枚举2的状态:c1=0: new_c=0+0=0, new_len=1+1=2.
up_bound = 0 - 0 + 0 = 0? 不对,k=0,所以up_bound=0-0+0=0.
max_dp[0]=1(因为1的dp[1][0]=1),candidate=2+1-1=2,ans=max(1,2)=2.
更新dp[1]:c1=0: new_c=0, new_len=2,dp[1][0]=max(1,2)=2.
重新计算max_dp:max_dp[0]=2.
然后,不分叉路径:ans=max(2,2)=2.
所以,输出2,正确。
因此,代码正确。
最后,我们提交。
注意:题目中n<=1000,k<=1000,所以数组大小1000*1000=1e6,空间足够。
我们使用递归DFS,深度最大1000,不会栈溢出。
所以,最终代码:</think>### 题目分析
题目要求在一棵节点有黑白颜色的树上,找到一条简单路径(不重复节点),使得路径上黑色节点数量不超过k,并且路径总节点数尽可能多。解决方案采用树形动态规划,结合前缀最大值优化,高效处理子树合并和路径拼接问题。
### 算法思路
1. **树形动态规划**:为每个节点维护一个DP数组 `dp[u][c]`,表示从节点u出发向下延伸的路径中,恰好包含c个黑色节点的最大路径长度(节点数)。
2. **子树合并优化**:使用前缀最大值数组 `max_dp` 快速查询满足条件的最大路径长度,避免O(k²)的枚举。
3. **分叉路径处理**:在节点u处,将来自不同子树的两条路径拼接,形成经过u的分叉路径,并更新答案。
4. **边界处理**:考虑k=0或全黑节点等特殊情况,确保路径至少包含一个节点(若存在合法路径)。
### 代码实现
```cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>
using namespace std;
const int INF = -100000000;
int ans = 0;
void dfs(int u, int parent, vector<vector<int>>& graph, vector<int>& color, vector<vector<int>>& dp, int k) {
for (int i = 0; i <= k; i++) {
dp[u][i] = INF;
}
if (color[u] <= k) {
dp[u][color[u]] = 1;
}
if (color[u] <= k) {
ans = max(ans, 1);
}
vector<int> max_dp(k+1, INF);
if (k >= 0) {
max_dp[0] = dp[u][0];
for (int i = 1; i <= k; i++) {
if (max_dp[i-1] > dp[u][i]) max_dp[i] = max_dp[i-1];
else max_dp[i] = dp[u][i];
}
}
for (int v : graph[u]) {
if (v == parent) continue;
dfs(v, u, graph, color, dp, k);
for (int c1 = 0; c1 <= k; c1++) {
if (dp[v][c1] == INF) continue;
int new_c = color[u] + c1;
if (new_c > k) continue;
int new_len = 1 + dp[v][c1];
int up_bound = k - new_c + color[u];
if (up_bound < 0) continue;
if (up_bound > k) up_bound = k;
if (max_dp[up_bound] == INF) continue;
int total_len = new_len + max_dp[up_bound] - 1;
if (total_len > ans) ans = total_len;
}
for (int c1 = 0; c1 <= k; c1++) {
if (dp[v][c1] == INF) continue;
int new_c = color[u] + c1;
if (new_c > k) continue;
int new_len = 1 + dp[v][c1];
if (new_len > dp[u][new_c]) {
dp[u][new_c] = new_len;
}
}
max_dp[0] = dp[u][0];
for (int i = 1; i <= k; i++) {
if (max_dp[i-1] > dp[u][i]) max_dp[i] = max_dp[i-1];
else max_dp[i] = dp[u][i];
}
}
for (int i = 0; i <= k; i++) {
if (dp[u][i] > ans) ans = dp[u][i];
}
}
int main() {
int n, k;
cin >> n >> k;
vector<int> color(n+1);
for (int i = 1; i <= n; i++) {
cin >> color[i];
}
vector<vector<int>> graph(n+1);
for (int i = 0; i < n-1; i++) {
int u, v;
cin >> u >> v;
graph[u].push_back(v);
graph[v].push_back(u);
}
vector<vector<int>> dp(n+1, vector<int>(k+1, INF));
ans = 0;
dfs(1, -1, graph, color, dp, k);
cout << ans << endl;
return 0;
}
```
### 代码解释
1. **初始化**:DP数组初始化为负无穷(`INF`),表示无效状态。根节点设置初始状态(包含自身)。
2. **前缀最大值数组**:`max_dp[i]`存储黑色节点数不超过`i`时的最大路径长度,用于快速查询。
3. **子树处理**:
- **组合路径**:枚举当前子树状态,与之前子树状态组合形成分叉路径,更新答案。
- **更新DP状态**:用当前子树状态更新节点u的DP值,保持最长路径。
- **更新前缀最大值**:每次更新DP后重新计算前缀最大值数组。
4. **答案更新**:在子树处理过程中,不断更新全局答案`ans`,包含不分叉和分叉路径的最大值。
5. **边界处理**:通过`color[u] <= k`检查确保状态有效,避免越界。
该算法时间复杂度为O(nk),空间复杂度为O(nk),适用于题目约束条件。