动态规划模型总结之树形dp

本文介绍了树形动态规划的基本概念、经典问题及其应用。树形dp是在树上进行动态规划,通过遍历获取信息来解决问题。文章列举了树的直径、重心、中心等经典问题的解决方法,并探讨了背包类树形dp的应用,如二叉苹果树和选课问题。此外,还提及了求树的最大独立集等相关题目。

刚讲完状压,我们来讲讲树形dp

1.基本概念

树形 d p dp dp,顾名思义就是在树上做动态规划,通过若干次的遍历树,得到有用的信息,解决问题

树形 d p dp dp的枚举顺序类似线性 d p dp dp,也有两个方向:

①:叶→根:根的子节点传递有用的信息给根,由根得出最优解

②:根→叶:需要取出所有点作为根节点求值,最后根节点获得整棵树的信息

动态规划的顺序一般是后序遍历,即当子节点处理完成再处理父节点

树形 d p dp dp一般通过记忆化搜索实现

普通的树形 d p dp dp的时间复杂度基本都是 O ( n ) O(n) O(n),若有附加维则为 O ( n ∗ m ) O(n*m) O(nm)

2.经典问题

①树的直径

给你一颗 n n n个节点的有权有根树,根节点编号为1,找到一条最长的路径

最长链不一定经过根,所以不能单纯的从根贪心

要解决这个问题我们可以使用树形 d p dp dp

d [ x ] d[x] d[x]表示从节点 x x x出发走向以 x x x为根的子树,能够到达的最远节点的距离

d [ x ] = max ⁡ i ∈ s o n [ x ] d [ i ] + l e n g t h [ x ] [ i ] d[x]=\max_{i∈son[x]}d[i]+length[x][i] d[x]=maxison[x]d[i]+length[x][i]

答案就是 max ⁡ x ∈ 1... n max ⁡ i ∈ s o n [ x ] d [ x ] + d [ i ] + l e n g t h [ x ] [ i ] \max_{x∈1...n}\max_{i∈son[x]}d[x]+d[i]+length[x][i] maxx1...nmaxison[x]d[x]+d[i]+length[x][i]

void dp(int x) {
   
   
    v[x] = 1 ;
    for (int i = head[x]; i; i = e[i].nxt) {
   
   
        int y = e[i].to ;
        if (v[y]) continue ;
        dp(y) ;
        ans = max(ans, d[x] + d[y] + e[i].w) ;
        d[x] = max(d[x], d[y] + e[i].w) ;
    }
}

这个问题也可以通过两遍 b f s bfs bfs解决,作者在此就不多说了

②树的重心

树的重心的定义是这样的:若把树变成该点为根的有根树时,最大子树的节点数最小

这个问题也比较简单

随便选一个点作为根

s z [ x ] sz[x] sz[x]表示以 x x x为根的子树大小

不难发现 s z [ x ] = ∑ y ∈ s o n [ x ] s z [ y ] + 1 sz[x]=\sum_{y∈son[x]}sz[y]+1 sz[x]=yson[x]sz[y]+1

删除 x x x节点后最大连通块有多少节点? 子树最大有 m a x { s z [ y ] } max\{sz[y]\} max{ sz[y]}个,还有上面的它的祖先构成的树大小为 n − s z [ x ] n-sz[x] nsz[x] d p dp dp时顺便更新答案

