完整教程:区间dp,数据结构优化dp等6种dp,各种trick深度讲解

news/2025/9/26 8:33:03/文章来源:https://www.cnblogs.com/wzzkaifa/p/19112445

DP提高课(一文讲全)

注:题单请看评论区。

之前我在写DP基础课的时候提到过会写提高课,结果因为各种事情拖了很久……这次终于补上了。

这次我们讲点进阶的动态规划:区间DP、数据结构优化DP、树形DP、倍增优化DP、状态压缩DP,以及这些DP中常用的技巧和优化方法。内容会比较多,我们一步一步来。


part1. 区间DP

part1.1 定义与深入理解

区间DP的核心思想是分治+合并。当我们面对一个区间问题时,可以将其分解为两个或多个子区间,分别求解后再合并。

更一般的状态设计思路:

  • 基础版:f[l][r] 表示区间 [l, r] 的最优解
  • 带附加信息:f[l][r][k] 表示区间 [l, r] 且具有某种特性k时的最优解
  • 多维度:f[l][r][x][y] 表示区间两端状态分别为x,y时的最优解

为什么枚举长度?
因为我们需要保证在计算大区间时,所有小区间都已经计算完成。这是DP的无后效性要求。

part1.2 经典例题深度分析

石子合并的数学证明

f(l, r) 为合并 [l, r] 的最小代价,s(l, r) 为区间和。

最优子结构证明:
对于任意分割点 k,有:

f(l, r) = f(l, k) + f(k+1, r) + s(l, r)

这是因为最后一次合并的代价固定为 s(l, r),而前后两部分的合并各自独立。

为什么这样设计正确?

  • 合并顺序不影响总代价(因为每次合并的代价都是被合并石子的总质量)
  • 问题具有最优子结构性质
  • 满足重叠子问题特性
环形处理的严格证明

对于环形问题,我们将其转化为长度为 2n 的线性问题。设原环上区间 [i, j] 对应线性数组中的 [i, j][i, j+n]

正确性证明:
环上任意长度为n的区间在线性数组中都存在且唯一,因此枚举所有长度为n的区间取最小值即可得到环上的最优解。

part1.3 高级技巧与优化

四边形不等式优化

定义: 代价函数 w(i, j) 满足:

  1. 区间包含单调性:w(i, j) ≤ w(i', j')[i, j] ⊆ [i', j']
  2. 四边形不等式:w(i, j) + w(i', j') ≤ w(i, j') + w(i', j)i ≤ i' ≤ j ≤ j'

应用: 如果区间DP的代价函数满足四边形不等式,那么决策点具有单调性,可以将复杂度从O(n³)优化到O(n²)。

// 优化后的石子合并
for (int len = 2; len <= n; len++) {
for (int l = 1; l + len - 1 <= n; l++) {
int r = l + len - 1;
int best_k = opt[l][r-1];
// 利用决策单调性
for (int k = best_k; k <= opt[l+1][r]; k++) {
if (f[l][k] + f[k+1][r] + s[r] - s[l-1] < f[l][r]) {
f[l][r] = f[l][k] + f[k+1][r] + s[r] - s[l-1];
opt[l][r] = k;
}
}
}
}
记忆化搜索的威力

对于复杂的区间划分,记忆化搜索往往更直观:

long long dfs(int l, int r) {
if (l == r) return 0;
if (f[l][r] != INF) return f[l][r];
long long res = INF;
for (int k = l; k < r; k++) {
res = min(res, dfs(l, k) + dfs(k+1, r) + s[r] - s[l-1]);
}
return f[l][r] = res;
}

part2. 数据结构优化DP

part2.1 系统化分类

数据结构优化DP可以分为以下几类:

1. 单调队列优化

适用条件:转移方程形如

f[i] = min_{j∈[i-k, i-1]} { f[j] + cost(j, i) }

且代价函数具有单调性。

经典例题: 滑动窗口最大值、多重背包单调队列优化

