【算法笔记】树形DP算法总结详解

article/2025/9/13 9:06:34

0. 定义

树形DP,又称树状DP,即在树上进行的DP,是DP(动态规划)算法中较为复杂的一种。

1. 基础

f [ u ] = f[u]=~ f[u]= 与树上顶点 u u u有关的某些数据,并按照拓扑序(从叶子节点向上到根节点的顺序)进行 DP \text{DP} DP,确保在更新一个顶点时其子节点的dp值已经被更新好,以更新当前节点的 DP \text{DP} DP值。为方便计算,一般写成dfs的形式,如下:

void dfs(int v) { // 遍历节点vdp[v] = ...; // 初始化for(int u: G[v]) { // 遍历v的所有子节点dfs(u);update(u, v); // 用子节点的dp值对当前节点的dp值进行更新}
}

下面来看一道简单的例题:

【例1.1】子树大小

给定一棵有 N N N个结点的树,根结点为结点 1 1 1。对于 i = 1 , 2 , … , N i=1,2,\dots,N i=1,2,,N,求以结点 i i i为根的子树大小(即子树上结点的个数,包括根结点)。

本题明显可以使用树形DP的方法,令 f [ v ] = f[v]=~ f[v]=  v v v为根的子树大小,则易得
f [ v ] = 1 + ∑ i = 1 deg v G [ v ] [ i ] f[v]=1+\sum_{i=1}^{\text{deg}_v} G[v][i] f[v]=1+i=1degvG[v][i]
即:一个结点的子树大小 = 1 ~=1  =1(根节点) + +~ + 每个子树的大小。

沿用刚才的模板,可得:

#include <cstdio>
#include <vector>
#define maxn 100
using namespace std;vector<int> G[maxn]; // 邻接表
int sz[maxn]; // dp数组,sz[v] = 子树v的大小void dfs(int v)
{sz[v] = 1; // 初始化,最初大小为1,后面累加for(int u: G[v]) // 遍历子结点{dfs(u); // 先对子结点进行dfssz[v] += sz[u]; // 更新当前子树的大小}
}int main()
{int n;scanf("%d", &n); // 结点个数for(int i=1; i<n; i++) // N-1条边{int u, v;scanf("%d%d", &u, &v); // 读入一条边G[u].push_back(v); // 存入邻接表}dfs(1);for(int i=1; i<=n; i++)printf("%d\n", sz[i]);return 0;
}

下面来看一道稍微复杂一点的题:

【例1.2】洛谷P1352 没有上司的舞会

本题即树的最大独立集问题。

N N N名职员,编号为 1 … N 1\dots N 1N,他们的关系就像一棵以老板为根的树,父节点就是子节点的直接上司。每个职员有一个快乐指数 r i r_i ri,现在要召开一场舞会,使得没有职员和直接上司一起参会。主办方希望邀请一部分职员参会,使得所有参会职员的快乐指数总和最大,求这个最大值。

f ( v ) f(v) f(v)表示以 v v v为根的子树中,选择 v v v的最优解, g ( v ) g(v) g(v)表示以 v v v为根的子树中,不选 v v v的最优解。

则对于每个状态,都存在两种决策(其中 u u u代表 v v v的儿子):

  • 选择 v v v时,可选也可不选 u u u,此时有 g ( v ) = ∑ max ⁡ { f ( u ) , g ( u ) } g(v)=\sum\max\{f(u),g(u)\} g(v)=max{f(u),g(u)}
  • 不选 v v v时,一定不能选 u u u,此时有 f ( v ) = r i + ∑ g ( u ) f(v)=r_i+\sum g(u) f(v)=ri+g(u)

时间复杂度为 O ( N ) \mathcal O(N) O(N)
注意本题需要寻找根节点,没有上司的结点即为根节点,读入时用数组标记即可。

