动态规划

动态规划

动态规划比较适合用来求解最优问题

动态规划解决问题的思路:

把问题分解为多个阶段,每个阶段对应一个决策。记录每一个阶段可达的状态集合(去掉重复的),然后通过当前阶段的状态集合,来推导下一个阶段的状态集合,动态地往前推进。

0-1 背包问题

对于一组不同重量、不可分割的物品,需要选择一些装入背包,在满足背包最大重量限制的前提下,背包中物品总重量的最大值是多少呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 回溯算法实现。注意:我把输入的变量都定义成了成员变量。
private int maxW = Integer.MIN_VALUE; // 结果放到maxW中
private int[] weight = {22463}; // 物品重量
private int n = 5; // 物品个数
private int w = 9; // 背包承受的最大重量
public void f(int i, int cw) { // 调用f(0, 0)
if (cw == w || i == n) { // cw==w表示装满了,i==n表示物品都考察完了
if (cw > maxW) maxW = cw;
return;
}
f(i+1, cw); // 选择不装第i个物品
if (cw + weight[i] <= w) {
f(i+1,cw + weight[i]); // 选择装第i个物品
}
}

递归树中的每个节点表示一种状态,用(i, cw)来表示。其中,i (从0开始)表示将要决策第几个物品是否装入背包,cw 表示当前背包中物品的总重量。比如,(2,2)表示我们将要决策第 2 个物品是否装入背包,在决策前,背包中物品的总重量是 2。

有些子问题的求解是重复的,记录已经计算好的 f(i, cw),当再次计算到重复的 f(i, cw) 的时候,可以直接从备忘录中取出来用,就不用再递归计算了,这样就可以避免冗余计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private int maxW = Integer.MIN_VALUE; // 结果放到maxW中
private int[] weight = {22463}; // 物品重量
private int n = 5; // 物品个数
private int w = 9; // 背包承受的最大重量
private boolean[][] mem = new boolean[5][10]; // 备忘录,默认值false
public void f(int i, int cw) { // 调用f(0, 0)
if (cw == w || i == n) { // cw==w表示装满了,i==n表示物品都考察完了
if (cw > maxW) maxW = cw;
return;
}
if (mem[i][cw]) return; // 重复状态
mem[i][cw] = true; // 记录(i, cw)这个状态
f(i+1, cw); // 选择不装第i个物品
if (cw + weight[i] <= w) {
f(i+1,cw + weight[i]); // 选择装第i个物品
}
}

动态规划

把整个求解过程分为 n 个阶段,每个阶段会决策一个物品是否放到背包中。每个物品决策(放入或者不放入背包)完之后,背包中的物品的重量会有多种情况,也就是说,会达到多种不同的状态,对应到递归树中,就是有很多不同的节点。

把每一层重复的状态(节点)合并,只记录不同的状态,然后基于上一层的状态集合,来推导下一层的状态集合。可以通过合并每一层重复的状态,这样就保证每一层不同状态的个数都不会超过 w 个(w 表示背包的承载重量),就成功避免了每层状态个数的指数级增长。

用一个二维数组 states[n][w+1],来记录每层可以达到的不同状态。

第 0 个(下标从 0 开始编号)物品的重量是 2,要么装入背包,要么不装入背包,决策完之后,会对应背包的两种状态,背包中物品的总重量是 0 或者 2。用 states[0][0]=true 和 states[0][2]=true 来表示这两种状态。

第 1 个物品的重量也是 2,基于之前的背包状态,在这个物品决策完之后,不同的状态有 3 个,背包中物品总重量分别是 0(0+0),2(0+2 or 2+0),4(2+2)。用 states[1][0]=true,states[1][2]=true,states[1][4]=true 来表示这三种状态。

