当前位置: 首页 > news >正文

区间dp,数据结构优化dp等5种dp,各种trick深度讲解

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<i} { f[j] + (a[i] - a[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;elser = 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
}

part6. 综合技巧与实战策略

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

1. 空间优化技巧
  • 滚动数组:f[i][j]f[i%2][j]
  • 降维打击:分析状态依赖关系,减少维度
2. 时间优化技巧
  • 预处理:提前计算常用值
  • 剪枝:排除不可能状态
  • 记忆化:避免重复计算
3. 常数优化
  • 循环展开
  • 位运算优化
  • 缓存友好访问

part6.2 实战解题策略

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

part6.3 进阶学习方向

1. 动态DP

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

2. 概率DP与期望DP

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

3. 数位DP

按数位进行状态压缩,处理数字相关问题。

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

最后的话

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

关键收获:

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

学习建议:

  • 每学一个技巧,找3-5道相关题目练习
  • 总结同类问题的共性
  • 尝试一题多解,比较不同方法的优劣

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

http://www.dtcms.com/a/394785.html

相关文章:

  • 数据结构笔试选择题:题组1
  • 前端基础:从0到1实现简单网页效果(一)
  • 数据结构|图论:从数据结构到工程实践的核心引擎
  • AI赋能个人效能提升:实战演练工作规划、项目复盘与学习发展
  • 7. Linux RAID 存储技术
  • iOS 上架 App 费用详解 苹果应用发布成本、App Store 上架收费标准、开发者账号与审核实战经验
  • kafka 2.12_3.9.1 版本修复 Apache Commons BeanUtils 访问控制错误漏洞(CVE-2025-48734)
  • 二分查找经典——力扣153.寻找旋转排序数组中的最小值
  • 离散数学之命题逻辑
  • 【Linux命令从入门到精通系列指南】ping 命令详解:网络连通性诊断的终极工具
  • 游戏UI告别“贴图”时代:用Adobe XD构建“活”的设计系统
  • NXP - 用MCUXpresso IDE导入lpcopen_2_10_lpcxpresso_nxp_lpcxpresso_1769.zip中的工程
  • ✅ Python+Django租房推荐系统 双协同过滤+Echarts可视化 租房系统 推荐算法 全栈开发(建议收藏)✅
  • Django入门-3.公共视图
  • 【 设计模式 | 结构型模式 代理模式 】
  • 小杰机器学习高级(five)——分类算法的评估标准
  • IS-IS 中同时收到 L1 和 L2 的 LSP 时,是否优选 L1
  • 【开源】基于STM32的智能车尾灯
  • 电子电气架构 --- 软件开发与产品系统集成流程(下)
  • Ubuntu系统目录架构是怎么样的
  • 自动驾驶仿真之“场景交互”技术研究
  • 《AI管家还是数字化身?—— 一种面向未来的个人智能架构构想》
  • AI提升工业生产制造安全,基于YOLOv9全系列【yolov9/t/s/m/c/e】参数模型开发构建工业生产制造加工场景下工业设备泄漏智能化检测识别预警系统
  • 深度学习(十一):深度神经网络和前向传播
  • js立即执行函数的几种写法
  • RecyclerView里更新列表数是不想让header也刷新,怎么处理
  • C#/.NET/.NET Core技术前沿周刊 | 第 55 期(2025年9.15-9.21)
  • 减少实验烦恼,革新实验效率——PFA塑料容量瓶降低实验成本与风险
  • 留给石头科技的赛道不多了
  • 基于卷积神经网络的人车识别技术:从原理突破到场景重构的深度探索