树形 DP,即在树上进行的 DP。由于树固有的递归性质,树形 DP 一般都是递归进行的。
树形 DP 的一般过程。
某大学有 n n n 个职员,编号为 1 − N 1 - N 1−N。他们之间有从属关系,也就是说他们的关系就像一棵以校长为根的树,父结点就是子结点的直接上司。现在有个周年庆宴会,宴会每邀请来一个职员都会增加一定的快乐指数 a i a_i ai,但是呢,如果某个职员的直接上司来参加舞会了,那么这个职员就无论如何也不肯来参加舞会了。所以,请你编程计算,邀请哪些职员可以使快乐指数最大,求最大的快乐指数。
我们设 f ( i , 0 / 1 ) f(i,0/1) f(i,0/1) 代表以 i i i 为根的子树的最优解(第二维的值 0 0 0 代表 i i i 不参加舞会的情况, 1 1 1 代表 i i i 参加舞会的情况)。
对于每个状态,都存在两种决策(其中下面的 x x x 都是 i i i 的儿子):
根据两种状态写出dp递推式
F ( i , 1 ) = Σ F ( x , 0 ) + a i F(i,1)=\Sigma F(x,0)+a_i F(i,1)=ΣF(x,0)+ai
F ( i , 0 ) = Σ m a x ( F ( x , 1 ) , F ( x , 0 ) ) F(i,0)=\Sigma max(F(x,1),F(x,0)) F(i,0)=Σmax(F(x,1),F(x,0))
略
树上的背包问题,背包与树形dp结合
现在有 n n n 门课程,第 i i i 门课程的学分为 a i a_i ai,每门课程有零门或一门先修课,有先修课的课程需要先学完其先修课,才能学习该课程。
一位学生要学习 m m m 门课程,求其能获得的最多学分数。
每门课最多只有一门先选课的特点,与有根树中一个点最多只有一个父亲节点的特性类似。
因此可以想到根据这一性质建树,从而所有的课程组成了一个森林结构。
新增一门0学分的课程,作为无前提课程的前提课程,整个森林变为以0为根的树。
设 f ( u , i , j ) f(u,i,j) f(u,i,j) 表示以 u u u 号点为根的子树中,已经遍历了 u u u 号点的前 i i i 棵子树,选了 j j j 门课程的最大学分。
转移的过程结合了树形 DP 和 背包 DP 的特点,我们枚举 u u u 点的每个子结点 v v v,同时枚举以 v v v 为根的子树选了几门课程,将子树的结果合并到 u u u 上。
以 x x x 为根的子树大小为 s i z x siz_x sizx 可以记因此有转移方程:
F ( u , i , j ) = m a x ( F ( u , i − 1 , j − k ) + F ( v , S v , k ) ) F(u,i,j)=max(F(u,i-1,j-k)+F(v,S_v,k)) F(u,i,j)=max(F(u,i−1,j−k)+F(v,Sv,k))
第二维可以滚动数组优化掉,倒序枚举j。
复杂度为O(nm)
#include
#include
#include
using namespace std;
int f[305][305], s[305], n, m;
vector<int> e[305];
int dfs(int u) {
int p = 1;
f[u][1] = s[u];
for (auto v : e[u]) {
int siz = dfs(v);
for (int i = min(p, m + 1); i; i--)
for (int j = 1; j <= siz && i + j <= m + 1; j++)
f[u][i + j] = max(f[u][i + j], f[u][i] + f[v][j]);
p += siz;
}
return p;
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) {
int k;
scanf("%d%d", &k, &s[i]);
e[k].push_back(i);
}
dfs(0);
printf("%d", f[0][m + 1]);
return 0;
}
树形 DP 中的换根 DP 问题又被称为二次扫描,通常不会指定根结点,并且根结点的变化会对一些值,例如子结点深度和、点权和等产生影响。
不妨令 u 为当前结点,v 为当前结点的子结点。首先需要用 s i s_i si 来表示以 i 为根的子树中的结点个数,并且有 s u = 1 + Σ s v s_u=1+\Sigma s_v su=1+Σsv。显然需要一次 DFS 来计算所有的 S i S_i Si,这次的 DFS 就是预处理,我们得到了以某个结点为根时其子树中的结点总数。
令 f u f_u fu 为以 u u u 为根时,所有结点的深度之和。
f v ⬅ f u f_v⬅f_u fv⬅fu 可以体现换根,即以 u u u 为根转移到以 v v v 为根。显然在换根的转移过程中,以 v v v 为根或以 u u u 为根会导致其子树中的结点的深度产生改变。具体表现为:
由此地递推方程:
f v = f u + n − s v = f u + n − 2 × s v f_v=f_u+n-s_v=f_u+n-2 × s_v fv=fu+n−sv=fu+n−2×sv
在第二次 DFS 遍历整棵树并状态转移 f v = f u + n − 2 ∗ s v f_v=f_u+n-2*s_v fv=fu+n−2∗sv,那么就能求出以每个结点为根时的深度和了。
#include
using namespace std;
int head[1000010 << 1], tot;
long long n, size[1000010], dep[1000010];
long long f[1000010];
struct node {
int to, next;
} e[1000010 << 1];
void add(int u, int v) { // 建图
e[++tot] = node{v, head[u]};
head[u] = tot;
}
void dfs(int u, int fa) { // 预处理dfs
size[u] = 1;
dep[u] = dep[fa] + 1;
for (int i = head[u]; i; i = e[i].next) {
int v = e[i].to;
if (v != fa) {
dfs(v, u);
size[u] += size[v];
}
}
}
void get_ans(int u, int fa) { // 第二次dfs换根dp
for (int i = head[u]; i; i = e[i].next) {
int v = e[i].to;
if (v != fa) {
f[v] = f[u] - size[v] * 2 + n;
get_ans(v, u);
}
}
}
int main() {
scanf("%lld", &n);
int u, v;
for (int i = 1; i <= n - 1; i++) {
scanf("%d %d", &u, &v);
add(u, v);
add(v, u);
}
dfs(1, 1);
for (int i = 1; i <= n; i++) f[1] += dep[i];
get_ans(1, 1);
long long int ans = -1;
int id;
for (int i = 1; i <= n; i++) {
if (f[i] > ans) {
ans = f[i];
id = i;
}
}
printf("%d\n", id);
}
给定一棵树,
树中每条边都有一个权值,
树中两点之间的距离定义为连接两点的路径边权之和。
树中最远的两个节点之间的距离被称为树的直径,
连接这两个点的路径被称为树的最长链。
————————————————————————————————
树的直径求法:双dfs或树形dp
考虑贪心策略,
对于树上的一个随机的点 W W W ,
我们找到离他最远的 P P P ,
找到离 P P P 距离最远的点 Q Q Q ,
P Q PQ PQ 的距离即为我们要求的直径。
如图,假设五号点为 W W W ,
找到离他距离最远的点 4 ( P ) 4(P) 4(P),
再找到距离 P P P 最远的点 6 ( Q ) 6(Q) 6(Q) ,
P Q PQ PQ 的距离即为直径。
#include
#define N 200005
using namespace std;
int n,m;
struct edge{
int to,nxt,w;
}e[N];
int tot;
int ans,pos;
int head[N],dis[N];
void add(int u,int v,int w){
e[++tot]={v,head[u],w},head[u]=tot;
}
void dfs(int me,int dad){
if(ans<dis[me])ans=dis[me],pos=me;
for(int i=head[me];i;i=e[i].nxt){
int son=e[i].to;
if(son==dad)continue;
dis[son]=dis[son]+e[i].w;
dfs(son,me);
}
}
int main(){
cin>>n;
for(int i=1;i<n;i++){
int u,v;
cin>>u>>v;
add(u,v,1),add(v,u,1);
}
dis[1]=0;
dfs(1,0);//第一次dfs,随便找一个点。
ans=0;dis[pos]=0,dfs(pos,0);//第二次dfs
cout<<ans<<endl;
}
考虑 f [ i ] f[i] f[i] 以 i i i 为根的子树中和从 i i i 出发的最长长度
考虑以 u u u 为根的子树
f [ u ] = m a x ( f [ u ] , f [ v ] + e [ i ] . w ) f[u] = max(f[u],f[v]+e[i].w) f[u]=max(f[u],f[v]+e[i].w)
最长长度就是两条链的和。
或者说,我们原来找到了一条 f [ u ] f[u] f[u] 的链
现在我们新找到了一条由 v v v 节点继承的链
这两条链长度之和的最大值即为 a n s ans ans
所以
a n s = m a x ( a n s , f [ u ] + f [ v ] + e [ i ] . w ) ans=max(ans,f[u]+f[v]+e[i].w) ans=max(ans,f[u]+f[v]+e[i].w)
PS: 要写在 f [ u ] f[u] f[u] 的转移之前
void dfs(int me,int dad)
{
f[me]=0;
for(int i=head[me];~i;i=e[i].nxt)
{
int son=e[i].to;
if(son==dad)continue;
dfs(son,me);
ans=max(ans,f[me]+f[son]+e[i].w);
f[me]=max(f[me],f[son]+e[i].w);
}
}
因为树形 d p dp dp 的做法不需要依赖于 x + y > x ( y ∈ R + ) x+y>x(y \in R^+) x+y>x(y∈R+) 的性质,所以边权可以为负。
考虑一个点,以它为根的树中,最大的子树节点数最少,我们把这个点称为树的重心。
例:下图中重心为 1 1 1 和 2 2 2 。
求解树的重心的时候,我们通常会采用树形 d p dp dp
我们用 s [ i ] s[i] s[i] 代表以 i i i 为根的子树节点数
f [ i ] f[i] f[i] 代表以 i i i 为根的子树中最大的子树节点个数
显然, f [ u ] = m a x ( f [ u ] , s [ v ] ) f[u]=max(f[u],s[v]) f[u]=max(f[u],s[v])
但是我们求重心的时候,是以 u u u 为根。
2 2 2 号节点的父亲变为儿子
所以最后统计 f [ u ] f[u] f[u] 的时候,还要记得统计 n − s [ u ] n-s[u] n−s[u] (即以原来父亲为根的子树的节点数)
void dfs(int me,int dad)
{
s[me]=1,f[me]=0;
for(int i=head[me];i;i=e[i].nxt)
{
int son=e[i].to;
if(son==dad)continue;
dfs(son,dad);
s[me]+=s[son];
f[me]=max(f[me],s[son]);
}
f[me]=max(f[me],n-s[me]);
}
#include
using namespace std;
#define rd(x) cin>>x
#define ll long long
#define pb push_back
#define print(x) cout<<x<<endl;
const int N = 3e5 + 7;
int n, rt, s[N], g[N], u, v, z[N];
vector<int> e[N];
ll ans, c1[N], c2[N];
inline void add(ll *c, int x, int k) {
++x;
while (x <= n + 1) c[x] += k, x += x & -x;
}
inline ll ask(ll *c, int x) {
++x;
ll k = 0;
while (x) k += c[x], x -= x & -x;
return k;
}
void dfs1(int x, int f) {
s[x] = 1, g[x] = 0;
bool fg = 1;
for (int i = 0; i < e[x].size(); i++) {
int y = e[x][i];
if (y == f) continue;
dfs1(y, x);
s[x] += s[y];
g[x] = max(g[x], s[y]);
if (s[y] > (n >> 1)) fg = 0;
}
if (n - s[x] > (n >> 1)) fg = 0;
if (fg) rt = x;
}
void dfs2(int x, int f) {
add(c1, s[f], -1);
add(c1, n - s[x], 1);
if (x ^ rt) {
ans += x * ask(c1, n - 2 * g[x]);
ans -= x * ask(c1, n - 2 * s[x] - 1);
ans += x * ask(c2, n - 2 * g[x]);
ans -= x * ask(c2, n - 2 * s[x] - 1);
if (!z[x] && z[f]) z[x] = 1;
ans += rt * (s[x] <= n - 2 * s[z[x] ? v : u]);
}
add(c2, s[x], 1);
for (int i = 0; i < e[x].size(); i++) {
int y = e[x][i];
if (y == f) continue;
dfs2(y, x);
}
add(c1, s[f], 1);
add(c1, n - s[x], -1);
if (x ^ rt) {
ans -= x * ask(c2, n - 2 * g[x]);
ans += x * ask(c2, n - 2 * s[x] - 1);
}
}
inline void solve() {
rd(n);
for (int i = 1; i <= n; i++) e[i].clear();
for (int i = 1, x, y; i < n; i++) rd(x), rd(y), e[x].pb(y), e[y].pb(x);
ans = 0;
dfs1(1, 0);
dfs1(rt, 0);
u = v = 0;
for (int i = 0; i < e[rt].size(); i++) {
int x = e[rt][i];
if (s[x] > s[v]) v = x;
if (s[v] > s[u]) swap(u, v);
}
for (int i = 1; i <= n + 1; i++) c1[i] = c2[i] = 0;
for (int i = 0; i <= n; i++) add(c1, s[i], 1), z[i] = 0;
z[u] = 1;
dfs2(rt, 0);
print(ans);
}
int main() {
int T;
rd(T);
while (T--) solve();
return 0;
}
主要是统计 x x x 为重心的次数