以此类推,直到考察完所有的物品后,整个 states 状态数组就都计算好了。图中 0 表示 false,1 表示 true。只需要在最后一层,找一个值为 true 的最接近 w(这里是 9)的值,就是背包中物品总重量的最大值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//weight:物品重量,n:物品个数,w:背包可承载重量
public int knapsack(int[] weight, int n, int w) {
boolean[][] states = new boolean[n][w+1]; // 默认值false
states[0][0] = true; // 第一行的数据要特殊处理,可以利用哨兵优化
if (weight[0] <= w) {
states[0][weight[0]] = true;
}
for (int i = 1; i < n; ++i) { // 动态规划状态转移
for (int j = 0; j <= w; ++j) {// 不把第i个物品放入背包
if (states[i-1][j] == true) states[i][j] = states[i-1][j];
}
for (int j = 0; j <= w-weight[i]; ++j) {//把第i个物品放入背包
if (states[i-1][j]==true) states[i][j+weight[i]] = true;
}
}
for (int i = w; i >= 0; --i) { // 输出结果
if (states[n-1][i] == true) return i;
}
return 0;
}

时间复杂度:耗时最多的部分就是代码中的两层 for 循环,所以时间复杂度是 O(n*w)。n 表示物品个数,w 表示背包可以承载的总重量。

动态规划优化

需要额外申请一个 n 乘以 w+1 的二维数组,对空间的消耗比较多。需要一个大小为 w+1 的一维数组就可以解决这个问题。动态规划状态转移的过程,都可以基于这个一维数组来操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static int knapsack2(int[] items, int n, int w) {
boolean[] states = new boolean[w+1]; // 默认值false
states[0] = true; // 第一行的数据要特殊处理,可以利用哨兵优化
if (items[0] <= w) {
states[items[0]] = true;
}
for (int i = 1; i < n; ++i) { // 动态规划
for (int j = w-items[i]; j >= 0; --j) {//把第i个物品放入背包
if (states[j]==true) states[j+items[i]] = true;
}
}
for (int i = w; i >= 0; --i) { // 输出结果
if (states[i] == true) return i;
}
return 0;
}

0-1 背包问题升级版

引入物品价值这一变量

对于一组不同重量、不同价值、不可分割的物品,选择将某些物品装入背包,在满足背包最大重量限制的前提下,背包中可装入物品的总价值最大是多少呢?

回溯算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private int maxV = Integer.MIN_VALUE; // 结果放到maxV中
private int[] items = {22463}; // 物品的重量
private int[] value = {34896}; // 物品的价值
private int n = 5; // 物品个数
private int w = 9; // 背包承受的最大重量
public void f(int i, int cw, int cv) { // 调用f(0, 0, 0)
if (cw == w || i == n) { // cw==w表示装满了,i==n表示物品都考察完了
if (cv > maxV) maxV = cv;
return;
}
f(i+1, cw, cv); // 选择不装第i个物品
if (cw + weight[i] <= w) {
f(i+1,cw+weight[i], cv+value[i]); // 选择装第i个物品
}
}

在递归树中,每个节点表示一个状态。现在需要 3 个变量(i, cw, cv)来表示一个状态。其中,i 表示即将要决策第 i 个物品是否装入背包,cw 表示当前背包中物品的总重量,cv 表示当前背包中物品的总价值。

在递归树中,有几个节点的 i 和 cw 是完全相同的,比如 f(2,2,4) 和 f(2,2,3)。在背包中物品总重量一样的情况下,f(2,2,4) 这种状态对应的物品总价值更大,可以舍弃 f(2,2,3) 这种状态,只需要沿着 f(2,2,4) 这条决策路线继续往下决策就可以。

对于 (i, cw) 相同的不同状态,只需要保留 cv 值最大的那个,继续递归处理,其他状态不予考虑。

动态规划

把整个求解过程分为 n 个阶段,每个阶段会决策一个物品是否放到背包中。每个阶段决策完之后,背包中的物品的总重量以及总价值,会有多种情况,也就是会达到多种不同的状态。

