[树形DP]叶子的颜色

题目描述

给一棵m个结点的无根树,你可以选择一个度数大于1的结点作为根,然后给一些结点(根、内部结点和叶子均可)着以黑色或白色。你的着色方案应该保证根结点到每个叶子的简单路径上都至少包含一个有色结点(哪怕是这个叶子本身)。
  对于每个叶结点u,定义c[u]为从u到根结点的简单路径上第一个有色结点的颜色。给出每个c[u]的值,设计着色方案,使得着色结点的个数尽量少。

Input
  第一行包含两个正整数m, n,其中n是叶子的个数,m是结点总数。结点编号为1,2,…,m,其中编号1,2,… ,n是叶子。以下n行每行一个0或1的整数(0表示黑色,1表示白色),依次为c[1],c[2],…,c[n]。以下m-1行每行两个整数a,b(1<=a < b <= m),表示结点a和b 有边相连。

Output
  仅一个数,即着色结点数的最小值。

Sample Input
5 3
0
1
0
1 4
2 5
4 5
3 5

Sample Output
2

Data Constraint  
 1<=N,M<=100000

分析

根据画图容易推出:
1、哪个节点作为根都(除叶节点)不会改变答案值
2、可以作为根的点必定互相可以到达
那么我们随便找一个点开始DFS(如n+1)
容易设出规划数组:fi,j表示第i个节点染成j色的最少染点数量
根据题目,我们可以转换一下意思,即fi,j表示以第i个节点为根的子树染成j色以后,这棵子树要染色的最少点数
容易知道初始值应该为1(因为自己染了嘛)
然后如果是叶节点,为了满足本身的颜色限制,明显对于这个叶节点的fi,j,j如果和叶节点的颜色限制相反的话,值为∞
然后状态转移方程也很容易推:
f[u,j]+=min{f[v,j]-1,f[v,!j]
为什么-1呢?因为同色的话染了根就没必要染下面的节点,所以-1

#include <iostream>
#include <cstdio>
#define rep(i,a,b) for (i=a;i<=b;i++)
const int N=100001;
using namespace std;
int f[N][2];
int m,n,a[N];
struct Edge {
    int u,v,nx;
}g[2*N];
int cnt,list[N];

void Addedge(int u,int v) {
    g[++cnt].u=u;g[cnt].v=v;g[cnt].nx=list[u];list[u]=cnt;
    g[++cnt].u=v;g[cnt].v=u;g[cnt].nx=list[v];list[v]=cnt;
}

void Dfs(int x,int F) {
    int i;
    f[x][0]=f[x][1]=1;
    if (x<=n)
    f[x][!a[x]]=2147483647;
    for (i=list[x];i;i=g[i].nx)
    if (g[i].v!=F) {
        Dfs(g[i].v,x);
        f[x][0]+=min(f[g[i].v][0]-1,f[g[i].v][1]);
        f[x][1]+=min(f[g[i].v][1]-1,f[g[i].v][0]);
    }
}

int main() {
    int i;
    scanf("%d%d",&m,&n);
    rep(i,1,n)
    scanf("%d",&a[i]);
    rep(i,1,m-1) {
        int u,v;
        scanf("%d%d",&u,&v);
        Addedge(u,v);
    }
    Dfs(n+1,0);
    printf("%d",min(f[n+1][0],f[n+1][1]));
}
<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),适用于题目约束条件。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值