// 单调队列优化框架
deque<
int> q;
for (int i = 1; i <= n; i++) {
// 维护队列单调性
while (!q.empty() && f[q.back()] >= f[i-1]) q.pop_back();
q.push_back(i-1);
// 移除过期决策
while (!q.empty() && q.front() < i - k) q.pop_front();
// 转移
f[i] = f[q.front()] + cost(q.front(), i);
}
2. 线段树/树状数组优化

适用条件:需要区间查询最值或区间和。

// 线段树优化DP框架
struct SegmentTree {
// 实现区间最值查询、单点更新
};
SegmentTree seg;
for (int i = 1; i <= n; i++) {
// 查询区间 [i-k, i-1] 的最值
int min_val = seg.query(i-k, i-1);
f[i] = min_val + cost(i);
seg.update(i, f[i]);
}
3. 李超线段树优化

适用条件:转移方程包含一次函数项。

原理: 将每个决策j看作一条直线 y = k_j * x + b_j,用线段树维护在x处的最小值直线。

part2.2 斜率优化详解

斜率优化是数据结构优化DP的精华,用于形如:

f[i] = min_{j

步骤:

  1. 推导斜率公式
  2. 维护凸包
  3. 二分查找最优决策点
// 斜率优化模板
vector<
int> q;
// 单调队列,存储决策点下标
for (int i = 1; i <= n; i++) {
// 维护凸包:删除不满足凸性的点
while (q.size() >= 2) {
int j = q[q.size()-2], k = q.back();
if ((f[k]-f[j])/(a[k]-a[j]) >= (f[i]-f[k])/(a[i]-a[k]))
q.pop_back();
else break;
}
q.push_back(i);
// 二分查找最优决策
int l = 0, r = q.size()-1;
while (l < r) {
int mid = (l+r)/2;
if (get_slope(q[mid], q[mid+1]) <= 2*a[i])
l = mid+1;
else
r = mid;
}
f[i] = f[q[l]] + (a[i]-a[q[l]])*(a[i]-a[q[l]]);
}

part3. 树形DP的深入探讨

part3.1 状态设计的艺术

树形DP的状态设计远比基础课中丰富:

1. 树上背包问题

状态:f[u][j] 表示以u为根的子树中选择j个节点的最优解

void dfs(int u) {
size[u] = 1;
// 初始化:通常f[u][0] = 0, f[u][1] = w[u]
for (int v : children[u]) {
dfs(v);
// 背包合并:注意倒序枚举避免重复
for (int j = size[u]; j >= 0; j--) {
for (int k = 0; k <= size[v]; k++) {
f[u][j+k] = max(f[u][j+k], f[u][j] + f[v][k]);
}
}
size[u] += size[v];
}
}
2. 换根DP

用于求解每个节点作为根时的答案。

技巧: 第一次DFS计算子树信息,第二次DFS通过父节点信息更新子节点。

void dfs1(int u, int parent) {
// 计算子树信息
for (int v : adj[u]) {
if (v == parent) continue;
dfs1(v, u);
f[u] += f[v] + size[v];
// 示例:求深度和
}
}
void dfs2(int u, int parent) {
// 换根:g[v] = g[u] - size[v] + (n - size[v])
for (int v : adj[u]) {
if (v == parent) continue;
g[v] = g[u] - 2 * size[v] + n;
dfs2(v, u);
}
}
3. 复杂状态设计示例

例题: 树的直径(路径最长)

// f[u][0]: u为根的子树中最长路径
// f[u][1]: u为根的子树中次长路径(与最长路径不相交)
// 或者:
// f[u]: 从u出发的最长链
// g[u]: 经过u的最长路径
void dfs(int u, int parent) {
for (int v : adj[u]) {
if (v == parent) continue;
dfs(v, u);
// 更新经过u的最长路径
g[u] = max(g[u], f[u] + f[v] + 1);
// 更新从u出发的最长链
f[u] = max(f[u], f[v] + 1);
}
}

part3.2 树形DP的优化技巧

1. 长链剖分优化

用于深度相关的树形DP,将复杂度从O(n²)优化到O(n)。

原理: 继承重儿子信息,暴力合并轻儿子。

void dfs(int u, int parent) {
// 继承重儿子
if (heavy_son[u]) {
dfs(heavy_son[u], u);
f[u] = f[heavy_son[u]];
// 指针操作,实际是O(1)
}
// 合并轻儿子
for (int v : adj[u]) {
if (v == parent || v == heavy_son[u]) continue;
dfs(v, u);
// 暴力合并轻儿子信息
for (int i = 0; i <= len[v]; i++) {
f[u][i+1] += f[v][i];
// 示例操作
}
}
}
2. 虚树优化

当只需要处理部分关键节点时,构建虚树减少节点数量。


part4. 倍增优化DP的扩展应用

part4.1 倍增思想的本质

倍增的核心是二进制分解:任何数字都可以表示为2的幂次之和,任何跳跃都可以分解为2^k步的跳跃。

part4.2 扩展应用场景

1. 树上路径查询

预处理每个节点向上2^k步的信息(最大值、最小值、和等)。

// 预处理
for (int k = 1; k < LOG; k++) {
for (int i = 1; i <= n; i++) {
fa[i][k] = fa[fa[i][k-1]][k-1];
max_val[i][k] = max(max_val[i][k-1], max_val[fa[i][k-1]][k-1]);
}
}
// 查询u到v路径上的最大值
int query_max(int u, int v) {
if (depth[u] < depth[v]) swap(u, v);
int res = -INF;
// u向上跳至与v同深度
for (int k = LOG-1; k >= 0; k--) {
if (depth[fa[u][k]] >= depth[v]) {
res = max(res, max_val[u][k]);
u = fa[u][k];
}
}
if (u == v) return res;
// 同时向上跳
for (int k = LOG-1; k >= 0; k--) {
if (fa[u][k] != fa[v][k]) {
res = max(res, max(max_val[u][k], max_val[v][k]));
u = fa[u][k], v = fa[v][k];
}
}
return max(res, max(max_val[u][0], max_val[v][0]));
}
2. 序列上的倍增DP

例题: 给定序列,求每个位置向右跳k步后的位置。

// f[i][k]: 从i开始跳2^k步到达的位置
for (int k = 1; k < LOG; k++) {
for (int i = 1; i <= n; i++) {
f[i][k] = f[f[i][k-1]][k-1];
}
}
// 查询从x开始跳y步
int jump(int x, int y) {
for (int k = 0; k < LOG; k++) {
if (y >> k &
1) {
x = f[x][k];
}
}
return x;
}
3. 倍增优化状态转移

当DP转移具有重复性时,可以用倍增优化。

示例:f[i][j] = f[f[i][j-1]][j-1]


part5. 状态压缩DP的高级应用

part5.1 从基础到进阶

1. 子集枚举的优化技巧

经典枚举方法:

// 枚举mask的所有子集
for (int submask = mask; submask; submask = (submask-1) & mask) {
// 处理子集submask
}

时间复杂度分析:

  • 对于有k个1的mask,有2^k个子集
  • 总复杂度:ΣC(n,k)*2^k = 3^n
2. 轮廓线DP(插头DP)

插头DP是状态压缩DP的高级形式,用于网格图上的路径问题。

核心概念:

  • 插头:网格边界的连通性状态
  • 括号表示法:用括号序列表示连通性
  • 逐格递推:逐个格子进行状态转移
// 插头DP框架(哈密顿路径计数)
struct State {
int mask;
// 轮廓线状态
long long cnt;
};
unordered_map<
int, long long> f[2];
// 滚动数组
void dp() {
int cur = 0;
f[cur][0] = 1;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
int nxt = cur ^ 1;
f[nxt].clear();
for (auto &
[mask, cnt] : f[cur]) {
int left = (mask >> j) &
1;
// 左插头
int up = (mask >>
(j+1)) &
1;
// 上插头
if (!grid[i][j]) {
// 障碍物
if (!left &&
!up) f[nxt][mask] += cnt;
continue;
}
if (!left &&
!up) {
// 新建连通分量
int new_mask = mask | (1 << j) | (1 <<
(j+1));
f[nxt][new_mask] += cnt;
} else if (left &&
!up) {
// 延续左插头
// 两种情况:向右或向下
} else if (!left && up) {
// 延续上插头
// 两种情况:向右或向下
} else {
// 合并两个连通分量
int new_mask = mask ^ (1 << j) ^ (1 <<
(j+1));
f[nxt][new_mask] += cnt;
}
}
cur = nxt;
}
}
}
3. 集合幂级数优化