用一个二维数组 states[n][w+1],来记录每层可以达到的不同状态。不过这里数组存储的值不再是 boolean 类型的了,而是当前状态对应的最大总价值。我们把每一层中 (i, cw) 重复的状态(节点)合并,只记录 cv 值最大的那个状态,然后基于这些状态来推导下一层的状态。

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
public static int knapsack3(int[] weight, int[] value, int n, int w) {
int[][] states = new int[n][w+1];
for (int i = 0; i < n; ++i) { // 初始化states
for (int j = 0; j < w+1; ++j) {
states[i][j] = -1;
}
}
states[0][0] = 0;
if (weight[0] <= w) {
states[0][weight[0]] = value[0];
}
for (int i = 1; i < n; ++i) { //动态规划,状态转移
for (int j = 0; j <= w; ++j) { // 不选择第i个物品
if (states[i-1][j] >= 0) states[i][j] = states[i-1][j];
}
for (int j = 0; j <= w-weight[i]; ++j) { // 选择第i个物品
if (states[i-1][j] >= 0) {
int v = states[i-1][j] + value[i];
if (v > states[i][j+weight[i]]) {
states[i][j+weight[i]] = v;
}
}
}
}
// 找出最大值
int maxvalue = -1;
for (int j = 0; j <= w; ++j) {
if (states[n-1][j] > maxvalue) maxvalue = states[n-1][j];
}
return maxvalue;
}

动态规划:一个模型三个特征

  1. 多阶段决策最优解模型

    一般是用动态规划来解决最优问题。而解决问题的过程,需要经历多个决策阶段。每个决策阶段都对应着一组状态。然后寻找一组决策序列,经过这组决策序列,能够产生最终期望求解的最优值。

  2. 最优子结构特征

    最优子结构指的是,问题的最优解包含子问题的最优解。

    可以通过子问题的最优解,推导出问题的最优解。后面阶段的状态可以通过前面阶段的状态推导出来。

  3. 无后效性特征

    1. 在推导后面阶段的状态的时候,只关心前面阶段的状态值,不关心这个状态是怎么一步一步推导出来的
    2. 某阶段状态一旦确定,就不受之后阶段的决策影响
  4. 重复子问题

    不同的决策序列,到达某个相同的阶段时,可能会产生重复的状态

“一个模型三个特征”实例剖析

假设有一个 n 乘以 n 的矩阵 w[n][n]。矩阵存储的都是正整数。棋子起始位置在左上角,终止位置在右下角。将棋子从左上角移动到右下角。每次只能向右或者向下移动一位。从左上角到右下角,会有很多不同的路径可以走。把每条路径经过的数字加起来看作路径的长度。那从左上角移动到右下角的最短路径长度是多少呢?

  1. 一个模型

    从 (0, 0) 走到 (n-1, n-1),总共要走 2*(n-1) 步,也就对应着 2*(n-1) 个阶段。每个阶段都有向右走或者向下走两种决策,并且每个阶段都会对应一个状态集合。

    把状态定义为 min_dist(i, j),其中 i 表示行,j 表示列。min_dist 表达式的值表示从 (0, 0) 到达 (i, j) 的最短路径长度。所以,这个问题是一个多阶段决策最优解问题,符合动态规划的模型。

  2. 三个特征

    如果我们走到 (i, j) 这个位置,我们只能通过 (i-1, j),(i, j-1) 这两个位置移动,也就是说,想要计算 (i, j) 位置对应的状态,只需要关心 (i-1, j),(i, j-1) 两个位置对应的状态,并不关心棋子是通过什么样的路线到达这两个位置的。所以,前面阶段的状态确定之后,不会被后面阶段的决策所改变,所以,这个问题符合“无后效性”这一特征。

    定义状态的时候,从起始位置 (0, 0) 到 (i, j) 的最小路径,记作 min_dist(i, j)。只有可能从 (i, j-1) 或者 (i-1, j) 两个位置到达 (i, j)。也就是说,到达 (i, j) 的最短路径要么经过 (i, j-1),要么经过 (i-1, j),而且到达 (i, j) 的最短路径肯定包含到达这两个位置的最短路径之一。换句话说就是,min_dist(i, j) 可以通过 min_dist(i, j-1) 和 min_dist(i-1, j) 两个状态推导出来。这就说明,这个问题符合“最优子结构”。

    1
    min_dist(i, j) = w[i][j] + min(min_dist(i, j-1), min_dist(i-1, j))

动态规划解题思路

状态转移表法