#include <cstdio>
#include <vector>
#define maxn 6005
using namespace std;inline int max(int x, int y) { return x > y? x: y; }vector<int> G[maxn]; // 邻接表
bool bad[maxn]; // 根结点标记
int f[maxn], g[maxn]; // 数据存储void dfs(int v) // 遍历结点v
{// 读入时已初始化,这里可省略for(int u: G[v]) // 遍历子结点{dfs(u); // 先对子结点进行dfs// 更新当前dp状态f[v] += g[u]; // 选择v,不能选ug[v] += max(f[u], g[u]); // 不选v,u可选可不选}
}int main()
{int n;scanf("%d", &n); // 结点个数for(int i=0; i<n; i++)scanf("%d", f + i); // 相当于提前初始化好f[i]=r[i]for(int i=1; i<n; i++) // N-1条边{int u, v;scanf("%d%d", &u, &v); // 读入一条边G[--v].push_back(--u); // 0-index,存入邻接表bad[u] = true; // 标记不可能是根结点}int root = -1; // 根结点变量for(int i=0; i<n; i++)if(!bad[i]) // 找到根结点{root = i; // 记录根结点break;}dfs(root); // 开始进行树形DPprintf("%d\n", max(f[root], g[root])); // 根结点也有两种选择return 0;
}

习题

  • HDU 2196 Computer / vjudge链接
  • POJ 1463 Strategic game
  • 洛谷 P3574 [POI2014] FAR-FarmCraft

2. 树上背包

在基本算法之上,树形dp还可以用于树上背包问题。来看一道例题:

【例2.1】洛谷P2014 / AcWing 286 选课

N N N门课,第 i i i门课的学分是 s i s_i si每门课有不超过一门先修课,需要上了先修课才能上这门课。现要选 M M M门课,使得学分总和最大。

每门课最多只有一门先修课,这符合树结构的特点,与有根树中一个点最多只有一个父亲结点的特点类似。因此,我们根据数据构造一棵树,课程的先修课为这门课的父结点。又由于给定的输入是一个森林(多棵树组成的不一定连通的图),不是一棵完整的树,因此我们添加虚拟根结点 0 0 0 s 0 = 0 s_0=0 s0=0),将没有先修课的结点全部连到它下面,并从这里开始dfs。注意此时必须选中 0 0 0号结点(它是所有课程的直接或间接先修课),所以操作前先将 M M M加上 1 1 1

格式问题解决,下面考虑如何 DP \text{DP} DP
f [ i ] [ j ] f[i][j] f[i][j]表示当前在结点 i i i、且已经选了 j j j门课时的最大学分数量,则答案为 f [ 0 ] [ M + 1 ] f[0][M+1] f[0][M+1]。状态转移方程等详见代码。时间复杂度为 O ( N M ) \mathcal O(NM) O(NM),有兴趣的可以自己尝试证明。