使用FWT(快速沃尔什变换)优化状态压缩DP的卷积运算。

// 子集卷积优化
void fwt(vector<
int>
&a) {
int n = a.size();
for (int k = 1; k < n; k <<= 1) {
for (int i = 0; i < n; i += k <<
1) {
for (int j = 0; j < k; j++) {
a[i+j+k] += a[i+j];
// 或使用其他变换
}
}
}
}

part5.2 状态设计的高级技巧

1. 双轮廓线DP

用于更复杂的网格问题,同时维护两行状态。

2. 三进制状态压缩

用于需要区分多种状态的情况(如:已选/未选/禁止选)。

3. 状态压缩+Meet in Middle

将状态分成两半,分别处理后再合并。

// 折半搜索框架
void dfs1(int idx, int mask, int value) {
if (idx == n/2) {
left_states[mask] = value;
return;
}
dfs1(idx+1, mask, value);
// 不选当前
dfs1(idx+1, mask | (1<<idx), value + w[idx]);
// 选当前
}
void dfs2(int idx, int mask, int value) {
if (idx == n) {
// 在left_states中寻找互补状态
int target = total_mask ^ mask;
if (left_states.count(target)) {
ans = max(ans, value + left_states[target]);
}
return;
}
// 类似dfs1
}

(upd on 2025.9.23,补充数位DP)

