文章目录
一、算法概述
- 问题定义:树上(分组)背包问题是将背包问题与树结构结合的优化问题。在树形结构中,每个节点对应一个物品,节点的子树构成物品分组。目标是在背包容量限制下,从树的节点(物品)中选择,使得所选物品的总价值最大(或满足其他优化目标)。
- 应用场景:适用于资源分配、任务调度等具有树形依赖关系的场景。例如,在公司组织架构树中分配资源,每个部门(节点)有不同资源需求和贡献价值,需在总资源限制下实现最大收益。
二、算法思路
- 状态定义:
dp[i][j]表示以节点i为根的子树中,选择容量总和为j的物品时能获得的最优价值。sumVol[i]记录以节点i为根的子树中所有子节点的总容量。
- 状态转移核心:
- 基于树的递归结构,从叶子节点向根节点自底向上计算。
- 对于每个节点,先处理其子树的背包问题,再结合自身物品属性进行状态转移。
- 采用类似分组背包的策略,将节点的子树视为不同分组,遍历每个分组内物品的不同选择情况。
- 辅助函数作用:
KnapsackTree_InitValue用于初始化状态初始值。KnapsackTree_InfValue定义非法状态值。KnapsackTree_Opt实现状态转移时的最优计算(如取最大值)。KnapsackTree_Post将递归过程中的临时数据转移到持久化的dp数组中。
三、伪代码实现
1. 数据结构与全局变量定义
常量 maxn = 1010 // 最大节点数
常量 maxv = 110 // 最大背包容量
结构体 Item:
vol: 整数 // 物品体积
wei: 整数 // 物品价值
数组 items[1..maxn] // 存储所有物品信息
数组 child[0..maxn][] // 邻接表存储树结构,child[i] 存储节点 i 的子节点
二维数组 dp[0..maxn][0..maxv] // dp[i][j] 表示以 i 为根,容量 j 时的最优价值
数组 sumVol[0..maxn] // 记录以 i 为根的子树总容量
整数 n // 物品(节点)数量
整数 V // 背包最大容量
2. 核心函数实现
算法:树上分组背包
输入:树的节点数量 n,背包容量 V,树结构和物品信息
输出:背包容量为 V 时的最大价值
function KnapsackTree_InitValue(u):
// 初始化状态初始值
return 0
function KnapsackTree_InfValue():
// 返回非法状态值
return -1000000000
function KnapsackTree_Opt(curVal, preVal, itemWei):
// 状态转移的最优计算,取最大值
return max(curVal, preVal + itemWei)
function KnapsackTree_Post(u, dpu[]):
// 将临时数据 dpu 转移到 dp 数组
for j from 0 to V:
// 当前物品不选
if j == 0:
dp[u][j] = KnapsackTree_InitValue(u)
else:
dp[u][j] = KnapsackTree_InfValue()
// 当前物品选择的情况
if j >= items[u].vol:
dp[u][j] = KnapsackTree_Opt(dp[u][j], dpu[j - items[u].vol], items[u].wei)
function KnapsackTree_Init(dpu[][], u):
// 树上分组初始化
pre = 0
dpu[pre][0] = KnapsackTree_InitValue(u)
for i from 1 to V:
dpu[pre][i] = KnapsackTree_InfValue()
return pre
function KnapsackTree_GetAnswer(root):
// 获取根节点的最优答案
ans = KnapsackTree_InfValue()
for i from 0 to V:
ans = KnapsackTree_Opt(ans, dp[root][i], 0)
return ans
function KnapsackTree_DFS(u, fat):
// 深度优先搜索处理树节点
二维数组 dpu[2][maxv]
pre = KnapsackTree_Init(dpu, u)
cur = 1 - pre
sumVol[u] = items[u].vol
for v in child[u]:
// 跳过父节点
if v == fat:
continue
KnapsackTree_DFS(v, u)
sumVol[u] += sumVol[v]
for j from 0 to V:
dpu[cur][j] = KnapsackTree_InfValue()
for k from 0 to min(j, sumVol[v]):
dpu[cur][j] = KnapsackTree_Opt(dpu[cur][j], dpu[pre][j - k], dp[v][k])
swap(pre, cur)
KnapsackTree_Post(u, dpu[pre])
// 主程序
输入 n, V
for i from 1 to n:
输入 items[i].vol, items[i].wei, x
将 i 添加到 child[x] 中
KnapsackTree_DFS(0, -1)
输出 KnapsackTree_GetAnswer(0)
四、算法解释
1. 初始化阶段
KnapsackTree_InitValue函数将合法初始状态值设为0。KnapsackTree_InfValue定义非法状态值,用于表示不可行的选择情况。KnapsackTree_Init函数对每个节点的临时数组dpu进行初始化,容量为0时设为初始值,其他容量设为非法状态值。
2. 递归处理阶段(KnapsackTree_DFS)
- 参数说明:
u为当前节点,fat为父节点,避免搜索时回退到父节点形成环。 - 子树处理:对当前节点
u的每个子节点v,递归调用KnapsackTree_DFS处理子树,并更新sumVol[u]为子树总容量与自身物品容量之和。 - 状态转移:
- 对于每个容量
j,先将dpu[cur][j]设为非法状态值。 - 遍历子节点
v的不同容量选择k,通过KnapsackTree_Opt函数更新dpu[cur][j],实现子树间的状态合并。 - 交换
pre和cur,交替使用临时数组记录状态。
- 对于每个容量
3. 数据持久化阶段(KnapsackTree_Post)
- 递归完成后,将临时数组
dpu中的数据转移到持久化的dp数组中。 - 分别考虑当前节点物品选与不选的情况,更新
dp[u][j]。不选时根据容量处理,选时结合临时数组数据和自身物品价值进行最优计算。
4. 结果获取阶段(KnapsackTree_GetAnswer)
- 遍历根节点在不同容量下的
dp值,通过KnapsackTree_Opt函数找到最大值,即为背包容量为V时的最优价值。
五、复杂度分析
- 时间复杂度:
- 树的遍历为
O
(
n
)
O(n)
O(n),其中
n为节点数。 - 对于每个节点,状态转移时存在两层循环(遍历容量
j和子树容量k),假设树的深度为h,每个节点平均子节点数为c,最坏情况下,时间复杂度为 O ( n × V × V ) O(n \times V \times V) O(n×V×V)。实际中,若树结构平衡,复杂度接近 O ( n × V × c ˉ ) O(n \times V \times \bar{c}) O(n×V×cˉ),其中 c ˉ \bar{c} cˉ 为平均子节点数。
- 树的遍历为
O
(
n
)
O(n)
O(n),其中
- 空间复杂度:
- 存储树结构的邻接表
child空间复杂度为 O ( n ) O(n) O(n)。 dp数组空间复杂度为 O ( n × V ) O(n \times V) O(n×V)。- 临时数组
dpu空间复杂度为 O ( 2 × V ) O(2 \times V) O(2×V),可忽略不计。因此,总体空间复杂度为 O ( n × V ) O(n \times V) O(n×V)。
- 存储树结构的邻接表
本文为作者(英雄哪里出来)在抖音的独家课程《英雄C++入门到精通》、《英雄C语言入门到精通》、《英雄Python入门到精通》三个课程的配套文字讲解,如需了解算法视频课程,请移步 作者本人 的抖音直播间。
5806

被折叠的 条评论
为什么被折叠?



