动态规划
动态规划(简称“DP”)是 1957 年 理查德·贝尔曼(Richard Bellman)在 Dynamic Programming 一书中提出的一种表格处理方法。它将原问题分解为若干子问题,自底向上先求解最小的子问题,并把结果存储在表格中,在求解大的子问题时直接从表格中查询小的子问题的解,以避免重复计算,从而提高效率。
具备的要素
动态规划算法常用来求解最优化问题,尤其是带有多步决策的最优化问题。
能用动态规划解决的问题具备以下 3 个要素:
-
最优子结构: 如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构。也就是说一个问题的最优解只取决于其子问题的最优解。
-
无后效性: 将原问题分解为若干子问题,每个子问题的求解过程作为一个阶段,当前阶段的求解只与之前阶段有关,与之后阶段无关。即某阶段的状态一旦确定,就不受这个状态后续决策的影响。
-
重叠子问题: 求解过程中每次产生的子问题并不总是新问题,会有大量子问题重复。在遇到重复的子问题时,只需在表格中查询,无需再次求解。这个性质不是使用动态规划解决问题的必要条件,但凸显了动态规划的一大优势。
解题步骤
解DP题目的一般模式如下:
-
划分阶段: 按照问题特征,将问题划分为若干阶段。注意每个阶段是无后效性的。
-
状态表示: 将问题发展到各个阶段时所处于的各种情况用不同的阶段进行表示。比如:
dp[i]代表了……。 -
决策与动态转移方程: 在对问题的处理中做出的每种选择性的行动成为“决策”。而根据上一阶段的状态和决策来导出本阶段的状态就是状态转移。
-
边界条件: 需要一个递推的终止条件或边界条件。
-
答案:问题的求解目标。
例子实操
01背包问题
题目地址--P1048 [NOIP 2005 普及组] 采药
题目摘自洛谷 P1048。
1.题目大意
山上有
每株草药有
求在规定时间内,最多可以采到的草药最大价值。
2.状态表示(初步)
我们先将题目中的值进行表示。
题目中提到两个重要的值:即
设数组
由此,显而易见地,最终答案为
3.决策与动态转移方程
对于数组
-
选择第
i 个物品; -
不选第
i 个物品。
其中,“不选第
先对
已知“不选第 dp[i][j]=dp[i-1][j],不妨推出“选第
max(dp[i-1][j],dp[i-1][j-v[i]+w[i])
直接枚举
4.参考代码
代码仅供参考,AC记录
#include <iostream>
#include <algorithm>
using namespace std;
int main() {
int T, M;
cin >> T >> M;
int v[105], w[105];
for (int i = 1; i <= M; ++i) {
cin >> v[i] >> w[i];
}
int dp[105][1005] = {0};
for (int i = 1; i <= M; ++i) {
for (int j = 0; j <= T; ++j) {
if (j >= v[i]) {
dp[i][j] = max(dp[i-1][j], dp[i-1][j-v[i]] + w[i]);
} else {
dp[i][j] = dp[i-1][j];
}
}
}
cout << dp[M][T] << endl;
return 0;
}
5.考虑优化
考虑将二维的
由于
j的遍历顺序要是反向,以避免内容的覆盖。
6.优化后代码
优化后AC记录
#include <iostream>
#include <algorithm>
using namespace std;
int main() {
int T, M;
cin >> T >> M;
int v[105], w[105];
for (int i = 1; i <= M; ++i) {
cin >> v[i] >> w[i];
}
int dp[1005] = {0};
for (int i = 1; i <= M; ++i) {
for (int j = T; j >= v[i]; --j) { //逆序遍历避免覆盖
dp[j] = max(dp[j], dp[j-v[i]] + w[i]);
}
}
cout << dp[T] << endl;
return 0;
}
最长上升子序列
题目地址--B3637 最长上升子序列
最长上升子序列(LIS)适用一维的动态规划来解决。
“最长上升子序列”是对于一个给定的长度为 n 的序列,求其单调递增的最长自序列的长度。
最长上升子序列是指,从原序列中按顺序取出一些数字排在一起,这些数字是逐渐增大的。
注:“子序列”可以是非连续的,但是顺序要与原序相同
1.状态设计
状态:
2.状态转移
dp[i] = \max_{\substack{1 \leq j < i \\ a[j] < a[i]}} (dp[j]) + 1
条件:a[j] < a[i](保证上升)。
操作:在所有满足条件的 j 中,找到最大的 dp[j],然后加 1(因为 a[i] 可以接在 a[j] 后面)。
边界:如果没有任何 j 满足 a[j] < a[i],则 dp[i] = 1(a[i] 自身构成一个长度为 1 的上升子序列)。
3.参考核心代码
这里只展示核心代码。
dp[0]=0;
ans=0;
for(int i=1;i<n;i++) {
dp[i]=1;
for(int j=1;j<=i-1;j++) {
if(a[j]<a[i])
dp[i]=max(dp[i],dp[j]+1);
}
if(dp[i]>ans)
ans=dp[i];
}
4.说明
说明:时间复杂度为
你可以试着用 贪心、二分查找 等办法进行优化。由于本篇讲述的是 动态规划,所以不展示优化的代码。
或许你可以优化到
推荐题目
不保证按照难度排序。
P1048 [NOIP 2005 普及组] 采药
B3637 最长上升子序列
P1216 [IOI 1994] 数字三角形 Number Triangles
P1002 [NOIP 2002 普及组] 过河卒
P1020 [NOIP 1999 提高组] 导弹拦截
P1091 [NOIP 2004 提高组] 合唱队形
P1880 [NOI1995] 石子合并
P1063 [NOIP 2006 提高组] 能量项链
P1060 [NOIP 2006 普及组] 开心的金明
P1064 [NOIP 2006 提高组] 金明的预算方案
P7074 [CSP-J2020] 方格取数
P8816 [CSP-J 2022] 上升点列
推荐学习资料
oi-wiki--《动态规划基础》
oi-wiki--《背包DP》
oi-wiki--《树形DP》
本篇字数:约 5000 字
完成时间:2025/8/16 10:07