状态表一般都是二维的,所以你可以把它想象成二维数组。其中,每个状态包含三个变量,行、列、数组值。根据决策的先后过程,从前往后,根据递推关系,分阶段填充状态表中的每个状态。最后,将这个递推填表的过程,翻译成代码,就是动态规划代码了。

矩阵最短路径问题

画出一个二维状态表,表中的行、列表示棋子所在的位置,表中的数值表示从起点到这个位置的最短路径。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public int minDistDP(int[][] matrix, int n) {
int[][] states = new int[n][n];
int sum = 0;
for (int j = 0; j < n; ++j) { // 初始化states的第一行数据
sum += matrix[0][j];
states[0][j] = sum;
}
sum = 0;
for (int i = 0; i < n; ++i) { // 初始化states的第一列数据
sum += matrix[i][0];
states[i][0] = sum;
}
for (int i = 1; i < n; ++i) {
for (int j = 1; j < n; ++j) {
states[i][j] =
matrix[i][j] + Math.min(states[i][j-1], states[i-1][j]);
}
}
return states[n-1][n-1];
}

状态转移方程法

状态转移方程法有点类似递归的解题思路。需要分析,某个问题如何通过子问题来递归求解,也就是所谓的最优子结构。根据最优子结构,写出递归公式,也就是所谓的状态转移方程。

矩阵最短路径问题

状态转移方程:

1
min_dist(i, j) = w[i][j] + min(min_dist(i, j-1), min_dist(i-1, j))

状态转移方程是解决动态规划的关键

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private int[][] matrix = 
{{1359}, {2134},{5267},{6843}};
private int n = 4;
private int[][] mem = new int[4][4];
public int minDist(int i, int j) { // 调用minDist(n-1, n-1);
if (i == 0 && j == 0) return matrix[0][0];
if (mem[i][j] > 0) return mem[i][j];
int minLeft = Integer.MAX_VALUE;
if (j-1 >= 0) {
minLeft = minDist(i, j-1);
}
int minUp = Integer.MAX_VALUE;
if (i-1 >= 0) {
minUp = minDist(i-1, j);
}

int currMinDist = matrix[i][j] + Math.min(minLeft, minUp);
mem[i][j] = currMinDist;
return currMinDist;
}

动态规划实战

如何实现搜索引擎中的拼写纠错功能?

在搜索框中,一不小心输错单词时,搜索引擎会非常智能地检测出拼写错误,并且用对应的正确单词来进行搜索

量化两个字符串的相似度

编辑距离(Edit Distance)

编辑距离指的就是,将一个字符串转化成另一个字符串,需要的最少编辑操作次数(比如增加一个字符、删除一个字符、替换一个字符)。编辑距离越大,说明两个字符串的相似程度越小;相反,编辑距离就越小,说明两个字符串的相似程度越大。对于两个完全相同的字符串来说,编辑距离就是 0。

编辑距离有多种不同的计算方式,比较著名的有莱文斯坦距离(Levenshtein distance)和最长公共子串长度(Longest common substring length)。

其中,莱文斯坦距离允许增加、删除、替换字符这三个编辑操作,莱文斯坦距离的大小,表示两个字符串差异的大小;最长公共子串长度只允许增加、删除字符这两个编辑操作,最长公共子串的大小,表示两个字符串相似程度的大小。

计算莱文斯坦距离

回溯

