惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
The Exploit Database - CXSecurity.com
F
Fortinet All Blogs
U
Unit 42
F
Full Disclosure
雷峰网
雷峰网
博客园 - 司徒正美
云风的 BLOG
云风的 BLOG
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
Tailwind CSS Blog
The Cloudflare Blog
Last Week in AI
Last Week in AI
罗磊的独立博客
D
DataBreaches.Net
C
Check Point Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
O
OpenAI News
C
CXSECURITY Database RSS Feed - CXSecurity.com
aimingoo的专栏
aimingoo的专栏
S
Security @ Cisco Blogs
大猫的无限游戏
大猫的无限游戏
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
S
SegmentFault 最新的问题
NISL@THU
NISL@THU
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
The Hacker News
The Hacker News
Webroot Blog
Webroot Blog
Security Latest
Security Latest
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Google DeepMind News
Google DeepMind News
酷 壳 – CoolShell
酷 壳 – CoolShell
N
News | PayPal Newsroom
P
Proofpoint News Feed
B
Blog RSS Feed
MongoDB | Blog
MongoDB | Blog
C
Cybersecurity and Infrastructure Security Agency CISA
N
News and Events Feed by Topic
Google Online Security Blog
Google Online Security Blog
H
Help Net Security
Spread Privacy
Spread Privacy
T
Threat Research - Cisco Blogs
GbyAI
GbyAI
I
Intezer
Application and Cybersecurity Blog
Application and Cybersecurity Blog
M
MIT News - Artificial intelligence
Vercel News
Vercel News
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
IT之家
IT之家
MyScale Blog
MyScale Blog
腾讯CDC

潮汐朝夕

Ad-Hoc问题:在字符串中删改字符 leetcode第一题,两数之和 频繁查询子串是否回文:区间DP 频繁查询子串是否回文:Manacher预处理后$O(1)$响应 状态转移方程描述最优子结构 | 优化状态表示 | 单调队列优化DP 滑动窗口 | 按位单独处理 | 字符计数 通过模拟解决算法问题:导论 Python的生成器对象与 filter 对象 按位单独处理的技巧 滑动窗口 | 满足条件的最短的子串 滑动窗口字符计数的优化:增加维护聚合信息应对高频查询 滑动窗口 | 满足条件的子串数目 最少硬币组合问题2:贪心算法【附算法导论习题答案下载】 力扣2274-不含特殊楼层的最大连续楼层数 最少硬币组合问题1:完全背包 带余除法 | 算法式证明 | 进制转换 2025年的内容与栏目建设 概率方法 | Cr不等式 | r阶绝对矩 Weierstrass不等式的另一边 | Bernoulli不等式的推广
树形 DP | 剪枝优化 | 移位操作
Cheng Zhaoxi · 2025-01-23 · via 潮汐朝夕

字数统计: 1.8k字   |   阅读时长: 7分

摘要: 等效性剪枝优化树形 DP


【对算法,数学,计算机感兴趣的同学,欢迎关注我哈,阅读更多原创文章】
我的网站:潮汐朝夕的生活实验室
我的公众号:算法题刷刷
我的知乎:潮汐朝夕
我的github:FennelDumplings
我的leetcode:FennelDumplings

各位好,今天我们来看一个难题拆解,算法的主框架为树形 DP,第一个难点是发现子树作为阶段需要再加上附加信息才能具备最优子结构,另一个难点是状态空间中存在大量可以被等效掉的状态,这些状态的结果是已知的,无需去计算而代入已知的结果即可,DP 推导过程中的这部分状态空间自然就省掉了,从剪枝的类型来看,这是一种等效性剪枝。

题目

有一棵由 n 个节点组成的无向树,以 0 为根节点,节点编号从 0 到 n - 1 。给你一个长度为 n - 1 的二维 整数 数组 edges ,其中 edges[i] = [ai, bi] 表示在树上的节点 ai 和 bi 之间存在一条边。另给你一个下标从 0 开始、长度为 n 的数组 coins 和一个整数 k ,其中 coins[i] 表示节点 i 处的金币数量。

从根节点开始,你必须收集所有金币。要想收集节点上的金币,必须先收集该节点的祖先节点上的金币。

节点 i 上的金币可以用下述方法之一进行收集:

  • 收集所有金币,得到共计 coins[i] - k 点积分。如果 coins[i] - k 是负数,你将会失去 abs(coins[i] - k) 点积分。
  • 收集所有金币,得到共计 floor(coins[i] / 2) 点积分。如果采用这种方法,节点 i 子树中所有节点 j 的金币数 coins[j] 将会减少至 floor(coins[j] / 2)

返回收集 所有 树节点的金币之后可以获得的最大积分。

提示:

1
2
3
4
5
6
n == coins.length
2 <= n <= 1e5
0 <= coins[i] <= 1e4
edges.length == n - 1
0 <= edges[i][0], edges[i][1] < n
0 <= k <= 1e4

