树的剖分:很厉害的性质题,代码也很好写。运用到了奇偶性拼凑答案的 trick。
观察
首先发现一个很重要的条件:一个点的点权只可能是 \(0,1,2\)。
这个条件开始我们可能无法用上,于是先想最后的结果应该是怎样的。
显然,我们最后取出的方案一定可以被描述为取下以某个节点为根的子树的一部分。同时,一个深度更大的点,如果能取出这样的一部分,那么先取掉这一部分一定是最优的。为啥呢,这一点和今年那道编辑字符串很像,因为如果我不取这个部分,以成全祖先取自己的部分的话,最后的结果还是 \(1\)。因此取这个部分一定不劣。
因此我们就得到了一个贪心的策略:从深度大的往深度小的选,能选则选,这样一定能保证答案最优。
那么怎么计算当前节点能不能恰好取到 \(k\) 呢?这时候我们再来看“一个点的点权只可能是 \(0,1,2\)”这个条件,很容易发现这个很像加工零件那道题分奇偶性讨论的 trick。具体而言,就是对于一个大小大于等于 \(2\) 的树,我们一定可以取下其中的某些叶子,使得整棵树权值的奇偶性不变。理由也很简单,首先 \(0\) 的叶子可以全部先摘掉,然后此时要么有 \(2\) 的点,直接摘下 \(2\) 的点即可。要么就是 \(0,2\) 都没有,只有 \(1\),那么此时叶子结点至少有 \(2\) 个(因为树上至少有一条链),这时我们可以取下这两个 \(1\) 的节点,就能让奇偶性不变的同时减少了。
因此,我们可以算出以 \(i\) 为根节点的子树中,让连通块权值为奇数时连通块的最大权值、让连通块权值为偶数时连通块的最大权值,再判断 \(k\) 对应的奇偶性的最大权值是否大于等于 \(k\) 就能判断能不能取了。
实现上,我们可以定义 \(dp_{i,0}\) 表示连通块权值为偶数时的最大权值,\(dp_{i,1}\) 表示连通块权值为奇数时的最大权值,进行如下转移:
注意,代码实现时这里不能直接转移,因为先转移哪一个都会导致 dp 值被覆盖,应当先用临时变量记录下转移后 dp 的值,再给 dp 数组赋值。
同时注意取了连通块后将 \(dp_{u,0}=dp_{u,1}=-inf\),切不可赋值为 \(0\),因为此时奇数的 dp 值就不成立了。初始化时也是同理,必须注意。
时间复杂度 \(O(n)\)。
代码
#include <bits/stdc++.h>
#define fi first
#define se second
#define lc (p<<1)
#define rc ((p<<1)|1)
#define eb(x) emplace_back(x)
#define pb(x) push_back(x)
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ldb;
using pi=pair<int,int>;
int n,k,w[100005],ans=0,dp[100005][2];
vector<int>g[100005];
void dfs(int u,int fa)
{dp[u][w[u]&1]=w[u];dp[u][(w[u]&1)^1]=-0x3f3f3f3f;for(auto v:g[u]){if(v==fa)continue;dfs(v,u);int dp0=max(dp[u][0],max(dp[u][0]+dp[v][0],dp[u][1]+dp[v][1]));int dp1=max(dp[u][1],max(dp[u][0]+dp[v][1],dp[u][1]+dp[v][0]));dp[u][0]=dp0;dp[u][1]=dp1;}if(dp[u][k&1]>=k){ans++;dp[u][0]=dp[u][1]=-0x3f3f3f3f;}
}
int main()
{ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);cin>>n>>k;for(int i=1;i<=n;i++)cin>>w[i];for(int i=1;i<n;i++){int u,v;cin>>u>>v;g[u].push_back(v);g[v].push_back(u);}dfs(1,0);cout<<ans;return 0;
}