洛谷上原文:动态规划
动态规划(简称“DP”)是 1957 年 理查德·贝尔曼(Richard Bellman)在 Dynamic Programming 一书中提出的一种表格处理方法。它将原问题分解为若干子问题,自底向上先求解最小的子问题,并把结果存储在表格中,在求解大的子问题时直接从表格中查询小的子问题的解,以避免重复计算,从而提高效率。
具备的要素
动态规划算法常用来求解最优化问题,尤其是带有多步决策的最优化问题。
能用动态规划解决的问题具备以下 3 个要素:
-
最优子结构: 如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构。也就是说一个问题的最优解只取决于其子问题的最优解。
-
无后效性: 将原问题分解为若干子问题,每个子问题的求解过程作为一个阶段,当前阶段的求解只与之前阶段有关,与之后阶段无关。即某阶段的状态一旦确定,就不受这个状态后续决策的影响。
-
重叠子问题: 求解过程中每次产生的子问题并不总是新问题,会有大量子问题重复。在遇到重复的子问题时,只需在表格中查询,无需再次求解。这个性质不是使用动态规划解决问题的必要条件,但凸显了动态规划的一大优势。
解题步骤
解DP题目的一般模式如下:
-
划分阶段: 按照问题特征,将问题划分为若干阶段。注意每个阶段是无后效性的。
-
状态表示: 将问题发展到各个阶段时所处于的各种情况用不同的阶段进行表示。比如:
dp[i]代表了……。 -
决策与动态转移方程: 在对问题的处理中做出的每种选择性的行动成为“决策”。而根据上一阶段的状态和决策来导出本阶段的状态就是状态转移。
-
边界条件: 需要一个递推的终止条件或边界条件。
-
答案:问题的求解目标。
例子实操
01背包问题
题目地址--P1048 [NOIP 2005 普及组] 采药
题目摘自洛谷 P1048。
1.题目大意
山上有 M 株草药,辰辰有 T 的时间去采药。
每株草药有 Wi 的价值,但采某株草药就会花费 Vi 的时间。
求在规定时间内,最多可以采到的草药最大价值。
2.状态表示(初步)
我们先将题目中的值进行表示。
题目中提到两个重要的值:即 V 和 W 。我们该如何对其进行表示?首先我们先定义一个二维数组来存储状态(即上文中的“表格”)。
设数组 dp[i][j] 为,考虑前 i 株草药,时间不超过 j 时能获得的最大价值。
由此,显而易见地,最终答案为 dp[M][T] ,也就是“考虑前 M 株草药,时间不超过 T ,能获得的最大价值”。
3.决策与动态转移方程
对于数组 dp ,我们不难发现有两种选择方式:
-
选择第 i 个物品;
-
不选第 i 个物品。
其中,“不选第 i 个物品”时,用 dp 数组的第一维表示为 dp[i−1][j] 。也就是根据上一个状态(未选择第 i 个物品)决定,这样的情况便能形成最优解。
先对 dp 数组进行初始化。根据上文定义 (dp[i][j] 为,考虑前 i 株草药,时间不超过 j 时能获得的最大价值),我们就可以把 dp[0][j] 和 dp[i][0] 初始为0,接着进行状态转移。
已知“不选第 i 个物品”的状态转移方程为dp[i][j]=dp[i-1][j],不妨推出“选第 i 个物品”的转移方程:
max(dp[i-1][j],dp[i-1][j-v[i]+w[i])
直接枚举 i 和 j 即可。
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.考虑优化
考虑将二维的 dp 数组优化成一维。
由于 dp[i][j] 只依赖于 dp[i−1][...] ,我们不妨把第一维优化掉。
j的遍历顺序要是反向,以避免内容的覆盖。
6.优化后代码
#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;
}
最长上升子序列
最长上升子序列(LIS)适用一维的动态规划来解决。
“最长上升子序列”是对于一个给定的长度为 n 的序列,求其单调递增的最长自序列的长度。
最长上升子序列是指,从原序列中按顺序取出一些数字排在一起,这些数字是逐渐增大的。
注:“子序列”可以是非连续的,但是顺序要与原序相同
1.状态设计
状态:dp[i] 表示以 a[i] 结尾的最长上升子序列的长度。
2.状态转移
dp[i]=1≤j<ia[j]<a[i]max(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.说明
说明:时间复杂度为 O(n2) ,遇到大的数据可能是会爆掉的。
你可以试着用 贪心、二分查找 等办法进行优化。由于本篇讲述的是 动态规划,所以不展示优化的代码。
或许你可以优化到 O(nlogn) 的级别。
推荐题目
不保证按照难度排序。
P1216 [IOI 1994] 数字三角形 Number Triangles
5765

被折叠的 条评论
为什么被折叠?