void dfs(int rt, int fat) {
   
   
	sz[rt] = 1 ;
	rep(i, 0, siz(e[rt]) - 1) {
   
   
		int to = e[rt][i] ;
		if (to == fat) continue ;
		dfs(to, rt) ;
		sz[rt] += sz[to] ;
		dp[rt]= max(dp[rt], dp[to]) ;
	}
	dp[rt] = max(dp[rt], n - dp[rt]) ;
	ans = min(ans, dp
<think> 我们正在讨论树形动态规划树形DP)的总结内容。根据引用内容,树形DP是将树的结构与动态规划相结合的算法,通常通过深度优先搜索(DFS)递归遍历实现自底向上的状态转移。核心在于状态设计和状态转移方程。 引用[1]提到树形DP涉及树的直径等问题,引用[2]给出了树形DP的通用模板,引用[3]强调状态转移方程的特殊性,引用[4]则给出了树的直径的两种常见状态转移模式。 接下来,我们将系统总结树形DP: 1. 基本概念:树形DP是在树结构上进行的动态规划,利用树的递归性质(无环、有根树可递归)进行状态转移。 2. 核心步骤: a. 状态定义:通常以节点为状态,dp[u]表示以u为根的子树的相关解(可能需要多个状态,如最长路径、次长路径等)。 b. 状态转移:根据子节点的状态更新当前节点的状态。由于树的结构,转移时需考虑所有子节点。 c. 边界条件:叶子节点的状态初始化。 d. 遍历方式:DFS(后序遍历,先处理子节点再处理当前节点)。 3. 经典问题: a. 树的直径:树中最长路径的长度。 - 状态定义: dp1[u]: 以u为根的子树中,从u出发向下走的最长路径长度。 dp2[u]: 以u为根的子树中,经过u的最长路径长度(可由两条向下路径拼接)。 - 状态转移: 对于每个子节点v,有边权w(u,v): dp1[u] = max(dp1[u], dp1[v] + w) # 更新最长路径 同时,我们可以记录下经过u的最长路径:用当前的最长路径和之前子树的最长路径拼接 # 在遍历子节点过程中,我们可以维护经过u的最长路径dp2[u]: # 实际上,整个树的直径就是所有节点的dp2[u]的最大值。 - 另一种常见做法:两次DFS/BFS求直径(无需DP),但树形DP可以同时求出所有节点的信息。 b. 二叉树中的最大路径和(LeetCode 124):路径不一定过根节点,但可以拆分为以每个节点为最高点的路径。 - 状态定义:dp[u]表示以u为起点向下走到某个节点的最大路径和(注意:这里路径可以不包括u,但通常定义包括u且只往一边延伸)。 - 但实际问题中,我们需要考虑经过u的路径,可能由左子树路径、u、右子树路径拼接而成。 - 因此,通常定义: dp[u]:以u为根向下延伸的最大路径和(只能选择左右一条路径,即要么左子树要么右子树,或者不选,因为不能分叉) - 同时,在递归过程中,计算经过u的最大路径和(左+u+右),并更新全局最大值。 c. 相邻字符不同的最长路径(LeetCode 2246):在树上寻找最长路径,要求相邻节点字符不同。 - 状态定义:dp[u]表示以u为起点的最长满足条件的向下路径长度。 - 状态转移:遍历u的子节点v,如果u和v的字符不同,则可以用dp[v]+1更新dp[u]。 - 同时,在节点u处,可以组合两个子节点(要求字符均与u不同且相互不同?题目要求路径上相邻节点字符不同,所以只要相邻不同即可,因此u和它的两个子节点只要分别不同就可以组合)的路径,更新全局答案。 4. 状态设计技巧: - 一维状态可能不够,有时需要二维(如:dp[u][0]和dp[u][1]分别表示选或不选u的最大值,如树的最大独立集)。 - 可能需要记录多个值,例如最长路径和次长路径(用于计算经过当前节点的最长直径)。 5. 通用模板(参考引用[2]): 使用邻接表存储树(无向图),DFS时记录父节点避免回访。 6. 注意事项: - 树形DP通常需要自底向上(后序遍历)计算,因此递归到叶子节点后返回时更新状态。 - 状态转移方程的设计要符合树的结构,避免重复或遗漏。 下面我们以树的直径问题为例,详细说明: 问题:给定一棵树,求树的直径(即树中最长路径的长度)。 解法1(两次遍历):任选一点,找到离它最远的点u,再从u出发找到离u最远的点v,则u到v的路径即为直径。 解法2(树形DP): 状态: dp1[u]: 从u出发向下走的最长路径长度(即u到其子树中叶子的最长路径) dp2[u]: 从u出发向下走的次长路径长度(与dp1[u]没有公共边,即来自不同的子树) 或者另一种常见做法: dp[u]:记录以u为根的子树中,以u为起点的最长路径长度。 同时,在更新dp[u]的过程中,我们计算经过u的路径(最长+次长)来更新全局直径。 具体步骤: 初始化:dp1[u] = 0, dp2[u] = 0 (或者初始化为0,因为路径长度是非负的,但如果有负边权则需要初始化为负无穷) 对于u的每个子节点v,边权为w: 递归处理v,得到dp1[v]。 计算从u经过v的路径长度:length = dp1[v] + w 然后更新: 如果length > dp1[u]: dp2[u] = dp1[u] # 原来的最长变成次长 dp1[u] = length 否则如果length > dp2[u]: dp2[u] = length 此时,经过u的最长路径(即直径候选)为 dp1[u] + dp2[u](因为最长和次长来自不同的子树,可以拼接) 全局直径取所有节点中 dp1[u]+dp2[u] 的最大值。 注意:如果题目允许负边权,那么初始化dp1[u]和dp2[u]为负无穷,并且更新时要注意。 代码框架(引用[4]的模式): int ans = 0; // 全局记录直径 void dfs(int u, int parent) { dp1[u] = 0; // 最长路径 dp2[u] = 0; // 次长路径,也可以不用dp2数组,用两个变量记录最长和次长 for (each child v of u with edge weight w) { if (v == parent) continue; dfs(v, u); int length = dp1[v] + w; if (length > dp1[u]) { dp2[u] = dp1[u]; dp1[u] = length; } else if (length > dp2[u]) { dp2[u] = length; } } ans = max(ans, dp1[u] + dp2[u]); } 但是引用[4]给出的是: dp1[u] = max(dp1[v] + w) // 这是更新最长路径 dp2[u] = max(dp1[v1] + dp1[v2] + w1 + w2) // 经过u的最长路径,注意这里实际上需要两个不同的子树,所以我们在更新dp1[u]时同时维护次长路径,然后直接用最长+次长即可(因为这两个一定来自不同子树),所以dp2[u]在这里可以理解为经过u的最长路径长度,那么全局答案就是所有节点的dp2[u]的最大值。 因此,在实际代码中,我们通常不单独为每个节点存储dp2[u](作为状态数组),而是用一个变量(比如ans)来记录全局最大值,每个节点处我们计算该节点作为转折点的最长路径(即dp1[u]+dp2[u])并更新ans。 总结树形DP树形DP的难点在于状态的设计和状态转移方程的建立。需要根据具体问题分析节点的状态如何由子节点状态转移过来。同时,树的结构决定了我们通常采用递归DFS来实现。 下面,我们根据用户要求生成相关问题: </think> ### 树形动态规划树形DP)总结 树形DP是一种将动态规划应用于树形结构的算法,核心是利用树的递归特性实现**自底向上**的状态转移。以下是系统性总结: --- #### 一、核心思想与步骤 1. **状态定义** - 状态 $dp[u]$ 表示以节点 $u$ 为根的子树的最优解(例如最长路径、最大和等) - 通常需扩展维度(如 $dp[u][0/1]$)处理带约束的问题(如选/不选节点)[^2][^3] 2. **状态转移** - **递归遍历**:通过DFS后序遍历,先处理子节点再合并结果 - **转移方程**: - 基础形式:$dp[u] = f(dp[v_1], dp[v_2], ..., \text{weight}(u,v))$ - 经典模型(树的直径): $$ \begin{align*} \text{dp1}[u] &= \max_{v \in \text{children}[u]} (\text{dp1}[v] + w(u,v)) \quad \color{gray}{\text{// 最长路径}} \\ \text{dp2}[u] &= \max_{v_1 \neq v_2} (\text{dp1}[v_1] + \text{dp1}[v_2] + w(u,v_1) + w(u,v_2)) \quad \color{gray}{\text{// 经过 } u \text{ 的路径}} \end{align*} $$ [^4] 3. **边界条件** - 叶子节点:$dp[\text{leaf}] = 0$ 或初始值(根据问题设定) 4. **全局答案** - 最终解为 $\max(dp[u])$ 或 $dp[\text{root}]$(需遍历所有节点) --- #### 二、经典问题与解法 | **问题** | **状态设计** | **关键转移逻辑** | |---------------------------|----------------------------------|--------------------------------------------------------------------------------| | **二叉树的直径** (LeetCode 543) | $dp[u]$:以 $u$ 为端点的最长路径 | $\text{ans} = \max(\text{ans}, \text{left\_len} + \text{right\_len})$ | | **最大路径和** (LeetCode 124) | $dp[u]$:以 $u$ 为起点的最大路径和 | $\text{ans} = \max(\text{ans}, \text{left\_sum} + \text{right\_sum} + u.\text{val})$ | | **相邻字符不同最长路径** (LeetCode 2246) | $dp[u]$:以 $u$ 为端点的最长路径 | 若 $u.\text{char} \neq v.\text{char}$,则 $dp[u] = \max(dp[u], dp[v] + 1)$ | --- #### 三、通用模板(C++) ```cpp const int N = 1e5 + 5; vector<int> tree[N]; int dp[N], ans = 0; // 根据问题扩展dp维度 void dfs(int u, int parent) { // 初始化边界(叶子节点) for (int v : tree[u]) { if (v == parent) continue; dfs(v, u); // 递归子节点 // 状态转移(根据问题修改) // dp[u] = max(dp[u], dp[v] + weight(u,v)); } // 后处理:更新全局答案 // ans = max(ans, dp[u]); } ``` --- #### 四、关键技巧 1. **避免环路**:DFS时记录父节点 `parent` 防止回溯 2. **多维度状态**:处理复杂约束(如节点选择、颜色限制) 3. **次大值维护**:求直径时需同时记录**最长**和**次长**路径(如 `dp1[u]`, `dp2[u]`) 4. **负权处理**:初始化 $dp[u] = -\infty$ 并检查边界 > 树形DP的核心在于**将树的结构转化为DP状态**,通过递归实现高效求解[^2][^3]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值