一、算法介绍
动态规划(Dynamic Programming,DP)是运筹学的一个分支,是求解决策过程最优化的过程。
-
应用场景:动态规划的应用极其广泛,包括工程技术、经济、工业生产、军事以及自动化控制等领域,并在背包问题、生产经营问题、资金管理问题、资源分配问题、最短路径问题和复杂系统可靠性问题等中取得了显著的效果。
-
核心思想:将大问题划分为小问题进行解决,从而一步步获取最优解的处理算法。
-
与分治算法的异同点:
- 相同点:动态规划算法与分治算法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。
- 不同点:与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的 ( 即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解 )。
-
动态规划可以通过填表的方式来逐步推进,得到最优解。
二、经典案例-背包问题
2.1 需求分析
背包问题(Knapsack problem)是一种组合优化的NP完全问题。问题可以描述为:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。
- 题目说明:有一个背包,总容量为 4 磅 , 现有如下物品:
物品 | 重量 | 价格 |
---|---|---|
吉他(G) | 1 | 1500 |
音响(S) | 4 | 3000 |
电脑(L) | 3 | 2000 |
- 需求一:要求达到的目标为装入的背包的总价格最大,并且重量不超出。
- 需求二:要求装入的物品不能重复。
- 补充说明:背包问题又分为01背包(每个物品最多放一个)和完全背包(每种物品都有无限件可用)。接下来我们会根据01背包的要求解决问题。
2.2 算法步骤
-
算法思路:
- 每次遍历到的第 i 个物品,根据 w[i] 和 v[i] 来确定是否需要将该物品放入背包中。(即对于给定的 n 个物品,设 v[i]、w[i]分别为第 i 个物品的价格和重量,c 为背包的容量)。
- 再令 v[i][j]表示在前 i 个物品中能够装入容量为 j 的背包中的最大价格(动态变化)。
-
具体步骤如下:
- 填入表(第一行和第一列是 0):v[i][0]=v[0][j]=0;
- 当准备加入新增的商品的容量大于当前背包的容量时,就直接使用上一个单元格的装入策略:当 w[i] > j 时:v[i][j]=v[i-1][j];
- 当准备加入的新增的商品的容量小于等于当前背包的容量时:当 w[i] <= j 时: v[i][j]=max{v[i-1][j], v[i]+v[i-1][j-w[i]]} (v[i-1][j]:表示上一个单元格的装入的最大值;v[i]:表示当前商品的价格 ;v[i-1][j-w[i]]:表示装入 i-1 商品,到剩余空间 j-w[i]的最大值 )。
- 示意图:
2.3 代码示例
public class KnapsackProblemDemo {
public static void main(String[] args) {
// 物品的重量。
int[] w = {1, 4, 3};
// 物品的价格(这里val[i] 就是前面讲的v[i])。
int[] val = {1500, 3000, 2000};
// 背包容量。
int m = 4;
// 物品数量。
int n = val.length;
// 【第一步】:初始化操作。
// 表示在前i个物品中能够装入容量为j的背包中的最大价格。
// 行为物品数量,列为背包容量。
int[][] v = new int[n + 1][m + 1];
// 用于记录商品放入情况。
int[][] records = new int[n + 1][m + 1];
// 初始化填入表(全0的行和列)。
for (int i = 0; i < v.length; i++) {
// 第一列设置为0。
v[i][0] = 0;
}
// 第一行设置为0.
Arrays.fill(v[0], 0);
// 【第二步】:根据公式进行动态规划处理。
for (int i = 1; i < v.length; i++) {
for (int j = 1; j < v[0].length; j++) {
// 如果新增商品重量大于当前背包的容量,就使用上一个单元格的策略。
// 说明1:程序i是从1开始的,因此原来公式中的 w[i] 修改成 w[i-1]。
if (w[i - 1] > j) {
v[i][j] = v[i - 1][j];
} else {
// v[i-1][j]:表示上一个单元格的装入的最大值。
// v[i]:表示当前商品的价格。
// v[i-1][j-w[i]]:表示装入 i-1 商品,到剩余空间 j-w[i]的最大值。
// 说明2:同样地,程序i是从1开始的,因此原来公式中的 w[i] 修改成 w[i-1]。
if (v[i - 1][j] < val[i - 1] + v[i - 1][j - w[i - 1]]) {
v[i][j] = val[i - 1] + v[i - 1][j - w[i - 1]];
// 记录当前情况。
records[i][j] = 1;
} else {
// 否则使用上一个单元格的策略。
v[i][j] = v[i - 1][j];
}
}
}
}
// 输出当前记录的情况。
for (int[] i : v) {
for (int j : i) {
System.out.print(j + " ");
}
System.out.println();
}
// 0 0 0 0 0
// 0 1500 1500 1500 1500
// 0 1500 1500 1500 3000
// 0 1500 1500 2000 3500
System.out.println("============================");
// 查看最后单元格策略是否符合预期。
// 行、列的最大下标。
int maxRow = records.length - 1;
int maxCol = records[0].length - 1;
System.out.println("最后单元格放入总价格=" + v[maxRow][maxCol]);
System.out.println("具体策略如下:");
while (0 < maxRow && 0 < maxCol) {
if (1 == records[maxRow][maxCol]) {
System.out.printf("第%d个商品放入到背包。", maxRow);
System.out.println();
maxCol -= w[maxRow - 1];
}
maxRow--;
}
// 最后单元格放入总价格=3500
// 具体策略如下:
// 第3个商品放入到背包。
// 第1个商品放入到背包。
}
}
三、结束语
“-------怕什么真理无穷,进一寸有一寸的欢喜。”
微信公众号搜索:饺子泡牛奶。