关于动态规划的一些理解
关于动态规划的一些理解
1. 什么是动态规划#
动态规划(DP,Dynamic Programming)是一种解决问题的方法。它通过将难以实现的整体的大问题划分成简单的局部的小问题。最后将小问题一一求解以完成问题。
对于动态规划能否使用有一些限制,这些限制我推荐参看动态规划基础 - OI Wiki中的描述。(实际上是不是用DP看一眼题就知道了)。
动态规划只是一种解决问题的方法,它并不是类似于DFS(深度优先搜索)有特定实现的算法,具体问题具体分析。
2. 常见的动态规划类型以及理解#
2.1 背包DP#
背包问题是动态规划的一个分支。对于大部分人而言,背包DP更容易理解、入门动态规划。
背包问题本质是在多个种类物品之间做选择。由于[重量]等原因对于选择做了限制,通常求选择的最优解。
2.1.1 01背包#
01背包是背包问题的一种。该类问题需要在多个种类的物品之间做出选择,每个种类只能选择一个,由于“选与不选”的特性,类似于二进制中的01[真假],所以称为01背包。
下面对于01背包一个例题:
P2871 [USACO07DEC] Charm Bracelet S - 洛谷
题目大意:
有 N 件物品和一个容量为 M 的背包。第 i 件物品的重量是 Wi,价值是 Di。求解将哪些物品装入背包可使这些物品的重量总和不超过背包容量,且价值总和最大。
输入样例:
4 6 1 4 2 6 3 12 2 7
输出样例:
23
解题思路:
这是一道01背包问题的模板题。对于容量 M 的背包,装 x 个物品使其总价值 D 最大。最先想到的可能是贪心。但是由于重量的存在,使得价值最高可能不是收益最高。高价值高重量的物品可能不及多个低价值低重量物品之和。所以应该使用动态规划。
假设我们有
那么我们就将求这道题的答案转化为了求
我们只关注于第 u 件物品,它有取与不取两种可能。若是取,则剩余
不管取了还是不取,我们都对第u件物品做出了决策。我们需要在两个决策中选择一个最好的。所以我们需要知道两个决策的价值并比较。
对于取了的情况,我们得到的价值是剩余的空间取前 u-1 个物品的最大价值加上这件物品的价值。即
对于没有取的情况,我们得到的价值是目前的空间前 u-1 个物品的最大价值。即
最后,我们对两个决策之间选择一个最大值,就是 f(u, v) 的最优解:
这就是这道题的状态转移方程。
由于这道题是模板题,实际上所有的01背包方程都是基于这个方程推导出来的。
核心代码:
// C++ for(int i = 1; i <= N; i++) // 循环从1开始,可以避免处理越界行为,DP默认初始化0 { for(int j = 1; i <= M; j++) // 列举背包空间的情况 { if (j < D[i]) // 在实际代码中,会有无法取的情况,这种情况就是不取 { DP[i][j] = DP[i-1][j]; // 不取 } else { DP[i][j] = std::max(DP[i-1][j-D[i]] + W[i], DP[i-1][j]); // 状态转移方程 } } }
优化:
仔细观察状态转移方程。发现对于
假设有状态
优化后的核心代码:
for(int i = 1; i <= N; i++) { for(int j = M; j >= W[i]; j--) { DP[j] = std::max(DP[j], DP[j - D[i]] + W[i]); // 状态转移方程 } }
一些练习题:
[P1049 NOIP2001 普及组] 装箱问题 - 洛谷
[P1048 NOIP2005 普及组] 采药 - 洛谷
2.1.2 完全背包#
完全背包是背包问题的一种。它与01背包不同,01背包每类物品只能取一次,而完全背包每类物品能取无数次。
实际上,由于“背包容量”的限制,实际能取到的数量是有限的,这也是需要决策取舍的地方。
例题:
题目大意:
在总共
解题思路:
这是一个完全背包问题。我们将决策从取与不取转换到取多少。我们用
有状态
如果对第
特别地,如果没有取,则
状态转移方程:
核心代码:
for(int i = 1; i <= m; i++) { for(int j = 1; j <= t; j++) { for(int k = 0; k * A[i] <= j; k++) // 如果不能取就没有必要增加取的数量 { DP[i][j] = std::max(DP[i][j], DP[i-1][j-k*A[i]]+k*B[i]); // 状态转移方程 } } }
这是一个
实际上,对于
对于核心代码,与01背包的枚举顺序是相反的:
for(int i = 1; i <= m; i++) { for(int j = 0; j <= t - A[i]; j++) // 与01背包相反的枚举顺序 { DP[j + A[i]] = std::max(DP[j] + B[i], DP[j + A[i]]); // 状态转移方程 } }
时间复杂度
2.2 区间DP#
区间DP是对一个区间进行合并,求合并操作后的最优解。
区间DP最经典的例题就是石子合并。实际上这道题还可以进行四边形不等式优化,在这里不展开,参见后文DP优化。
例题:
题目大意:
在一个圆形操场的四周摆放
样例输入:
4 4 5 9 4
样例输出:
43 54
解题思路:
这是一道区间DP模板题。非常经典。我们先不考虑环形的问题。假设他是直线的。
对于这道题,有状态转移方程
仍然是表示状态转移方程。对于区间
我们使用枚举法:
其中:
然后设
随后我们设
如果你看到这里想要试一试,不妨尝试这道简化版无环:
如何处理环?
我们将原数组
核心代码:
for(int l = 2; l <= N; l ++) // 枚举所有可能的长度 { for(int i = 1; i <= 2 * N + 1 - l; i++) // 枚举所有的i { int j = l + i - 1; // 通过长度与i计算j for(int k = i; k < j; k++) // 枚举所有可能的k { DP[i][j] = std::max(DP[i][j], DP[i][k] + DP[k + 1][j] + sum[j] - sum[i - 1]); // 状态转移方程 } } }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix