动态规划
基本思想
动态规划方法的基本思想是,把求解的问题分成许多阶段或多个子问题,然后按顺序求解各子问题。最后一个子问题就是初始问题的解。
由于动态规划的问题有重叠子问题的特点,为了减少重复计算,对每一个子问题只解一次,将其不同阶段的不同状态保存在一个二维数组中。
动态规划=贪婪策略+递推(降阶)+存储递推结果
空间换取时间
- 递归算法效率低的主要原因是因为进行了大量的重复计算。而动态规划的基本动机就是充分利用重叠子问题(Overlapping subproblems)。
- 因为动态规划将以前(子问题)计算过的结果都记录下来,遇到使用子问题结果的时候只需查表。
- 动态规划是一种用空间换取时间的方法。
因此,动态规划常常因为空间消耗太大而难以实现。
主要概念
- 阶段:把问题分成几个相互联系的有顺序的几个环节,这些环节即称为阶段。
- 状态:某一阶段的出发位置称为状态。通俗地说状态是对问题在某一时刻的进展情况的数学描述。
- 决策:从某阶段的一个状态演变到下一个阶段某状态的选择。
- 状态转移方程:根据上一阶段的状态和决策导出本阶段的状态。这就像是“递推”。
例: 数塔问题
从顶部出发,在每一结点可以选择向左走或是向右走,一直走到底层,要求找出一条路径,使路径上的数值和最大。
- 阶段:每行就是一个阶段;
- 状态:d[i][j],即取第i行,第j个数能够达到的最大值;
- 决策:取第i行第j个数,则可以有两种方案:取第i-1行第j-1个数或取第i-1行第j个数后再取第i行第j个数;
- 状态转移方程:
d[i][j] = max (d[i+1][j],d[i+1][j+1]) + data[i][j];
表示取第i行第j个数所能达到的最大和;
1 | int a[100][100], f[100][100]; |
适合解决的问题的性质
- 动态规划算法的问题及决策应该具有两个性质:最优化原理、无后向性。
- 最优化原理(或称为最佳原则、最优子结构);
- 无后向性(无后效性):某阶段状态一旦确定以后,就不受这个状态以后决策的影响。即某状态以后的过程不会影响以前的状态,只与当前状态有关。
- 能够体现动态规划优点的性质:
- 子问题重叠性质;
- 动态规划用空间换取时间,在有大量重叠子问题的时候其优势才能充分体现出来。
例:数塔问题
- 最优化原理(最优子结构)
9->12->10->18->10
显然12->10->18->10也是12到最后一层的最大和…… - 无后效性
如,计算到12的最大和只要考虑到10的最大和与到6的最大和哪个更大,而不要考虑到10的最大和或者到6的最大和具体是哪几个数构成的。设计步骤
设计动态规划算法的基本步骤
设计一个标准的动态规划算法的步骤:
- 划分阶段;
- 选择状态;
- 确定决策并写出状态转移方程。
实际应用当中的简化步骤:
- 分析最优解的性质,并刻划其结构特征。
- 递推地定义最优值。
- 以自底向上的方式或自顶向下的记忆化方法(备忘录法)计算出最优值。
- 根据计算最优值时得到的信息,构造问题的最优解。
例题: 数塔问题
//从顶部出发,在每一结点可以选择向左走或是向右走,一直走到底层,要求找出一条路径,使路径上的数值和最大。
1 | public class Test1 { |
1 | public class Test1 { |
资源分配问题
设有资源n(n为整数),分配给m个项目,gi(x)为第i个项目分得资源x(x为整数)所得到的利润。
求总利润最大的资源分配方案,也就是解下列问题:
1
2max z=g1(x1)+ g2(x2)+……gm(xm)
x1+x2+x3+……xm = n,0≤xi≤n,i=1,2,3,……,m函数gi(x)以数据表的形式给出。
例如:现有7万元投资到A,B,C 三个项目,利润见表,问题:求总利润最大的资源分配方案。
- 划分阶段或找到子问题;
每个阶段增加一个项目,考察投资利润情况。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
50
51
52
53
54
55
56
57
58
59
60
public class Test2 {
static double[][] v = {
{0, 0.11, 0.13, 0.15, 0.21, 0.24, 0.3, 0.35},
{0, 0.12, 0.16, 0.21, 0.23, 0.25, 0.24, 0.34},
{0, 0.08, 0.12, 0.2, 0.24, 0.26, 0.3, 0.35}};
static int m = 3;
static int n = 7;
public static void main(String[] args) {
double[] q = new double[10]; //原始数据(逐行使用)
double[] f = new double[10];//当前最大收益情况
double[] temp = new double[10]; //正在计算的最大收益
int[][] a = new int[10][10]; //前i个项目投资j获得最大利润时,给第i个项目分配的资源数(m*(n+1)维)
int[] gain = new int[10];//在不同投资数下获最大利润时第i个工程所得资源数。
// 全部资源用于A(第一个项目)
q = f = v[0];
for (int j = 0; j <= n; j++) {
a[1][j] = j;
}
// 第二阶段: 全部资源用于两个项目
// 第三阶段: 全部资源用于三个项目
for (int k = 2; k <= m; k++) {
for (int j = 0; j <= n; j++) {
temp[j] = q[j];
a[k][j] = 0;
}
//赋值, 第二/三个项目的收益情况 (数组下标从0开始,故此处k-1)
q = v[k - 1];
for (int j = 0; j <= n; j++) {
for (int i = 0; i <= j; i++) {
if (f[j - i] + q[i] > temp[j]) {
temp[j] = f[j - i] + q[i];
a[k][j] = i;
}
}
}
// 更新 当前最大收益情况
for (int j = 0; j <= n; j++)
f[j] = temp[j];
}
int rest = n;
for (int i = m; i >= 1; i--) {
gain[i] = a[i][rest];
rest = rest - gain[i];
}
for (int i = 1; i <= m; i++) {
System.out.print(gain[i] + " ");
}
System.out.println((f[n]));
}
}
资源分配问题二
资源分配问题是将数量一定的一种或若干种资源(原木料、资金、设备或劳动力等)合理地分配给若干个使用者,使总收益最大。
例如,某公司有3个商店A、B、C,拟将新招聘的5名员工分配给这3个商店,各商店得到新员工后每年的赢利情况如表所示,求分配给各商各多少员工才能使公司的赢利最大?
解析:
其实就是完全背包的变形
用dp[i][j]表示为前i个商店共分配j个人时盈利的最大值,状态转移方程如下(k表示为i商店分配的人数):
dp[i][j] = max(dp[i][j], dp[i-1][j-k] + c[i][k])
1 | package com.example.demo; |
1 | package com.example.demo; |
0-1背包问题(knapsack problem)
一个小偷面前有一堆(n个)财宝,每个财宝有重量w和价值v两种属性,而他的背包只能携带一定重量的财宝(Capacity),在已知所有财宝的重量和价值的情况下,如何选取财宝,可以最大限度的利用当前的背包容量,取得最大价值的财宝(或求出能够获取财宝价值的最大值)。
- “填二维表”的动态规划方法
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
26public class Test4 {
static int MAX = 20;
static int[] value = {0, 1, 6, 18, 22, 28}; //物品价值
static int[] weight = {0, 1, 2, 5, 6, 7}; //物品重量
static int[][] dp = new int[MAX][MAX]; // dp[i][j]表示容量为j, 前i个物品的总价值。
static int[] result = new int[MAX]; // 选择哪些物品
static int n = 5;//五种物品
static int C = 11; //背包容量
public static void main(String[] args) {
//背包容量从 1 开始递增 到 11
for (int i = 1; i <= 5; i++) {
for (int j = 1; j <= C; j++) {
if (weight[i] > j) {
dp[i][j] = dp[i - 1][j];
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
}
System.out.println(dp[n][C]);
}
} - 空间优化 / 填一维表”的动态规划方法
https://blog.csdn.net/weixin_41162823/article/details/87878853
dp[j] = max(dp[j], dp[j−w[i]]+v[i])
上述状态表示,我们需要用二维数组,但事实上我们只需要一维的滚动数组就可以递推出最终答案。考虑到用dp[j]来保存每层递归的值,由于我们求dp[i][j] 的时候需要用到的是dp[ i-1 ][j] 和 dp[i-1][j-w[i]] 于是可以知道,
只要我们在求 dp[j] 时不覆盖 dp[ j - w[i] ],那么就可以不断递推至所求答案。所以我们采取倒序循环,即 v = m(m为背包总容积)伪代码如下:
1 | for i = 1..N |
完全背包问题:
完全背包(unbounded knapsack problem)与01背包不同就是每种物品可以有无限多个:一共有N种物品,每种物品有无限多个,
第i(i从1开始)种物品的重量为w[i],价值为v[i]。
在总重量不超过背包承载上限 W 的情况下,能够装入背包的最大价值是多少?
分析一
跟01背包一样,完全背包也是一个很经典的动态规划问题,不同的地方在于01背包问题中,每件物品最多选择一件,
而在完全背包问题中,只要背包装得下,每件物品可以选择任意多件。从每件物品的角度来说,与之相关的策略已经不再是选或者不选了,而是有取0件、取1件、取2件…直到取⌊T/Vi⌋(向下取整)件。
1 | k为装入第i种物品的件数, k <= j/w[i] |
我们通过对0/1背包的思路加以改进,就得到了完全背包的一种解法,这种解法时间复杂度为O(n^3),空间复杂度为O(n^2)。
时间优化
时间优化:根据上述 dp[i][j]
的定义,其为前 i 种物品恰好放入容量为 v 的背包的最大权值。根据上述状态转移方程可知,
我们假设的是子结果dp[i-1][j-k*w[i]]
中并没有选入第 i 种物品,所以我们需要逆序遍历(像0/1背包一样)来确保该前提;
但是我们现在考虑“加选一件第 i 种物品”这种策略时,正需要一个可能已经选入第 i 种物品的子结果dp[i-1][j-k*w[i]]
,于是当我们顺序遍历时,就刚好达到该要求。这种做法,使我们省去了一层循环,即第 i 种物品放入的件数k,从而时间复杂度优化为O(n^2)。
空间优化:
正如0/1背包的空间优化,上述状态转移方程已经优化为:dp[i][j] = max{dp[i-1][j], (dp[i][j-k*w[i]] + k*v[i]) } ( 0 <= k*v[i] <= W)
dp[i][j]表示将前i种物品装进限重为j的背包可以获得的最大价值, 0<=i<=N, 0<=j<=W
初始状态也是一样的,我们将dp[0][0…W]初始化为0,表示将前0种物品(即没有物品)装入书包的最大价值为0。那么当 i > 0 时dp[i][j]也有两种情况:
- 不装入第i种物品,即dp[i−1][j],同01背包;
- 装入第i种物品,此时和01背包不太一样,因为每种物品有无限个(但注意书包限重是有限的),
所以此时不应该转移到dp[i−1][j−w[i]]而应该转移到dp[i][j−w[i]],即装入第i种商品后还可以再继续装入第种商品。
所以状态转移方程为dp[i][j] = max(dp[i−1][j], dp[i][j−w[i]] + v[i]) // j >= w[i]
多重背包:
第i(i从1开始)种物品的重量为w[i],价值为v[i]。第i种物品最多有Mi件可用
在总重量不超过背包承载上限 W 的情况下,能够装入背包的最大价值是多少?
// dp[i][j] : 前i种物品放入一个容量为 j 的背包获得的最大价值
//对于第i种物品,我们有k种选择,0 <= k <= M[i] && 0 <= k * w[i] <= j,即可以选择0、1、2…M[i]个第i种物品,所以递推表达式为:dp[i][j] = max(dp[i][j− k * w[i]]+ k * v[i]) // 0 <= k <= M[i] && 0 <= k * w[i] <= j
视频:
- https://www.bilibili.com/video/BV18x411V7fm?from=search&seid=5454260386958297352
- https://www.bilibili.com/video/BV1X741127ZM?from=search&seid=17606712350225562747
参考文章
- 动态规划–资源分配问题
- 动态规划应用举例—资源分配问题
- 动态规划之背包问题系列
- 背包问题九讲 pdf
- 背包问题-笔记整理
- 动态规划之背包问题系列
动态规划之01背包问题- 0-1背包问题的动态规划算法
- 背包问题总结(上)
- 背包问题总结(下)
- 五大常用算法之二:动态规划算法
- 经典算法系列:动态规划
- 经典中的经典算法:动态规划(详细解释,从入门到实践,逐步讲解)
- 【常见笔试面试算法题12】动态规划算法案例分析
- 动态规划十大经典案例
- 常见动态规划题目详解
- 动态规划算法详解及经典例题
- 【动态规划】一次搞定三种背包问题
- 【动态规划】多重背包问题
- 【动态规划】01背包问题
- 【动态规划】01背包问题【续】
- 【动态规划】完全背包问题
- 动态规划–01背包模型