回溯是一个递归处理的过程。如果 a[i]与 b[j]匹配,递归考察 a[i+1]和 b[j+1]。如果 a[i]与 b[j]不匹配:

  1. 可以删除 a[i],然后递归考察 a[i+1]和 b[j];
  2. 可以删除 b[j],然后递归考察 a[i]和 b[j+1];
  3. 可以在 a[i]前面添加一个跟 b[j]相同的字符,然后递归考察 a[i]和 b[j+1];
  4. 可以在 b[j]前面添加一个跟 a[i]相同的字符,然后递归考察 a[i+1]和 b[j];
  5. 可以将 a[i]替换成 b[j],或者将 b[j]替换成 a[i],然后递归考察 a[i+1]和 b[j+1]。
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
private char[] a = "mitcmu".toCharArray();
private char[] b = "mtacnu".toCharArray();
private int n = 6;
private int m = 6;
private int minDist = Integer.MAX_VALUE; // 存储结果
// 调用方式 lwstBT(0, 0, 0);
public lwstBT(int i, int j, int edist) {
if (i == n || j == m) {
if (i < n) edist += (n-i);
if (j < m) edist += (m - j);
if (edist < minDist) minDist = edist;
return;
}
if (a[i] == b[j]) { // 两个字符匹配
lwstBT(i+1, j+1, edist);
} else { // 两个字符不匹配
lwstBT(i + 1, j, edist + 1); // 删除a[i]或者b[j]前添加一个字符
lwstBT(i, j + 1, edist + 1); // 删除b[j]或者a[i]前添加一个字符
lwstBT(i + 1, j + 1, edist + 1); // 将a[i]和b[j]替换为相同字符
}
}
private char[] a = "mitcmu".toCharArray();
private char[] b = "mtacnu".toCharArray();
private int n = 6;
private int m = 6;
private int minDist = Integer.MAX_VALUE; // 存储结果
// 调用方式 lwstBT(0, 0, 0);
public lwstBT(int i, int j, int edist) {
if (i == n || j == m) {
if (i < n) edist += (n-i);
if (j < m) edist += (m - j);
if (edist < minDist) minDist = edist;
return;
}
if (a[i] == b[j]) { // 两个字符匹配
lwstBT(i+1, j+1, edist);
} else { // 两个字符不匹配
lwstBT(i + 1, j, edist + 1); // 删除a[i]或者b[j]前添加一个字符
lwstBT(i, j + 1, edist + 1); // 删除b[j]或者a[i]前添加一个字符
lwstBT(i + 1, j + 1, edist + 1); // 将a[i]和b[j]替换为相同字符
}
}

在递归树中,每个节点代表一个状态,状态包含三个变量 (i, j, edist),其中,edist 表示处理到 a[i]和 b[j]时,已经执行的编辑操作的次数。

在递归树中,(i, j) 两个变量重复的节点很多。对于 (i, j) 相同的节点,只需要保留 edist 最小的,继续递归处理就可以了,剩下的节点都可以舍弃。所以,状态就从 (i, j, edist) 变成了 (i, j, min_edist),其中 min_edist 表示处理到 a[i]和 b[j],已经执行的最少编辑次数。

动态规划

状态转移方程

1
2
3
4
5
6
7
如果:a[i]!=b[j],那么:min_edist(i, j)就等于:
min(min_edist(i-1,j)+1, min_edist(i,j-1)+1, min_edist(i-1,j-1)+1)

如果:a[i]==b[j],那么:min_edist(i, j)就等于:
min(min_edist(i-1,j)+1, min_edist(i,j-1)+1,min_edist(i-1,j-1))

其中,min表示求三数中的最小值。
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
public int lwstDP(char[] a, int n, char[] b, int m) {
int[][] minDist = new int[n][m];
for (int j = 0; j < m; ++j) { // 初始化第0行:a[0..0]与b[0..j]的编辑距离
if (a[0] == b[j]) minDist[0][j] = j;
else if (j != 0) minDist[0][j] = minDist[0][j-1]+1;
else minDist[0][j] = 1;
}
for (int i = 0; i < n; ++i) { // 初始化第0列:a[0..i]与b[0..0]的编辑距离
if (a[i] == b[0]) minDist[i][0] = i;
else if (i != 0) minDist[i][0] = minDist[i-1][0]+1;
else minDist[i][0] = 1;
}
for (int i = 1; i < n; ++i) { // 按行填表
for (int j = 1; j < m; ++j) {
if (a[i] == b[j]) minDist[i][j] = min(
minDist[i-1][j]+1, minDist[i][j-1]+1, minDist[i-1][j-1]);
else minDist[i][j] = min(
minDist[i-1][j]+1, minDist[i][j-1]+1, minDist[i-1][j-1]+1);
}
}
return minDist[n-1][m-1];
}

private int min(int x, int y, int z) {
int minv = Integer.MAX_VALUE;
if (x < minv) minv = x;
if (y < minv) minv = y;
if (z < minv) minv = z;
return minv;
}

