[笔记]树形 DP 刷题记录
P11294 [NOISG 2022 Qualification] Tree Cutting
Tag:树的直径,杂项。
删除一条边,会产生两棵树,此时最优就是将两树的直径端点连起来,答案为两树直径之和再\({}+1\)。
因此考虑枚举删除的边 \((u,v)\),不妨令 \(u\) 为父节点。我们需要知道:
- 子树 \(v\) 的直径。
- 原树删除子树 \(v\) 后的直径。
前者只需要按正常的树形 DP,记录每个节点能向下延伸的最长、次长链(\(mx[u][0/1]\))即可求得。
后者的答案相当于在下面三项中选两项:
- 删去子树 \(v\) 后,从 \(u\) 向下延伸的最长链。
- 删去子树 \(v\) 后,从 \(u\) 向下延伸的次长链。
- 从 \(u\) 向子树 \(u\) 外延伸的最长链。
第三项可以采用 \(2\) 次 DFS 的方法,一次从下往上,一次从上往下(即 P10962)。
前两项可以额外维护 \(u\) 能向下延伸的第 \(3\) 长链(\(mx[u][2]\))。
- 若 \(mx[v][0]+1=mx[u][0]\),则删除 \(v\) 后的最长链、次长链即为 \(mx[u][1],mx[u][2]\)。
- 若 \(mx[v][0]+1=mx[u][1]\),则删除 \(v\) 后的最长链、次长链即为 \(mx[u][0],mx[u][2]\)。
- 其他情况,删除 \(v\) 后的最长链、次长链即为 \(mx[u][0],mx[u][1]\)。
时间复杂度 \(O(n)\)。
点击查看代码
#include<bits/stdc++.h>
#define eb emplace_back
using namespace std;
const int N=3e5+5;
int n,mx[N][3],g[N],f[N],ans;//mx:子树内前k大的距离 g:子树内的直径 f:子树外最大的距离
inline void merge(int a[3],int w){
if(w>=a[0]) a[2]=a[1],a[1]=a[0],a[0]=w;
else if(w>=a[1]) a[2]=a[1],a[1]=w;
else if(w>=a[2]) a[2]=w;
}
vector<int> G[N];
inline void dfs1(int u,int fa){
for(int i:G[u]){
if(i^fa){
dfs1(i,u);
merge(mx[u],mx[i][0]+1);
g[u]=max(g[u],g[i]);
}
}
g[u]=max(g[u],mx[u][0]+mx[u][1]);
}
inline void dfs2(int u,int fa){
for(int i:G[u]){
if(i^fa){
if(mx[i][0]+1==mx[u][0]) f[i]=max(f[u],mx[u][1])+1;
else f[i]=max(f[u],mx[u][0])+1;
dfs2(i,u);
ans=max(ans,g[i]+1+(
mx[i][0]+1==mx[u][0]?mx[u][1]+max(f[u],mx[u][2]):
mx[i][0]+1==mx[u][1]?mx[u][0]+max(f[u],mx[u][2]):
mx[u][0]+max(f[u],mx[u][1])
));
}
}
}
signed main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>n;
for(int i=1,u,v;i<n;i++){
cin>>u>>v;
G[u].eb(v),G[v].eb(u);
}
dfs1(1,0),dfs2(1,0);
cout<<ans<<"\n";
return 0;
}
P3177 [HAOI2015] 树上染色
Tag:树上背包。
下文记 \(m\) 为黑点的数量。
直接计算全局贡献不好做,考虑只记录子树中的贡献。
具体来说,令 \(f[u][j]\) 为子树 \(u\) 中,将 \(j\) 个点染成黑色,子树内的最大答案。
转移时,用类似树上背包的方式,将已 \(u\) 和合并过的子节点视作一个泛化物品,则有转移:
\[f[u][j]\gets \max_k f[u][j-k]+f[i][k]+\color{#9900aa}w\times k\times (m-k)\color{none}+\color{#00aa99} w\times (siz_i-k)\times (n-m-(siz_i-k))
\]
时间复杂度 \(O(nk)\),注意上下界优化,否则会假掉。
点击查看代码
#include<bits/stdc++.h>
#define int long long
#define eb emplace_back
using namespace std;
const int N=2005,M=2005;
int n,m,siz[N],f[N][M];
struct Ed{int to,w;};
vector<Ed> G[N];
inline void dfs(int u,int fa){
siz[u]=1;
for(Ed i:G[u]){
int v=i.to,w=i.w;
if(v==fa) continue;
dfs(v,u);
for(int j=min(m,siz[u]+siz[v]);~j;j--){
for(int k=max(0ll,j-siz[u]);k<=min(siz[v],j);k++){
f[u][j]=max(f[u][j],f[u][j-k]+f[v][k]+(k*(m-k)+(siz[v]-k)*(n-m-siz[v]+k))*w);
}
}
siz[u]+=siz[v];
}
}
signed main(){
cin>>n>>m;
for(int i=1,u,v,w;i<n;i++){
cin>>u>>v>>w;
G[u].eb(Ed{v,w});
G[v].eb(Ed{u,w});
}
dfs(1,0);
cout<<f[1][m]<<"\n";
return 0;
}
CF815C Karen and Supermarket ~ Codeforces
Tag:树上背包。
Trick:互换维度。
考虑树上背包,但是 \(m\) 太大了。
一个常用的技巧是互换维度,令 \(f[u][j][0/1]\) 为子树 \(u\) 中恰好买 \(j\) 个东西,\(u\) 是/否使用优惠券,所需要的最小价值。
则有转移:
\[f[u][j][0]\gets\min_k f[u][j-k][0]+f[i][k][0]\\
f[u][j][1]\gets\min_k f[u][j-k][0]+\min(f[i][k][0],f[i][k][1])
\]
使用上下界优化,时间复杂度 \(O(n^2)\)。
点击查看代码
#include<bits/stdc++.h>
#define int long long
#define eb emplace_back
using namespace std;
const int N=5005;
int n,m,f[N][N][2],u[N],v[N],siz[N];
//f[u][j][k]:子树u中恰好买j个物品,u是/否使用优惠券,最小花费
vector<int> G[N];
inline void chmn(int &x,int y){x=min(x,y);}
inline void dfs(int u){
siz[u]=1;
for(int i:G[u]){
dfs(i);
for(int j=min(n,siz[u]+siz[i]);j;j--){
for(int k=max(0ll,j-siz[u]);k<=min(siz[i],j);k++){
chmn(f[u][j][1],f[u][j-k][1]+min(f[i][k][0],f[i][k][1]));
chmn(f[u][j][0],f[u][j-k][0]+f[i][k][0]);
}
}
siz[u]+=siz[i];
}
}
signed main(){
memset(f,0x3f,sizeof f);
cin>>n>>m;
for(int i=1,u,v,x;i<=n;i++){
cin>>u>>v;
f[i][0][0]=0;//不买不能使优惠券,所以f[i][0][1]=inf
f[i][1][0]=u,f[i][1][1]=u-v;
if(i>1) cin>>x,G[x].eb(i);
}
dfs(1);
for(int i=n;~i;i--)
if((f[1][i][0]<=m)||(f[1][i][1]<=m))
cout<<i<<"\n",exit(0);
return 0;
}
浙公网安备 33010602011771号