数据结构与算法(十二)——算法-动态规划
一、青蛙跳台阶&斐波那契数列
1、问题
一只青蛙跳台阶,每次可以跳 1 层或 2 层。青蛙跳到 n 层一共有多少种跳法?
2、思想
先把问题规模缩小,考虑 n = 1时,n = 2的解。那么,显然有:
(1)边界条件:dp[1] = 1、dp[2] = 2
(2)再考虑 n = 3时,逆向思维一下,要跳 3 层,是不是只能是从第 2 阶跳 1 层到或者是从第 1 阶跳 2 层到。所以dp[3] = dp[2] + dp[1]。
(3)同理n = 4时,是不是也是只能是从第 3 阶跳 1 层到或者是从第 3 阶跳 2 层到。所以dp[4] = dp[3] + dp[2]。
(3)……
(4)同理可得,状态转移方程:dp
= dp[n - 1] + dp[n - 2]。
public class Knapsack { private Goods[] goods; // 背包的容量 private int c; // dp[i][j]:表示在前i个物品中选择,能够装入容量为j的背包中的最大价值 private int[][] dp; private int[][] path; public Knapsack(Goods[] goods, int c) { if (goods == null || goods.length == 0 || c <= 0) { return; } this.goods = goods; this.c = c; this.dp = new int[goods.length][c + 1]; this.path = new int[goods.length][c + 1]; // 初始化第一列、第一行为0.可不写,默认就是0 // for (int i = 0; i < dp.length; i++) { // dp[i][0] = 0; // } // for (int i = 0; i < dp[0].length; i++) { // dp[0][i] = 0; // } } // 动态规划 public void dp() { for (int i = 1; i < dp.length; i++) { for (int j = 1; j < dp[0].length; j++) { // 表明当前物品放不了背包 if (goods[i].weight > j) { dp[i][j] = dp[i - 1][j]; } else { dp[i][j] = Math.max(dp[i - 1][j], goods[i].value + dp[i - 1][j - goods[i].weight]); // 用于记录物品选择策略 if (dp[i - 1][j] < goods[i].value + dp[i - 1][j - goods[i].weight]) { path[i][j] = 1; } } } } } // 获取物品的选择策略 public List<Goods> getPlan() { // 1.查看 dp 填写情况 for (int i = 0; i < dp.length; i++) { for (int j = 0; j < dp[i].length; j++) { System.out.print(dp[i][j] + " "); } System.out.println(); } // 2.物品选择策略 List<Goods> result = new ArrayList<>(); int i = path.length - 1; int j = path[0].length - 1; // 从path的最后一个开始找 while (i > 0 && j > 0) { if (path[i][j] == 1) { result.add(goods[i]); j = j - goods[i].weight; } i--; } return result; } } // 物品 class Goods { public String name; // 物品重量 public int weight; // 物品价值 public int value; public Goods(String name, int weight, int value) { this.name = name; this.weight = weight; this.value = value; } @Override public String toString() { return "Goods{" + "name='" + name + '\'' + ", weight=" + weight + ", value=" + value + '}'; } }0-1背包
public class Main { public static void main(String[] args) { Goods[] goods = new Goods[4]; // 为了使得 goods[1] 是第1个物品 goods[0] = null; goods[1] = new Goods("吉他", 1, 1500); goods[2] = new Goods("音响", 4, 3000); goods[3] = new Goods("电脑", 3, 2000); // 背包容量 final int C = 4; Knapsack knapsack = new Knapsack(goods, C); System.out.println("动态规划dp:"); knapsack.dp(); final List<Goods> plan = knapsack.getPlan(); System.out.println("物品选择策略:"); System.out.println(plan); } } // 结果 动态规划dp: 0 0 0 0 0 0 1500 1500 1500 1500 0 1500 1500 1500 3000 0 1500 1500 2000 3500 物品选择策略: [Goods{name='电脑', weight=3, value=2000}, Goods{name='吉他', weight=1, value=1500}]测试类
4、说明
0-1背包问题不太好理解。下面用一些图文帮助读者理解。首先,我们深刻理解一下0-1背包的问题,如图:
不妨假设,在这前 i 个物品中,选取总重量不超过背包容量 C 的物品,且使得物品总价值最大。选择策略叫方案A,此时的最大总价值设为 maxValue。这里理解一下前 i 个物品,由于动态规划问题是规模逐渐增大的,所以,要姑且把物品看成是"有序的"。
那么,我们思考一个问题,如果此时来了第 i + 1个物品(价值:V,重量:W),会怎么样呢?如图:
主要分为两种情况谈论:
1、W > C :第 i + 1 个物品(钻石)不可取。那么,此时(总体物品是 i + 1个,但是)是不是就相当于没有第 i + 1 这个物品(因为在i + 1个物品中选择,不会取到第 i + 1这个物品),问题的解,是不是就等价于图一(在 i 个物品中选择,背包容量为C),所以,就是方案A,最大总价值就是 maxValue。
2、W <= C :第 i + 1个物品(钻石)可取。又分为两种情况:即拿或不拿。(注:这是0-1背包,一个物品要么不选择,要么只选择一次)
2.1:不拿。同样,此时(这种情况下)问题的解,与上述(W > C)相同,即方案A,最大总价值就是 maxValue
2.2:拿。那么,第 i + 1 个物品(钻石)已经被选择了,此时背包容量还有(C - W)。由于第 i + 1个物品已经被选择了,此时问题规模,就是在前 i 个物品中,选取总重量不超过背包容量 C - W 的物品。价值就是dp[i][c - w]。
由于2.1和2.2属于一种情况。所以整个问题的解就是 max( maxValue, V + dp[i][c - w] )。
下面,再对上面填表的过程,以及代码做一些解释:
- 算法导论16.2-2--动态规划(0-1背包问题)
- 算法_动态规划_石子合并问题
- 算法---贪心算法和动态规划
- 五大常用算法----贪心、动态规划、分支限界、分治算法和回溯算法
- 算法学习——动态规划策略入门 (转载)
- OpenCV2学习笔记(十二):特征提取算法SIFT与SURF
- 【动态规划】LIS最长单调递增子序列 logn算法 并且输出子序列
- 五大算法之二--动态规划
- LeetCode初级算法之动态规划
- 算法:动态规划
- java实现经典算法实例详解(递归,穷举,贪心,分治,动态规划,回溯,其他)求职必备
- 算法笔记——动态规划:最长子公共字符串
- noip数据结构与算法 之 基础小算法 1 一维前缀和维护
- 算法系列之十二:多边形区域填充算法--改进的扫描线填充算法
- 浅析数据结构与算法12--无向图相关算法基础
- C++(数据结构与算法)83:---动态规划应用之(0/1背包、矩阵乘法链、所有顶点对之间的最短路径、带有负值的单源最短路径、网组的无交叉子集)
- 贪心算法-分治算法-动态规划-回溯-分支限界的简单介绍
- 【算法学习】双调欧几里得旅行商问题(动态规划)
- 矩阵连乘-动态规划-(只是感觉描述的清晰易懂,并不是什么新算法)
- 五大常用算法:分治、动态规划、贪心、回溯和分支界定