#include <cstdio>
#include <vector>
#include <algorithm>
#define maxn 305
using namespace std;// dp算法中常用的模板,等效于x=max(x,y)
inline void setmax(int& x, int y)
{if(x < y) x = y;
}vector<int> G[maxn]; // 邻接表
int n, m, f[maxn][maxn];int dfs(int u) // 遍历结点u,返回值为其子树大小
{int tot = 1; // 记录子树大小,初始为1for(int v: G[u]) // 遍历u的所有子结点{int sz = dfs(v); // 对当前子结点进行搜索// 状态转移,注意i倒序,防止串连转移现象for(int i=min(tot, m); i>0; i--) // 子树大小优化可降低算法复杂度for(int j=1, lim=min(sz, m-i); j<=lim; j++)setmax(f[u][i + j], f[u][i] + f[v][j]); // 更新状态tot += sz; // 加到当前子树下}return tot; // 返回子树大小
}int main()
{scanf("%d%d", &n, &m);for(int i=1; i<=n; i++){int a;scanf("%d%d", &a, f[i] + 1); // 初始化f[i][1]=s[i]G[a].push_back(i);}m ++; // 别忘了这一句dfs(0);printf("%d\n", f[0][m]);return 0;
}

习题

  • LOJ #2546. 「JSOI2018」潜入行动
  • LOJ #2268. 「SDOI2017」苹果树

3. 换根 DP

换根DP,即为不知道根结点时使用的一种树形DP,时间复杂度一般为 O ( N ) \mathcal O(N) O(N)

【例3.1】洛谷 P3478 [POI2008] STA-Station

给定一个 n n n个点的树,请求出一个结点,使得以这个结点为根时,所有结点的深度之和最大。

先考虑最简单粗暴的方法,即为枚举所有结点,代码如下:

#include <cstdio>
#include <vector>
#define maxn 1000005
using namespace std;vector<int> G[maxn];int dfs(int v, int d, int par)
{int s = d;for(int u: G[v])if(u != par)s += dfs(u, d + 1, v);return s;
}int main()
{int n;scanf("%d", &n);for(int t=n; --t; ){int u, v;scanf("%d%d", &u, &v);G[--u].push_back(--v);G[v].push_back(u);}int ans = 0, maxDepth = dfs(0, 0, -1);for(int root=1; root<n; root++){int d = dfs(root, 0, -1);if(d > maxDepth) ans = root, maxDepth = d;}printf("%d\n", ++ans);return 0;
}

很明显,这种做法时间复杂度为 O ( n 2 ) \mathcal O(n^2) O(n2),又因为 n ≤ 1 0 6 n\le 10^6 n106,所以无法得全分,评测结果如下:
评测结果
好家伙,居然还有50分,本以为最多30…

下面来考虑换根DP的方法。不妨令 u u u为当前结点, v v v为其子结点。先预处理出每个结点的子树大小 s [ u ] = 1 + ∑ s [ v ] s[u]=1+\sum s[v] s[u]=1+s[v]和以 1 1 1为根结点时所有结点的深度( depth i \text{depth}_i depthi),此时第一遍DFS即为预处理。

f u f_u fu表示以 u u u为根时,所有结点的总深度和,则 f 1 = ∑ depth i f_1=\sum\text{depth}_i f1=depthi
考虑 f u → f v f_u\to f_v fufv的转移,即“根结点从 u u u变成 v v v时所有结点深度和的变化”,则有:

  • 所有在 v v v的子树上的结点深度全部 − 1 -1 1,则总深度和减少 s v s_v sv
  • 所有不在 v v v的子树上的结点深度都 + 1 +1 +1,则总深度和增加 n − s v n-s_v nsv

此时,可得 f v = f u − s v + n − s v = f u + n − 2 s v f_v=f_u-s_v+n-s_v=f_u+n-2s_v fv=fusv+nsv=fu+n2sv。注意数据类型,使用long long

#include <cstdio>
#include <vector>
#define maxn 1000005
using namespace std;using LL = long long;vector<int> G[maxn];
LL sz[maxn], f[maxn];
int n, ans;LL dfs1(int v, int d, int par)
{sz[v] = 1;LL s = d;for(int u: G[v])if(u != par)s += dfs1(u, d + 1, v), sz[v] += sz[u];return s;
}void dfs2(int v, int par)
{if(f[v] > f[ans]) ans = v;for(int u: G[v])if(u != par){f[u] = f[v] + n - (sz[u] << 1LL);dfs2(u, v);}
}int main()
{scanf("%d", &n);for(int t=n; --t; ){int u, v;scanf("%d%d", &u, &v);G[--u].push_back(--v);G[v].push_back(u);}f[0] = dfs1(0, 0, -1);dfs2(0, -1);printf("%d\n", ++ans);return 0;
}

AC

习题

  • POJ 3585 Accumulation Degree
  • 洛谷 P2986 [USACO10MAR] Great Cow Gathering G
  • CodeForce 708C Centroids
  • ABC 222F - Expensive Expense

4. 后记

好像这玩意也并不是开头所说的那么难…… 记得给个三连哦!

参考文献:

  • 树形 DP - OI wiki
  • 树形dp - tom0727’s blog
  • 【动态规划】树形DP完全详解! - RioTian - 博客园

http://chatgpt.dhexx.cn/article/yMlpOQyQ.shtml

相关文章

★动态规划(DP算法)详解

什么是动态规划&#xff1a;动态规划_百度百科 内容太多了不作介绍&#xff0c;重点部分是无后效性&#xff0c;重叠子问题&#xff0c;最优子结构。 问S->P1和S->P2有多少种路径数&#xff0c;毫无疑问可以先从S开始深搜两次&#xff0c;S->P1和S->P2找出所有路…

ubuntu安装qt

Ubuntu安装qt 到qt官网下载“qt-opensource-linux-x64-5.9.1.run” 分别安装g, build-essential, libglq-mesa-dev, libglu1-mesa-dev freeglut3-dev 输入指令&#xff1a;”sudo apt-get install g” -> “sudo apt-get install build-essential” -> “sudo apt-get i…

QT安装、构建套件(MSVC、MinGW)配置

QT安装、构建套件(MSVC、MinGW)配置 1. QT框架及QT Creator下载 登录QT官网-https://www.qt.io/download。 点击downloads for open source users 在页面最下方&#xff0c;点击Download the QT online Installer。 在安装网页的最下方处有一行小字 “We do recommend yo…

QT linux安装

转载地址&#xff1a;http://www.cnblogs.com/tangkaixuan/p/6504102.html 文章来自https://lug.ustc.edu.cn/sites/qtguide/ 1.4 Qt在Linux下安装 Qt在Linux系统里的安装要稍微复杂一些&#xff0c;因为Linux发行版众多&#xff0c;所以安装过程有些差异。 由于Linux系统都可…

Qt国内镜像安装

下载安装程序 https://download.qt.io/official_releases/online_installers/ 使用国内镜像打开安装程序 G:\下载\qt-unified-windows-x64-4.5.1-online.exe --mirror https://mirror.nju.edu.cn/qt清华源 https://mirrors.tuna.tsinghua.edu.cn/qt/online/qtsdkrepository/win…

Qt下载与安装

一、Qt和Qt Creator的区别 Qt是C的一个库&#xff0c;或者说是开发框架&#xff0c;里面集成了一些库函数&#xff0c;提高开发效率。 Qt Creator是一个IDE&#xff0c;就是一个平台&#xff0c;一个开发环境&#xff0c;类似的比如说VS&#xff0c;也可以进行Qt开发&#xff…

Ubuntu 安装QT

一、最近这家公司接到一个订单&#xff0c;客户使用到国产操作系统&#xff0c;意味着需要使用到 Linux 系统&#xff0c;于是乎&#xff0c;之前的东西又要捡起来&#xff0c;而且&#xff0c;平时代码主要是windows 平台&#xff0c;这次需要将代码移植到linux 平台&#xff…

QT的安装------QT

由于现在的qt都是线上下载了&#xff0c;那我们就去qt的官网下载 download.qt.io 我们选择倒数第二个------archive(档案)&#xff0c;顺便学一下英文。 然后选择online install&#xff08;在线安装&#xff09; 身为先进人&#xff0c;当然选择最新的版本&#xff08;团新版…

VS+QT安装配置

心血来潮&#xff0c;装个QT&#xff0c;遇到好多问题&#xff0c;记录一下&#xff08;铭记那些踩过的坑&#xff09; 软件版本&#xff1a;vs是2022&#xff0c;qt是6.3.1 qt下载地址&#xff1a;Download Qt | Develop Desktop & Embedded Systems | Qt 点进去往下翻&…

Windows Qt安装教程

目录 一、Qt安装包下载二、Qt安装流程 一、Qt安装包下载 Qt官网&#xff1a;https://download.qt.io/ Qt官方提供的专门的资源下载网站&#xff0c;所有的开发环境和相关工具都可以从这里下载&#xff0c;Qt 官方下载网站如下图&#xff1a; 点击进入archive目录&#xff0c;…

Qt安装教程-详细

本文主要介绍了Qt5.9.7的安装步骤。 Qt下载 Qt的下载地址: http://download.qt.io/archive/qt/ qt-opensource-windows-x86-5.9.7.exe 是一个综合的安装包(5.8之前分开下载各个编译器版本SDK)&#xff0c;下载后安装的时候可以选择安装哪个编译器对应的SDK。一般可选MinGW 或…

QT下载安装

1、下载路径 https://download.qt.io/archive/qt/5.9/5.9.9/ 文件比较大&#xff0c;建议可以Qt 镜像网站 下载 国内著名的 Qt 镜像网站&#xff0c;主要是各个高校的&#xff1a;中国科学技术大学&#xff1a;http://mirrors.ustc.edu.cn/qtproject/ 清华大学&#xff1a;htt…

QT安装 and VS2019中安装QT插件

目录 VS中安装QT扩展插件1. 安装qt软件2. 在VS中安装qt扩展插件 VS中创建QT项目 VS中安装QT扩展插件 1. 安装qt软件 QT下载网址&#xff08;各版本&#xff09;. QT(5.12.11)直达界面. 根据操作系统不同&#xff0c;选择相应版本下载 双击运行… 输入QT账号信息&#xff0…

QT安装简介

1、下载QT安装包 下载网址&#xff1a;http://download.qt.io/ 或者http://download.qt.io/archive/qt/ 选择一个你需要的版本&#xff0c;例如 5.10 点击进去后&#xff0c;选择对应操作系统的安装包下载&#xff0c;例如qt-opensource-windows-x86-5.10.0.exe 2、安装QT …

QT安装教程(简易)

官方网址 Qt官方网址&#xff1a;http://download.qt.io 注意&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;这里下载记得要下载5版本以上的&#xff0c;因为5版本以上的自带打包工具&#xf…

Qt的安装及配置

一、Qt的安装 1.下载链接 或者 网盘下载 链接: https://pan.baidu.com/s/15Fwh8kOtrj4GIIg6ptnb7A 提取码: uvar 2.先注册账号&#xff0c;用自己的qq邮箱就可(注意&#xff1a;密码要有数字和大小写字母) 3.看图 4. 第一个&#xff1a;通过在Q中启用发送假名使用统计数据来…

Linux下的QT安装及初步使用过程(一)

目录 1.QT的安装 2.创建第一个QT程序 &#xff08;1&#xff09;QT代码&#xff08;C&#xff09; &#xff08;2&#xff09;使用qmake工具生成工程文件 ①确保qmake是可用的 ②如果不能找到qmake&#xff0c;则以下方式参考 ③使用qmake生成工程文件 ④生成Makefile文…

QT的安装 [新版2022]

QT的安装 [新版2022] 1 概述2 qt官网3 下载安装3.1 登录账号3.2 阅读同意许可3.3 开始安装3.4 帮助改进建议3.5 指定安装目录3.6 选择组件3.7 选择版本的依赖文件3.8 手动下载安装程序 1 概述 最近QT发布了6版本&#xff0c;5.x版本依然坚挺&#xff0c;官方也给出了LTS的标识…

【QT】简单易学的QT安装教程

对于在Windows系统上安装QT&#xff0c;经常会出现各种各样的错误&#xff0c;从不知选择版本到安装好后也无法使用&#xff0c;实属让人头疼。经过多次试错&#xff0c;找到了简单易学的QT方式&#xff0c;同时也会说明其中需要注意的点 一、QT安装和新建QT文件 https://dow…

Qt安装详细步骤

qt安装地址 安装步骤如下&#xff1a; &#xff08;上图为所勾选的编译组件详细勾选项&#xff0c;只适用于初学初学者&#xff09;