赛前训练 5 树形 dp
A
做树形 dp 时,尝试将题目转化为只考虑子树内.
对于这个题,因为起点到终点的路径总能拆成 起点 -> LCA -> 终点 的形式,所以我们考虑枚举 LCA 进行 dp.为了使汽油量最大,我们维护 \(dp_i\) 表示子树内跑到 \(i\) 的最大值,\(f_i\) 表示次大值.答案即为 \(\max\{dp_i+f_i+w_i\}\),转移是套路的.
实现
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
#include <stdlib.h>
#include <vector>
#include <queue>
#include <cmath>
#include <stack>
#include <map>
#include <set>
#define int long long
using namespace std;
const int N=3e5+5;
int n,ans;
struct EDGE{
int v,w;
};
vector<EDGE> G[N];
int w[N],dp[N],f[N];
void dfs(int cur,int fa){
for(auto i:G[cur]){
if(i.v==fa)
continue;
dfs(i.v,cur);
if(dp[i.v]-i.w+w[i.v]>dp[cur])
f[cur]=dp[cur],dp[cur]=dp[i.v]-i.w+w[i.v];
else if(dp[i.v]-i.w+w[i.v]>f[cur])
f[cur]=dp[i.v]-i.w+w[i.v];
}
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n;
for(int i=1;i<=n;i++)
cin>>w[i];
for(int i=1,u,v,w;i<n;i++){
cin>>u>>v>>w;
G[u].push_back({v,w});
G[v].push_back({u,w});
}
dfs(1,0);
int ans=-1e18;
for(int i=1;i<=n;i++)
ans=max(ans,dp[i]+f[i]+w[i]);
cout<<ans;
return 0;
}
B
这个题是讲过的,关于解法请查阅往期笔记.
错因:对于 dp 状态的定义太模糊.dp 题一定要在草稿纸上写出四要素.
C
两年前的这个时候打的月赛题,当时保龄了(现在估计也差不多).more and more vegetable,what should i do?
首先可以观察出来一些东西:
-
必然先炸后连,不然有可能把刚连上的炸掉.
-
由于是森林,最后一定需要 \(n-m-1\) 次操作来维持连通性.
有了上面两条结论,我们就只需要考虑炸的部分了.于是最优化问题很难不想到树形 dp.
分类讨论起手.不难发现,对于每个节点,只有三种情形:
-
自我毁灭,出边全炸.
-
保留一个孩子,自己作为链头/链尾.
-
保留两个孩子,自己作为链的中间点.
自然地,令 \(dp_{i,0/1/2}\),表示节点 \(i\) 全炸/留一个/留两个的最少操作次数.
答案是所有连通块的 \(\min(dp_{root,0},dp_{root,1},dp_{root,2})\) 之和.
转移同样分类讨论:
-
自我毁灭的情形,它必然还是得把那些炸掉的边连起来,炸的这一次也会付出 \(1\) 的代价,这部分是 \(deg_i+1\) 的;同时,它还需要考虑从子节点转移,子节点三种状态均可,只是如果子节点也是自我毁灭,那么它和父节点连的那条边就会重复算,需要减掉,这部分是 \(\min(dp_{son,0}-1,dp_{son,1},dp_{son,2})\) 的.两部分加起来就好.
-
留一个的情形,显然我们需要儿子们全都炸了,所以先加上 \(\sum dp_{son,0}\).但我们还得留一个,所以得扣除一个 \(dp_{son,0}\),再加上 \(dp_{son,1}\).那么选哪个儿子最好呢?把后面两项合并为 \(-(dp_{son,0}-dp_{son,1})\),显然减号后面这一坨取 \(\max\) 的时候最优,简单维护即可.
-
留两个的情形,从留一个出发,我们还得做一遍扣除再加上的操作,但不能和先前留的那一个重合,所以维护一个次大值就好.
于是转移方程就出来了:

