算法随笔——高级树形DP
换根DP学习博客
https://www.luogu.com.cn/article/wdk0q56f
一个粗暴的模板。
void dfs(int x,int fa)
{
for (auto y : v[x])
{
if (y == fa) continue;
dfs(y,x);
}
}
树上背包
指在树上做类似背包的问题,是树形
P2014 [CTSC1997] 选课
P2014
题意:在树上选出
做法
设
可以发现转移过程中可以枚举子树选
则有转移
注意这里的
与普通的背包类似。
CODE
// Problem: P2014 [CTSC1997] 选课
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P2014
// Memory Limit: 125 MB
// Time Limit: 1000 ms
// Author: Eason
// Date:2024-09-23 18:40:54
//
// Powered by CP Editor (https://cpeditor.org)
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define INF 0x3f3f3f3f
#define re register
#define PII pair<int,int>
#define rep(k,a,b) for (int k = a;k <= b;k++)
#define mem memset
#define rd read
int read()
{
int f=1,k=0;char c = getchar();
while(c <'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
while(c>='0'&&c<='9')k=(k<<1)+(k<<3)+(c^48),c=getchar();
return k*f;
}
const int N = 2e3+5;
int n,m,val[N];
int f[N][N];
int siz[N];
vector<int> v[N];
int tmp[N*2];
void dfs(int x)
{
f[x][1] = val[x]; //初始设置
siz[x] = 1;
for (auto y : v[x])
{
dfs(y);
rep(i,1,siz[x] + siz[y]) tmp[i] = -INF;
//因为根必选 ,因此 i 从 1 开始枚举
for (int i = 1;i <= siz[x];i++) //枚举到当前已合并完的树的siz
for (int j = 0;j <= siz[y];j++)
tmp[i+j] = max(tmp[i+j],f[x][i]+f[y][j]);
rep(i,1,siz[x] + siz[y]) f[x][i] = tmp[i]; //将tmp复制回f
siz[x] += siz[y]; //记录当前的树size
}
}
int main()
{
cin >> n >> m;
rep(i,1,n)
{
int x = rd();
val[i ]= rd();
v[x].push_back(i);
}
dfs(0);
cout << f[0][m+1] << endl;
return 0;
}
关于时间复杂度
这样似乎是
简单证明一下:
当两个背包合并时,代价为
重要结论
做树上背包时:
- 如果合并子树的代价为子树大小之积,时间复杂度
。 - 如果合并子树的代价为
,则时间复杂度为 。
P1273
https://www.luogu.com.cn/problem/P1273
简单题意
叶子节点有权值,每条边有花费,问最多连接多少个叶子节点到根,使得权值总和大于花费总和。
做法
设
答案就为 最大的 j 使得
考虑转移
这是一个分组背包,每个子节点都是一个组,通过背包的经验,可以将 i 这一维滚动掉。
于是就没了。
注意枚举 k 时不能超过 j,也不能超过当前子节点的 size,这一细节可以优化成
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define INF 0x3f3f3f3f
#define re register
#define PII pair<int,int>
int read()
{
int f=1,k=0;char c = getchar();
while(c <'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
while(c>='0'&&c<='9')k=(k<<1)+(k<<3)+(c^48),c=getchar();
return k*f;
}
const int N = 4005;
int n,m;
vector<PII> v[N];
int c[N];
//dp[i][x][j] 表示在第 x 个节点,使用前 i 个子节点的子树,选j个最大能获得的钱数。
int dp[N][N];
int siz[N]; //siz表示子树下有多少叶子
void dfs(int x)
{
if (x >= n-m+1)
{ //判断为叶子节点
siz[x] = 1;
dp[x][1] = c[x]; //选一个为自己
return;
}
for (int i = 0;i < v[x].size();i++) //根据分组背包,先枚举组别
{
auto [y,w] = v[x][i];
dfs(y);
siz[x] += siz[y];
//为了滚动,倒序枚举
for (int j = siz[x];j >= 0;j--) //前 i 个j上界为当前的size
{
for (int k = 0;k <= siz[y];k++) //枚举决策
{
if (k > j) break;
dp[x][j] = max(dp[x][j],dp[x][j-k] + dp[y][k] - w);
}
}
}
}
int main()
{
memset(dp,-INF,sizeof dp);
cin >> n >> m;
for (int i = 1;i <= n-m;i++)
{
int k = read();
for (int j = 1;j <= k;j++)
{
int x = read(),c= read();
v[i].push_back({x,c});
}
dp[i][0] = 0; //初始化,选0个代价为0
}
for (int i = 1;i <= m;i++) c[i+n-m] = read();
dfs(1);
for (int k = m;k >= 0;k--)
{
if (dp[1][k] >= 0)
{
cout << k << endl;
return 0;
}
}
return 0;
}
P1272 重建道路
题意:求分离出一个大小为
设
乍一看不太好
相当于一个孤点。则初值设为
又因为
初值设完后,
取最小值即可。
比较板子的一题。
// Problem: P1272 重建道路
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P1272
// Memory Limit: 125 MB
// Time Limit: 1000 ms
// Author: Eason
// Date:2024-09-23 21:50:57
//
// Powered by CP Editor (https://cpeditor.org)
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define INF 0x3f3f3f3f
#define re register
#define PII pair<int,int>
#define rep(k,a,b) for (int k = a;k <= b;k++)
#define mem memset
#define rd read
int read()
{
int f=1,k=0;char c = getchar();
while(c <'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
while(c>='0'&&c<='9')k=(k<<1)+(k<<3)+(c^48),c=getchar();
return k*f;
}
const int N = 155;
int n,p;
vector<int> v[N];
int f[N][N];
int siz[N];
int tmp[N*2];
void dfs(int x)
{
f[x][0] = 1;
f[x][1] = 0;
siz[x] = 1;
for (auto y : v[x])
{
dfs(y);
for (int i = 1;i <= siz[x] + siz[y];i++) tmp[i] = INF;
for (int i = 1;i <= siz[x];i++)
for (int j = 0;j <= siz[y];j++)
tmp[i+j] = min(tmp[i+j],f[x][i] + f[y][j]);
for (int i = 1;i <= siz[x] + siz[y];i++) f[x][i] = tmp[i];
siz[x] += siz[y];
}
}
int main()
{
cin >> n >> p;
rep(i,1,n-1)
{
int x = rd(),y = rd();
v[x].push_back(y);
}
memset(f,INF,sizeof f);
dfs(1);
int ans = INF;
rep(i,1,n) ans = min(ans,f[i][p] + (i!=1));
cout << ans << endl;
return 0;
}
P4516 [JSOI2018] 潜入行动
树形DP无敌好题。
=P2014 + P2279
需要较为细心的分析和讨论。
题目分析
题意:给定一棵树,选择一个点会给与其相连的所有点染色(不包括自己),问选择 个点使得所有点都染上色的方案数
做法
设 表示:
考虑以 为根的子树,选择 个点, 是否被染色, 是否选择该点。
先考虑比较简单的两种情况,该点被其他点染色时,其儿子可选可不选,直接做树上背包转移即可。
//初始值
f[x][0][1][0] = 1;
f[x][1][1][1] = 1;
// 被染色且选
rep(i,0,k) tmp[i] = 0;
for (int i = 1;i <= min(siz[x],k);i++)
for (int j = 0;j <= min(siz[y],k) && (i+j<=k);j++)
(tmp[i+j] += 1ll*f[x][i][1][1] * (f[y][j][1][1]+f[y][j][1][0])%mod)%=mod;
rep(i,0,k) f[x][i][1][1] = tmp[i];
// 被染色且不选
rep(i,0,k) tmp[i] = 0;
for (int i = 0;i <= min(siz[x],k);i++)
for (int j = 0;j <= min(siz[y],k) && (i+j<=k);j++)
(tmp[i+j] += 1ll*f[x][i][1][0] * (f[y][j][0][1]+f[y][j][0][0])%mod)%=mod;
rep(i,0,k) f[x][i][1][0] = tmp[i];
然后是
还是用合并子树的思路,最开始时可以看成只有
f[x][1][0][1] = 0;
f[x][0][0][0] = 0;
考虑合并子树的过程,假设考虑到
-
不选,则转移为 : -
选,则此时 相当于被染色了,于是转移有:
Code
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define INF 0x3f3f3f3f
#define re register
#define PII pair<int,int>
#define rep(k,a,b) for (int k = a;k <= b;k++)
#define mem memset
#define rd read
int read()
{
int f=1,k=0;char c = getchar();
while(c <'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
while(c>='0'&&c<='9')k=(k<<1)+(k<<3)+(c^48),c=getchar();
return k*f;
}
const int N = 1e5+5,M = 105,mod = 1e9 +7;
int n,k;
vector<int> v[N];
int f[N][M][2][2];
int siz[N];
int tmp[N*2];
void dfs(int x,int fa)
{
siz[x] = 1;
f[x][0][0][0] = 0;
f[x][0][1][0] = 1;
f[x][1][0][1] = 0;
f[x][1][1][1] = 1;
for (auto y : v[x])
{
if (y == fa) continue;
dfs(y,x);
// 没被染色且不选
rep(i,0,k) tmp[i] = 0;
for (int i = 0;i <= min(siz[x],k);i++)
for (int j = 0;j <= min(siz[y],k) && (i+j<=k);j++)
{
(tmp[i+j] += 1ll*f[x][i][0][0] * f[y][j][0][0]%mod) %= mod;
if (j >= 1)
(tmp[i+j] += (1ll*f[y][j][0][1] *f[x][i][1][0])%mod)%=mod;
}
rep(i,0,k) f[x][i][0][0] = tmp[i];
// 没被染色且选
rep(i,0,k) tmp[i] = 0;
for (int i = 1;i <= min(siz[x],k);i++)
for (int j = 0;j <= min(siz[y],k) && (i+j<=k);j++)
{
(tmp[i+j] += 1ll*f[x][i][0][1] * f[y][j][1][0]%mod)%=mod;
if (j >= 1)
(tmp[i+j] += 1ll*f[y][j][1][1] *f[x][i][1][1]%mod)%=mod;
}
rep(i,0,k) f[x][i][0][1] = tmp[i];
// 被染色且不选
rep(i,0,k) tmp[i] = 0;
for (int i = 0;i <= min(siz[x],k);i++)
for (int j = 0;j <= min(siz[y],k) && (i+j<=k);j++)
(tmp[i+j] += 1ll*f[x][i][1][0] * (f[y][j][0][1]+f[y][j][0][0])%mod)%=mod;
rep(i,0,k) f[x][i][1][0] = tmp[i];
// 被染色且选
rep(i,0,k) tmp[i] = 0;
for (int i = 1;i <= min(siz[x],k);i++)
for (int j = 0;j <= min(siz[y],k) && (i+j<=k);j++)
(tmp[i+j] += 1ll*f[x][i][1][1] * (f[y][j][1][1]+f[y][j][1][0])%mod)%=mod;
rep(i,0,k) f[x][i][1][1] = tmp[i];
siz[x] += siz[y];
}
//print(x);
}
int main()
{
cin >> n >> k;
rep(i,1,n-1)
{
int x = rd(),y = rd();
v[x].push_back(y);
v[y].push_back(x);
}
dfs(1,0);
cout << (f[1][k][0][1] + f[1][k][0][0])%mod <<endl;
return 0;
}
注意这里
还有就是应该先处理
总结
树形背包是一种将树形DP 与经典背包问题结合起来的问题,做这种题的时候一定要熟悉合并子树的思路,即每次考虑一棵子树的过程可以看出是合并了一棵子树的过程。有利于简化问题,理清思路。
换根dp
又称二次扫描,是树形dp的一种。
特点
一般比较方便求出以一个点为根的答案,但题目要求求出以所有点为根的每个答案,暴力求是
通常用两次 dfs,第一次求出以 1 为根的答案并预处理一些方便转移的东西,第二次换根,求出每个点为根的答案。
Tree Painting
简明题意
做法
可以求出以 1 为根的答案,考虑从 以 fa 为根转移到 x的贡献。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define INF 0x3f3f3f3f
#define re register
#define PII pair<int,int>
int read()
{
int f=1,k=0;char c = getchar();
while(c <'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
while(c>='0'&&c<='9')k=(k<<1)+(k<<3)+(c^48),c=getchar();
return k*f;
}
const int N = 2e5+5;
int n;
vector<int> v[N];
int siz[N];
ll ans[N];
void dfs1(int x,int fa)
{
siz[x] = 1;
for (auto y : v[x])
{
if (y != fa)
{
dfs1(y,x);
siz[x] += siz[y];
}
}
}
void dfs2(int x,int fa)
{
if (x != 1)
ans[x] = ans[fa] + (n-siz[x]) - siz[x];
for (auto y : v[x])
{
if (y == fa) continue;
dfs2(y,x);
}
}
int main()
{
cin >> n;
for (int i = 1;i <= n-1;i++)
{
int x = read(),y = read();
v[x].push_back(y);
v[y].push_back(x);
}
dfs1(1, 0);
for (int i = 1;i <= n;i++) ans[1] += siz[i];
dfs2(1,0);
ll maxn = 0;
for (int i = 1;i <= n;i++) maxn = max(maxn,ans[i]);
cout << maxn << endl;
return 0;
}
CF771C
一道换根 dp 好题。
题意为求出
维护一下 x 到子树的点距离模 k 为 c的数量和x 到整棵树树的点距离模 k 为 c的数量。
加上一些容斥便可以完成转移。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define INF 0x3f3f3f3f
#define re register
#define int ll
#define PII pair<int,int>
int read()
{
int f=1,k=0;char c = getchar();
while(c <'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
while(c>='0'&&c<='9')k=(k<<1)+(k<<3)+(c^48),c=getchar();
return k*f;
}
const int N = 2e5+5;
int n;
vector<int> v[N];
int k;
int cnt1[N][15],cnt2[N][15];
int ans[N];
int dep[N];
void dfs1(int x,int fa)
{
dep[x] = dep[fa] + 1;
cnt1[x][0]++;
for (auto y : v[x])
{
if (y == fa) continue;
dfs1(y,x);
for (int i = 0;i < k;i++) cnt1[x][i] += cnt1[y][(i-1+k)%k];
}
}
void dfs2(int x,int fa)
{
if (x != 1)
{
ans[x] = ans[fa] -cnt1[x][0] + cnt2[fa][0] - cnt1[x][k-1];
// cout << x << ' ' << ans[x] << ' ' << cnt1[x][0] << ' ' << cnt2[fa][0] << ' ' << cnt1[x][k-1] << endl;
for (int i = 0;i < k;i++) cnt2[x][i] = cnt2[fa][(i-1+k)%k] - cnt1[x][(i-2+2*k)%k] + cnt1[x][i];
}
//cout << x << endl;
for (auto y : v[x])
{
if (y == fa) continue;
dfs2(y,x);
}
}
signed main()
{
cin >> n >> k;
for (int i = 1;i <= n-1;i++)
{
int x = read(),y = read();
v[x].push_back(y);
v[y].push_back(x);
}
dep[0 ] = -1;
dfs1(1,0);
for (int i = 1;i <= n;i++) ans[1] += (dep[i]+k-1)/k;
// cout << ans[1] << endl;
for (int i = 0;i < k;i++) cnt2[1][i] = cnt1[1][i];
dfs2(1,0);
int res = 0;
for (int i = 1;i <= n;i++) res += ans[i];
cout << res / 2 << endl;
return 0;
}
P6419 [COCI2014-2015#1] Kamp
题意:给定一棵树,边带权,有
我们可以先考虑到每个点最后回到
转移很简单:
其中
做完这个后,我们可以发现答案就是
于是一个粗暴的做法就出来了:第一次
其中线段树维护需要转化为
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define INF 0x3f3f3f3f
#define re register
#define int ll
#define PII pair<int,int>
#define rep(k,a,b) for (int k = a;k <= b;k++)
#define mem memset
#define rd read
int read()
{
int f=1,k=0;char c = getchar();
while(c <'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
while(c>='0'&&c<='9')k=(k<<1)+(k<<3)+(c^48),c=getchar();
return k*f;
}
const int N = 5e5+5;
int n,k;
vector<PII> v[N];
int pos[N];
int siz[N];
int g[N];
bool fk[N];
int dep[N];
struct node
{
int mx,tag;
}t[N<<2];
void stf(int k,int v)
{
t[k].mx += v;
t[k].tag += v;
}
void pushdown(int k)
{
if (t[k].tag)
{
stf(k<<1,t[k].tag);
stf(k<<1|1,t[k].tag);
t[k].tag = 0;
}
}
void modify(int k,int l,int r ,int x,int y,int v)
{
if (x > r || y <l) return;
if (x <=l && r <= y)
{
stf(k,v);
return;
}
int mid =l +r >> 1;
pushdown(k);
modify(k<<1,l,mid,x,y,v);
modify(k<<1|1,mid+1,r,x,y,v);
t[k].mx = max(t[k<<1].mx,t[k<<1|1].mx);
}
int dfn[N],id[N],tot;
int sz[N];
void dfs1(int x,int fa,int c)
{
if (fk[x]) siz[x] = 1;
dep[x] = dep[fa] + c;
dfn[x] = ++tot;
id[tot] = x;
sz[x] = 1;
for (auto [y,c] : v[x])
{
if (y == fa) continue;
dfs1(y,x,c);
siz[x] += siz[y];
if (siz[y])
g[x] += c * 2 + g[y];
sz[x] += sz[y];
}
}
int ans[N];
void dfs2(int x,int fa,int len)
{
if(x!=1)
{
modify(1,1,n,dfn[x],dfn[x]+sz[x]-1,-2*len);
modify(1,1,n,1,n,len);
if (siz[x] > 0)
{
if(k-siz[x] == 0) g[x] = g[fa] - 2 * len;
else g[x] = g[fa];
}
else g[x] = 2 * len +g[fa];
ans[x] = g[x] - t[1].mx;
}
for (auto [y,c] : v[x])
{
if (y == fa) continue;
dfs2(y,x,c);
}
modify(1,1,n,dfn[x],dfn[x]+sz[x]-1,2*len);
modify(1,1,n,1,n,-len);
}
signed main()
{
cin >> n >> k;
rep(i,1,n-1)
{
int x = rd(),y = rd(),z = rd();
v[x].push_back({y,z});
v[y].push_back({x,z});
}
rep(i,1,k)
pos[i] = rd(),fk[pos[i]] = 1;
dep[0] = 0;
dfs1(1,0,0);
modify(1,1,n,1,n,-INF);
rep(i,1,k) modify(1,1,n,dfn[pos[i]],dfn[pos[i]],INF + dep[pos[i]]);
ans[1] = g[1] - t[1].mx;
dfs2(1,0,0);
rep(i,1,n) cout << ans[i] << endl;
return 0;
}
P2279 [HNOI2003] 消防局的设立
一道较复杂的树形dp。
设出
然后转移一下。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define INF 0x3f3f3f3f
#define re register
#define int ll
#define PII pair<int,int>
int read()
{
int f=1,k=0;char c = getchar();
while(c <'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
while(c>='0'&&c<='9')k=(k<<1)+(k<<3)+(c^48),c=getchar();
return k*f;
}
const int N = 1005;
vector<int> v[N];
int f[N][5];
void dfs(int x,int fa)
{
if (x != 1 && v[x].size() == 1)
{
f[x][0] = 1;
f[x][1] = INF;
f[x][2] = INF;
f[x][3] = 0;
f[x][4] = 0;
return;
}
f[x][2] = 0;
f[x][1] = 0;
f[x][0] = 1;
f[x][3] = 0;
f[x][4] = 0;
for (auto y : v[x])
{
if (y == fa) continue;
dfs(y,x);
int minn = INF;
for (int i = 0;i < 4;i++) minn = min(f[y][i],minn);
f[x][0] += minn;
minn = INF;
for (int i = 0;i < 5;i++) if (i != 3) minn = min(f[y][i],minn);
f[x][3] += minn;
minn = INF;
for (int i = 0;i< 3;i++) minn = min(minn,f[y][i]);
f[x][4] += minn;
f[x][1] += min(f[y][0],min(min(f[y][1],f[y][2]),f[y][4]));
f[x][2] += min(f[y][1],f[y][2]);
}
int sum1 = f[x][1];
int sum2 = f[x][2];
f[x][1] = INF,f[x][2] = INF;
for (auto y : v[x])
{
if (y == fa) continue;
f[x][1] = min(f[x][1],sum1-min(f[y][0],min(min(f[y][1],f[y][2]),f[y][4])) + f[y][0]);
if (!(y != 1 && v[y].size() == 1))
f[x][2] = min(f[x][2],sum2-min(f[y][1],f[y][2]) + f[y][1]);
}
}
signed main()
{
int n;
cin >> n;
for (int i = 2;i <= n;i++)
{
int x = read();
v[i].push_back(x);
v[x].push_back(i);
}
memset(f,INF,sizeof f);
dfs(1,0);
cout << min(min(f[1][0],f[1][1]),f[1][2]) << endl;
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!