part6. 数位DP

part6.1 定义与核心思想

数位DP是一类非常有代表性的 数字问题专用DP。它的核心在于:
当我们处理一个关于数字的计数或最优问题时,可以 逐位枚举(从高位到低位),并用状态来记录“是否贴合上界”、“前缀信息”等条件,从而避免重复计算。

典型问题类型:

为什么能用DP?
因为 每一位的选择只取决于更高位的状态(是否已经达到上界、是否仍有前导零等),而与之后的低位无关,满足无后效性。


part6.2 状态设计方法论

一个标准的数位DP状态可以写作:

f(pos,sum,lead,limit) f(pos, sum, lead, limit) f(pos,sum,lead,limit)

  • pos:当前处理到的位数(一般从高位到低位)
  • sum:记录到当前位置的某种性质(如数位和、余数、最大数位等)
  • lead:是否还在前导零(防止把前导零算作有效位)
  • limit:当前是否受上界约束(是否必须 ≤ 给定数字的这一位)

例子:

  • 如果题目是「统计 [1,n][1, n][1,n] 内数位和为k的数个数」
    状态就是 f(pos,cur_sum,lead,limit)f(pos, cur\_sum, lead, limit)f(pos,cur_sum,lead,limit)
  • 如果题目是「统计 [1,n][1, n][1,n] 内不含4的数个数」
    状态就是 f(pos,has_4,lead,limit)f(pos, has\_4, lead, limit)f(pos,has_4,lead,limit)

part6.3 经典例题解析

例题1:统计 [1,n][1, n][1,n] 内数位和等于 k 的数

转移过程:

  1. 枚举当前位置填的数字 d(0~9,若 limit=1 则上限为 n[pos])
  2. 如果 lead=1 且 d=0,那么 sum 不变,继续 lead=1
  3. 否则 sum 累加 d,lead=0
  4. 下一层递归 f(pos-1, sum+d, new_lead, new_limit)

最终在 pos=0 时判断 sum==k。


例题2:不含相邻相等数字的数
  • 状态设计:f(pos, last_digit, lead, limit)
  • last_digit=-1 表示还没选过数字
  • 转移时要求 d ≠ last_digit,才能合法转移