示例 1:
输入:edges = [[0,1],[1,2],[2,3]], coins = [10,10,3,3], k = 5
输出:11
解释:
使用第一种方法收集节点 0 上的所有金币。总积分 = 10 - 5 = 5 。
使用第一种方法收集节点 1 上的所有金币。总积分 = 5 + (10 - 5) = 10 。
使用第二种方法收集节点 2 上的所有金币。所以节点 3 上的金币将会变为 floor(3 / 2) = 1 ,总积分 = 10 + floor(3 / 2) = 11 。
使用第二种方法收集节点 3 上的所有金币。总积分 = 11 + floor(1 / 2) = 11.
可以证明收集所有节点上的金币能获得的最大积分是 11 。

示例 2:
输入:edges = [[0,1],[0,2]], coins = [8,4,4], k = 0
输出:16
解释:
使用第一种方法收集所有节点上的金币,因此,总积分 = (8 - 0) + (4 - 0) + (4 - 0) = 16 。

题解

算法: 树形DP

考虑树中的节点 $u$,假设 $u$ 有 $s$ 个子节点,分别记为 $v_{1}, \cdots, v_{s}$。如果忽略 $u$ 的父节点 $f$ 带来的 影响,仅考虑 $u$ 这棵子树自身最多能获得多少积分,这就是一个子问题。

但仅考虑子树自身尚不能形成最优子结构,也就是原问题(即根节点对应的树可以获得的最大积分)不一定包含 $u$ 这棵子树视为孤立的树时的最大积分。

因为父节点有其它信息会对 $u$ 这棵子树上的结果有影响,这个信息就是 $u$ 的祖先链上共做了多少次操作 (2),把这个信息加上,就可以形成最优子结构了。

综上,这是阶段(子树)和附加信息(祖先链上操作 (2) 的次数)共同形成的最优子结构,其状态表示为 $dp[u][j]$,也就时当 $u$ 的祖先链上的操作 (2) 次数为 $j$ 时,$u$ 对应的子树可以取得的最大积分。

状态这样表示的话,$dp[root][0]$ 为问题的答案,叶节点对应初始状态。状态转移方程如下:

其中 $g(u, j)$ 表示当祖先链上有 $j$ 次操作 (2) 时,$c[u]$ 会减少至多少,若 $c[u]$ 本身为 0,则 $g(u, j) = 0$。

代码 (Python)

注:DP 推导的无效状态太多,空间超限,需要对状态空间剪枝。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from functools import lru_cache

class Solution:
def maximumPoints(self, edges: List[List[int]], coins: List[int], k: int) -> int:
n = len(coins)
graph = [[] for _ in range(n)]
for u, v in edges:
graph[u].append(v)
graph[v].append(u)

@lru_cache(maxsize=int(1e7))
def g(u: int, j: int) -> int:
if j == 0:
return coins[u]
return g(u, j - 1) // 2

@lru_cache(maxsize=int(1e7))
def solve(fa: int, u: int, j: int) -> int:
ans1 = g(u, j) - k
ans2 = g(u, j) // 2
for v in graph[u]:
if v == fa:
continue
ans1 += solve(u, v, j)
ans2 += solve(u, v, j + 1)
return max(ans1, ans2)

return solve(-1, 0, 0)

优化

如果只是上述的树形 DP 算法,本题还不至于很难。后面的两个优化是通过本题的关键。

优化1:移位操作

一次操作 (2) 要做一次 $x = \lfloor\frac{x}{2}\rfloor$,这相当于 x = (x >> 1)

另一方面,移位操作可以叠加,也就是 x >> 1 >> 1 = x >> 2。于是 $j$ 次操作 (2) 后,自然有 x = (x >> j)

这样,计算 $g(u, j)$ 就变得简单了,直接 g(u, j) = coins[u] >> j 即可。

优化2:剪枝优化 DP

如前所述,DP 推导的无效状态太多,空间超限,需要对状态空间剪枝。

由于树中各节点的值是有范围的,它们移位若干次后,就都变为零了,此时再进行操作 (2),子树上得到的结果只会是 0,这样就不用去计算 2 对应的状态了,这部分状态空间就可以省了。

由数据范围给定的 0 <= coins[i] <= 1e4,其对应的二进制最多 14 位,也就是最多移位 14 次,就进入了上面说的操作 (2) 对应的结果直接为 0 的情况了。于是可以增加等效性剪枝:当 j > 14,跳过操作 (2) 的计算,直接将 0 作为对应的结果即可。

可以想象,如果没有这个剪枝,如果树的形状是高度非常高的那种,$j$ 这一维度的状态就会撑得很大,但如前所述,其中大于 14 的部分都是无效的。

代码 (Python)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from functools import lru_cache

class Solution:
def maximumPoints(self, edges: List[List[int]], coins: List[int], k: int) -> int:
n = len(coins)
graph = [[] for _ in range(n)]
for u, v in edges:
graph[u].append(v)
graph[v].append(u)

@lru_cache(maxsize=int(1e7))
def solve(fa: int, u: int, j: int) -> int:
ans1 = (coins[u] >> j) - k
ans2 = coins[u] >> (j + 1)
for v in graph[u]:
if v == fa:
continue
ans1 += solve(u, v, j)
if j <= 14:
ans2 += solve(u, v, j + 1)
return max(ans1, ans2)

return solve(-1, 0, 0)