计算最长公共子串长度

动态规划

状态转移方程

每个状态包括三个变量 (i, j, max_lcs),max_lcs 表示 a[0…i]和 b[0…j]的最长公共子串长度。

  1. 如果 a[i]与 b[j]互相匹配,将最大公共子串长度加一,并且继续考察 a[i+1]和 b[j+1]。
  2. 如果 a[i]与 b[j]不匹配,最长公共子串长度不变
    1. 删除 a[i],或者在 b[j]前面加上一个字符 a[i],然后继续考察 a[i+1]和 b[j];
    2. 删除 b[j],或者在 a[i]前面加上一个字符 b[j],然后继续考察 a[i]和 b[j+1]。
1
2
3
4
5
6
7
如果:a[i]==b[j],那么:max_lcs(i, j)就等于:
max(max_lcs(i-1,j-1)+1, max_lcs(i-1, j), max_lcs(i, j-1));

如果:a[i]!=b[j],那么:max_lcs(i, j)就等于:
max(max_lcs(i-1,j-1), max_lcs(i-1, j), max_lcs(i, j-1));

其中max表示求三数中的最大值。
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
public int lcs(char[] a, int n, char[] b, int m) {
int[][] maxlcs = new int[n][m];
for (int j = 0; j < m; ++j) {//初始化第0行:a[0..0]与b[0..j]的maxlcs
if (a[0] == b[j]) maxlcs[0][j] = 1;
else if (j != 0) maxlcs[0][j] = maxlcs[0][j-1];
else maxlcs[0][j] = 0;
}
for (int i = 0; i < n; ++i) {//初始化第0列:a[0..i]与b[0..0]的maxlcs
if (a[i] == b[0]) maxlcs[i][0] = 1;
else if (i != 0) maxlcs[i][0] = maxlcs[i-1][0];
else maxlcs[i][0] = 0;
}
for (int i = 1; i < n; ++i) { // 填表
for (int j = 1; j < m; ++j) {
if (a[i] == b[j]) maxlcs[i][j] = max(
maxlcs[i-1][j], maxlcs[i][j-1], maxlcs[i-1][j-1]+1);
else maxlcs[i][j] = max(
maxlcs[i-1][j], maxlcs[i][j-1], maxlcs[i-1][j-1]);
}
}
return maxlcs[n-1][m-1];
}

private int max(int x, int y, int z) {
int maxv = Integer.MIN_VALUE;
if (x > maxv) maxv = x;
if (y > maxv) maxv = y;
if (z > maxv) maxv = z;
return maxv;
}

背包问题总结

01背包

dp[i][j]的含义:从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少?

不放物品i:由dp[i - 1][j]推出,即背包容量为j,里面不放物品i的最大价值,此时dp[i][j]就是dp[i - 1][j]。(其实就是当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以被背包内的价值依然和前面相同。)

放物品i:由dp[i - 1][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]);


dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]

dp[j]可以通过dp[j - weight[i]]推导出来,dp[j - weight[i]]表示容量为j - weight[i]的背包所背的最大价值。

dp[j - weight[i]] + value[i] 表示 容量为 j - 物品i重量 的背包 加上 物品i的价值。(也就是容量为j的背包,放入物品i了之后的价值即:dp[j])

此时dp[j]有两个选择,一个是取自己dp[j] 相当于 二维dp数组中的dp[i-1][j],即不放物品i,一个是取dp[j - weight[i]] + value[i],即放物品i,指定是取最大的,毕竟是求最大价值,

dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);


  1. 当动态规划数组为二维数组时–先遍历物品 再遍历背包无所谓

  2. 当动态规划数组为一维数组时–先遍历物品再遍历背包,并且逆序遍历背包

完全背包

对于完全背包,不论动态规划数组为一维还是二维,对物品遍历与背包遍历的顺序都是无所谓的

背包问题排列组合问题

之前的遍历顺序不受凑成总和的元素的元素的顺序影响,而排列组合问题不同,求的是凑成总和的方法种类

如果求组合数就是外层for循环遍历物品,内层for遍历背包

如果求排列数就是外层for遍历背包,内层for循环遍历物品

查看评论