本文是基本常见的动态规划题解

一、动态规划简介

0-1背包理论基础(一)

动规五部曲分析

  1. 确定dp数组以及下标的含义

那么这里 i 、j、dp[i][j] 分别表示什么呢?

i 来表示物品、j表示背包容量。

即****dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少****。

  1. 确定递推公式

以上过程,抽象化如下:

  • ****不放物品i****:背包容量为j,里面不放物品i的最大价值是dp[i - 1][j]。
  • ****放物品i****:背包空出物品i的容量后,背包容量为j - weight[i],dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]且不放物品i的最大价值,那么dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值

递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

  1. dp数组如何初始化

****关于初始化,一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱****。

如果背包容量j为0的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为0。

状态转移方程 dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 可以看出i 是由 i-1 推导出来,那么i为0的时候就一定要初始化。

dp[0][j],即:i为0,存放编号0的物品的时候,各个容量的背包所能存放的最大价值。

那么很明显当 j < weight[0]的时候,dp[0][j] 应该是 0,因为背包容量比编号0的物品重量还小。

j >= weight[0]时,dp[0][j] 应该是value[0],因为背包容量放足够放编号0物品。

dp[0][j] 和 dp[i][0] 都已经初始化了,那么其他下标应该初始化多少呢?

其实从递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 可以看出dp[i][j] 是由左上方数值推导出来了,那么 其他下标初始为什么数值都可以,因为都会被覆盖。

***初始-1,初始-2,初始100,都可以!*****

但只不过一开始就统一把dp数组统一初始为0,更方便一些

最后初始化代码如下:

1
2
3
4
5
// 初始化 dp
vector<vector<int>> dp(weight.size(), vector<int>(bagweight + 1, 0));
for (int j = weight[0]; j <= bagweight; j++) {
dp[0][j] = value[0];
}
  1. 确定遍历顺序

那么问题来了,***先遍历 物品还是先遍历背包重量呢?*****

****其实都可以!! 但是先遍历物品更好理解******。

为什么也是可以的呢?

****要理解递归的本质和递推的方向******。

dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 递归公式中可以看出dp[i][j]是靠dp[i-1][j]和dp[i - 1][j - weight[i]]推导出来的。

dp[i-1][j]和dp[i - 1][j - weight[i]] 都在dp[i][j]的左上角方向(包括正上方向)

***大家可以看出,虽然两个for循环遍历的次序不同,但是dp[i][j]所需要的数据就是左上角,根本不影响dp[i][j]公式的推导!*****

但先遍历物品再遍历背包这个顺序更好理解。

***其实背包问题里,两个for循环的先后循序是非常有讲究的,理解遍历顺序其实比理解推导公式难多了*****

  1. 举例推导dp数组

手动模拟一下

模板代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <bits/stdc++.h>

using namespace std;

int main(void) {
// n代表个数
// bagweight代表行李箱空间
int n, bagweight;
scanf("%d %d", &n, &bagweight);
vector<int> weight(n, 0); // 存储每件物品所占空间
vector<int> value(n, 0); // 存储每件物品价值
for (int i = 0; i < n; i++) {
scanf("%d", &weight[i]);
}
for (int i = 0; i < n; i++) {
scanf("%d", &value[i]);
}

// dp数组的初始化
// dp数组, dp[i][j]代表行李箱空间为j的情况下,从下标为[0, i]的物品里面任意取,能达到的最大价值
// 重点: 这里是 任意取得
vector<vector<int>> dp(n, vector<int>(bagweight + 1, 0));
for (int j = weight[0]; j < bagweight + 1; j++) {
dp[0][j] = value[0];
}

// 递推公式
// 因为前面只有横着初始化, 所以这里的剩余面积就是从i = 1, j = 0开始的(好理解背诵)
for (int i = 1; i < n; i++) {
for (int j = 0; j < bagweight + 1; j++) {
// 其实这里写成int j = 1;开始也可以.
// 但是还是遵从模板来写, j 从0 开始.
// 记忆口诀就是.前面有横着初始化了.
// 竖着初始化(i递增,j保持0),就放在这里
// 因此有一个j = 0, 又因为任何正整数都大于0,
// 所以自然想得起来j - weight[i] < 0导致的数组非法下标
// 自然而然想得起来有一个if(j < weight[i])的判断
if (j < weight[i]) {
dp[i][j] = dp[i - 1][j];
} else {
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
}

printf("%d", dp[n - 1][bagweight]);

return 0;
}

二、动态规划题解

300. 最长递增子序列

又叫做最长上升子序列

子序列一般是如下定义

“子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序”。

子串一般是连续的

当然也有特例, 不过特例题干都会明确标出来.例如说 连续的 子序列

  1. dp[i]的定义

***dp[i]表示i之前包括i的以nums[i]结尾的最长递增子序列的长度*****

  1. 状态转移方程

位置i的最长升序子序列等于j从0到i-1各个位置的最长升序子序列 + 1 的最大值。

所以:if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);

****注意这里不是要dp[i] 与 dp[j] + 1进行比较,而是我们要取dp[j] + 1的最大值******。

  1. dp[i]的初始化

每一个i,对应的dp[i](即最长递增子序列)起始大小至少都是1.

  1. 确定遍历顺序

dp[i] 是有0到i-1各个位置的最长递增子序列 推导而来,那么遍历i一定是从前向后遍历。

j其实就是遍历0到i-1,那么是从前到后,还是从后到前遍历都无所谓,只要吧 0 到 i-1 的元素都遍历了就行了。 所以默认习惯 从前向后遍历。

C++代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public:
int lengthOfLIS(vector<int> &nums) {
if (nums.size() <= 1) {
return nums.size();
}
vector<int> dp(nums.size(), 1);
int result = 0;
for (int i = 1; i < nums.size(); i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
dp[i] = max(dp[i], dp[j] + 1);
}
}
if (dp[i] > result) {
result = dp[i];
}
}
return result;
}
};

待更新……