part6.4 常见技巧

1. 记忆化 vs 表格DP
  • 数位DP常用记忆化搜索写法,因为 limit 和 lead 状态不能简单预处理
  • 当 limit=0 时,状态可以记忆化;limit=1 时需要继续递归
2. 区间问题 [L, R]

只需写一个 solve(n) 求 [0, n] 的答案,再用 solve® - solve(L-1) 即可。

3. 优化方向
  • 压缩状态(sum 只取模时,可用 dp[pos][mod])
  • 预处理组合数/阶乘(如数位组合计数)
  • 高位剪枝(当剩余位数不足以满足条件时提前返回)


part6.5 常见题型总结

  1. 数位和类

  2. 数位限制类

  3. 计数+最优性结合

  4. 复杂条件


part7. 综合技巧与实战策略

part7.1 DP优化技巧的系统总结

1. 空间优化技巧
  • 滚动数组:f[i][j]f[i%2][j]
  • 降维打击:分析状态依赖关系,减少维度
2. 时间优化技巧
3. 常数优化
  • 循环展开
  • 位运算优化
  • 缓存友好访问

part7.2 实战解题策略

1. 状态设计思维流程
1. 识别问题类型(计数、最优、可行性)
2. 确定状态维度(位置、选择情况、附加条件)
3. 设计状态表示(数组维度、含义)
4. 推导转移方程(如何从小状态推到大状态)
5. 确定边界条件(最小子问题的解)
6. 优化空间时间(数据结构、数学性质)
2. 调试技巧
  • 打印DP表观察状态转移
  • 小数据手动验证
  • 对拍验证正确性

part7.3 进阶学习方向

1. 动态DP

支持修改操作的动态规划,通常用矩阵表示转移,用线段树维护。

2. 概率DP与期望DP

处理随机过程,需要概率论知识。

3. 状压DP的进一步扩展
  • 斯坦纳树问题
  • 一般图匹配
  • 旅行商问题的进一步优化

最后的话

DP提高课到这里就真正结束了。从基础的区间DP到复杂的插头DP,我们覆盖了竞赛中常见的各种DP类型和优化技巧。

关键收获:

  1. 状态设计是核心:好的状态设计决定了解题的成败
  2. 优化需要洞察力:发现问题的特殊性质才能有效优化
  3. 实践出真知:只有大量练习才能掌握这些技巧

学习建议:

其实 DP 本质上需要多练,否则你看懂了其实并没有用,但是你没看懂直接去练题也没有用,所以说请认真读懂,否则等于没用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/918002.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

win10开机输入密码后一直转圈,很长时间才登录到桌面

经过研究后,查看了系统日志有如下记录: 发现是winlogon通知订户 <Profiles> 正在长时间处理此通知事件(Logon) 然后发现耗时确实过长,花了将近5分钟 解决方法 最后是通过将用户缓存文件删除解决的,Win+R然…

安庆网站建设推荐安徽秒搜科技必应搜索引擎首页

解决办法可以参考&#xff1a;修改hosts 不生效? 三种方法解决

Windows11 右键菜单管理

Windows11 右键菜单管理 取消一些压根用不上的选项jcmaxx33@gmail.com jcmaxx33Team@github

Mac 安装PDF2zh

一、homebrew 1、安装homebrew /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" 官网:https://brew.sh/zh-cn/ 2、加入PATH(USERNAME需更换) echo &g…

完整教程:leetcode (2)

完整教程:leetcode (2)pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", &qu…

西安网站建设流程海南平台网站建设平台

目录 依赖项:timm库。 cuda版1060显卡运行时间 14ms左右 高通不支持gelu激活函数 需要的 SqueezeExcite代码,不是SqueezeExcite_o

福州网站排名买车看车app排行榜

可编辑(一) 像素函数56. putpiel() 画像素点函数57. getpixel()返回像素色函数(二) 直线和线型函数58. line() 画线函数59. lineto() 画线函数60. linerel() 相对画线函数61. setlinestyle() 设置线型函数62. getlinesettings() 获取线型设置函数63. setwritemode() 设置画线模…