然后这题就做完了.本题轻微卡常,注意不要开 LL.
实现
#include <bits/stdc++.h>
using namespace std;
const int N=2e6+5;
int n,m;
int dp[N][3];
bool dfn[N];
vector<int> G[N];
void dfs(int cur,int fa){
dfn[cur]=1;
int fir=0,sec=0;
for(int i:G[cur]){
if(dfn[i])
continue;
dfs(i,cur);
if(dp[i][0]-dp[i][1]>fir)
sec=fir,fir=dp[i][0]-dp[i][1];
else if(dp[i][0]-dp[i][1]>sec)
sec=dp[i][0]-dp[i][1];
dp[cur][0]+=min({dp[i][0]-1,dp[i][1],dp[i][2]});
dp[cur][1]+=dp[i][0];
}
dp[cur][0]+=G[cur].size()+1;
dp[cur][1]-=fir;
dp[cur][2]=dp[cur][1]-sec;
}
signed main(){
//freopen("traffic2.in","r",stdin);
//freopen("traffic2.out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n>>m;
for(int i=1,u,v;i<=m;i++){
cin>>u>>v;
G[u].push_back(v);
G[v].push_back(u);
}
int ans=0;
for(int i=1;i<=n;i++)
if(!dfn[i])
dfs(i,0),ans+=min({dp[i][0],dp[i][1],dp[i][2]});
cout<<ans+n-1-m;
return 0;
}
D
观察数据范围,容易设计 \(dp_{i,j}\) 表示以 \(i\) 为根的子树中,可达节点为 \(j\) 个时是否存在方案.答案 \(dp_{1,l_1}\),初始 \(dp_{i,1}=1\).
现在难点在于转移,我们需要讨论父子之间边的状态,可是真的需要吗?
注意到,题中所给出的 \(l_i\) 实际上是在暗示我们边的状态.当 \(l_u=l_v\) 时,\(u,v\) 要么双向、要么无连接;当 \(l_u>l_v\) 时,只能 \(v\) 连向 \(u\),反之 \(u\) 连向 \(v\).这样就可以分三种情况讨论出转移了.
-
\(l_u=l_v\)
若双向,则 \(dp_{u,j} \texttt{|=} \ dp_{u,j} \ \texttt{\&} \ dp_{v,k}\);若不连边,则必须 \(dp_{v,l_v}=1\).
-
\(l_u>l_v\)
不连边同上;若 \(v \to u\),则 \(dp_{u,j+l_v} \texttt{|=} \ dp_{v,l_v}\).
-
\(l_u<l_v\)
不连通同上;若 \(u \to v\),则必须 \(dp_{v,l_v-l_u}=1\).
实现
//
// P12017.cpp
//
//
// Created by _XOFqwq on 2025/10/18.
//
#include <cstdio>
#include <iomanip>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
#include <stdlib.h>
#include <vector>
#include <queue>
#include <cmath>
#include <stack>
#include <map>
#include <set>
#define int long long
using namespace std;
const int N=5e3+5;
int n;
int l[N],dp[N][N],f[N],siz[N];
vector<int> G[N];
void dfs(int cur,int fa){
siz[cur]=1;
for (int i:G[cur]) {
if (i==fa) {
continue;
}
dfs(i,cur);
if (l[cur]==l[i]) {
for (int j=0; j<=siz[cur]; j++) {
for (int k=0; k<=siz[i]; k++) {
f[j+k]|=(dp[cur][j]&dp[i][k]);
}
}
if (dp[i][l[i]]) {
for (int j=0; j<=siz[cur]; j++) {
f[j]|=dp[cur][j];
}
}
} else {
if (l[cur]>l[i]) {
if (!dp[i][l[i]]) {
cout<<"NO",exit(0);
} else {
for (int j=0; j<=siz[cur]; j++) {
f[j+l[i]]|=dp[cur][j];
f[j]|=dp[cur][j];
}
}
} else {
if (!dp[i][l[i]]&&!dp[i][l[i]-l[cur]]) {
cout<<"NO",exit(0);
} else {
for (int j=0; j<=siz[cur]; j++) {
f[j]|=dp[cur][j];
}
}
}
}
siz[cur]+=siz[i];
for (int j=0; j<=siz[cur]; j++) {
dp[cur][j]=f[j];
}
memset(f,0,sizeof f);
}
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n;
for (int i=1; i<=n; i++) {
cin>>l[i];
}
for (int i=1; i<=n; i++) {
dp[i][1]=1;
}
for (int i=1,u,v; i<n; i++) {
cin>>u>>v;
G[u].push_back(v);
G[v].push_back(u);
}
dfs(1,0);
cout<<(dp[1][l[1]]?"YES":"NO");
return 0;
}
总结
树形 dp:转化为子树内问题、分类讨论设状态以及转移.
两个技巧点. 以上.

浙公网安备 33010602011771号