如何通过 Python + Selenium + BeautifulSoup 爬取动态加载的网页数据 - 教程

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

实用指南:【连载6】 C# MVC 日志管理最佳实践:归档清理与多目标输出配置

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

2025Unity必备知识——GUI(完整详细) - 指南

2025Unity必备知识——GUI(完整详细) - 指南pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", &q…

HBM之父:HBM的终点是HBF

微信视频号:sph0RgSyDYV47z6快手号:4874645212抖音号:dy0so323fq2w小红书号:95619019828B站1:UID:3546863642871878B站2:UID: 3546955410049087韩国半导体公司SK海力士日前宣布,已完成下一代超高性能AI存储器产…

深圳哪家建设网站公司好网页链接打不开是什么原因

12天 本节学习了基于MindSpore的GPT2文本摘要。 1.数据集加载与处理 1.1.数据集加载 1.2.数据预处理 2.模型构建 2.1构建GPT2ForSummarization模型 2.2动态学习率 3.模型训练 4.模型推理

实用指南:40.应用层协议HTTP(三)

实用指南:40.应用层协议HTTP(三)pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco…

建德市住房和城乡建设局网站制作公司官网的步骤

目录 1 搭建嵌入式gdb调试环境 1.1 交叉编译工具链自带的gdb和gdbserver 1.2 使用gdb进行嵌入式程序调试 1.2.1编写简单测试程序 1.2.2 gdb调试程序 1.3 源码编译gdb和gdbserver 1.3.1 下载gdb和gdbserver源码 1.3.2 编译gdb 1.3.3 移植gdbserver 2 VSCodegdbserver 图…

南宁制作网站网页代码大全

我初始化h2数据库&#xff0c; 然后把jar 放到 springside-4.0.0.RC2\examples\mini-web 中 &#xff0c;最后配置启动tomcat。 就出现这个错。 最开始我用 quick-start.bat &#xff0c;但总是执行到一半就 挂掉了。 提示spy 包没找到 。 这个是在装载 xml&#xff0c;解析成b…

网站建设调研提纲wordpress评论页面

目录 1. 文本分类 2. 图像识别 3. 生物信息学 4. 金融预测 5. 其他领域 1. 文本分类 垃圾邮件过滤&#xff1a;SVM通过训练大量标记为垃圾邮件和非垃圾邮件的样本&#xff0c;学习出能够区分两者的模型&#xff0c;从而实现对新邮件的自动分类。情感分析&#xff1a;在社…

【GitHub每日速递 250926】12 周 24 课,边学边练!微软 AI 初学者的通关秘籍

原文:https://mp.weixin.qq.com/s/t99TeeaVhDTuzobY6WkYng 揭秘Linera:高可扩展区块链基础设施,附CLI工具快速上手攻略 linera-protocol 是一个基于Rust构建的高性能、去中心化微服务区块链协议的底层框架。简单讲,…

mcp-use mcp server 交互工具

mcp-use mcp server 交互工具mcp-use mcp server 交互工具,当然mcp-use 不支持简单的mcp client,还支持agent开发,通过mcp-use 我们可以将llm 与mcp server 集成起来,提升agent 的能力 简单示例 import asyncio i…

年薪破百万、涨薪60%,人形机器人企业疯狂「抢人」

微信视频号:sph0RgSyDYV47z6快手号:4874645212抖音号:dy0so323fq2w小红书号:95619019828B站1:UID:3546863642871878B站2:UID: 35469554100490872025年,人形机器人不再只是春晚舞台上的炫技明星,而是逐渐走进具…

深入解析:HttpClientFactory vs new HttpClient:.NET Core HTTP 客户端的正确打开方式

深入解析:HttpClientFactory vs new HttpClient:.NET Core HTTP 客户端的正确打开方式2025-09-26 08:01 tlnshuju 阅读(0) 评论(0) 收藏 举报pre { white-space: pre !important; word-wrap: normal !important;…