完结撒花常见算法——动态规划
Posted 水之Coding工房
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了完结撒花常见算法——动态规划相关的知识,希望对你有一定的参考价值。
动态规划
Dynamic Programming
实例分析引入“动态规划”
首先,我们从一个简单版的背包问题入手:
对于一组不同重量、不可分割的物品,我们需要选择一些装入背包,在满足背包最大重量限制的前提下,背包中物品总重量的最大值是多少呢?
其实,这个问题可以通过穷举所有的装法,然后通过比较找出满足条件的最大值的方法来实现。其实这种算法也有一个名字,叫做回溯算法(回溯算法相当于穷举搜索。穷举所有的情况,然后对比得到最优解)。
1// 回溯算法实现。P.S.输入的变量都定义成了成员变量。
2private int maxW = Integer.MIN_VALUE; // 结果放到 maxW 中
3private int[] weight = {3,3,5,8,4}; // 物品重量
4private int n = 5; // 物品个数
5private int w = 12; // 背包承受的最大重量
6// 调用 f(0, 0),i参数表示将要决策第几个物品是否装入背包,cw参数表示当前背包中物品的总重量
7public void f(int i, int cw) {
8 if (cw == w || i == n) { // cw==w 表示装满了,i==n 表示物品都考察完了
9 if (cw > maxW) maxW = cw;
10 return;
11 }
12 f(i+1, cw); // 选择不装第 i 个物品
13 if (cw + weight[i] <= w) {
14 f(i+1,cw + weight[i]); // 选择装第 i 个物品
15 }
16}
那么我们该如何去优化这个递归算法呢?有一种“打小抄”的方法可以帮助优化递归算法,这个方法实际上就是,记录已经计算好的f(i, cw)放到“小抄”(抽象而言即是一种数据结构,可以是数组)中,当再次计算到重复的 f(i, cw) 的时候,可以直接从”小抄”中取出来用,就不用再递归计算了,这样就可以避免冗余计算。
1private int maxW = Integer.MIN_VALUE; // 结果放到 maxW 中
2private int[] weight = {3,3,5,8,4}; // 物品重量
3private int n = 5; // 物品个数
4private int w = 12; // 背包承受的最大重量
5private boolean[][] mem = new boolean[5][10]; //小抄,默认值 false
6public void f(int i, int cw) {
7 if (cw == w || i == n) { // cw==w 表示装满了,i==n 表示物品都考察完了
8 if (cw > maxW) maxW = cw;
9 return;
10 }
11 if (mem[i][cw]) return; // 重复状态
12 mem[i][cw] = true; // 记录 (i, cw) 这个状态
13 f(i+1, cw); // 选择不装第 i 个物品
14 if (cw + weight[i] <= w) {
15 f(i+1,cw + weight[i]); // 选择装第 i 个物品
16 }
17}
我们可以把整个求解过程分为 n 个阶段,每个阶段会决策一个物品是否放到背包中。每个物品决策(放入或者不放入背包)完之后,背包中的物品的重量会有多种情况,也就是说,会达到多种不同的状态。
把每一层重复的状态合并,只记录不同的状态,然后基于上一层的状态集合,来推导下一层的状态集合。我们可以通过合并每一层重复的状态,这样就保证每一层不同状态的个数都不会超过 w 个(w 表示背包的承载重量),也就是例子中的 12。于是,我们就成功避免了每层状态个数的指数级增长。
我们用一个二维数组 states[n][w+1],来记录每层可以达到的不同状态。
第 0 个(下标从 0 开始编号)物品的重量是3,要么装入背包,要么不装入背包,决策完之后,会对应背包的两种状态,背包中物品的总重量是 0 或者 3。我们用states[0][0]=true 和 states[0][3]=true 来表示这两种状态。
第 1 个物品的重量也是 3,基于之前的背包状态,在这个物品决策完之后,不同的状态有 3 个,背包中物品总重量分别是 0(0+0),3(0+3 or 3+0),6(3+3)。我们用 states[1][0]=true,states[1][3]=true,states[1][6]=true 来表示这三种状态。
以此类推,直到考察完所有的物品后,整个 states 状态数组就都计算好了。我们只需要在最后一层,找一个值为 true 的最接近 w(也就是例子中的12)的值,就是背包中物品总重量的最大值。
1//weight: 物品重量,n: 物品个数,w: 背包可承载重量
2public int knapsack(int[] weight, int n, int w) {
3 boolean[][] states = new boolean[n][w+1]; // 默认值 false
4 states[0][0] = true; // 第一行的数据要特殊处理,可以利用哨兵优化
5 states[0][weight[0]] = true;
6 for (int i = 1; i < n; ++i) { // 动态规划状态转移
7 for (int j = 0; j <= w; ++j) {// 不把第 i 个物品放入背包
8 if (states[i-1][j] == true) states[i][j] = states[i-1][j];
9 }
10 for (int j = 0; j <= w-weight[i]; ++j) {// 把第 i 个物品放入背包
11 if (states[i-1][j]==true) states[i][j+weight[i]] = true;
12 }
13 }
14 for (int i = w; i >= 0; --i) { // 输出结果
15 if (states[n-1][i] == true) return i;
16 }
17 return 0;
18}
该代码的时间复杂度也可以根据耗时最多的部分,也就是代码中的两层 for 循环,所以时间复杂度是 O(n*w)。n 表示物品个数,w 表示背包可以承载的总重量。这样从理论来看,指数级的时间复杂度肯定要比 O(n*w) 高很多。
尽管动态规划的执行效率比较高,但是就刚刚的代码实现,我们需要额外申请一个 n 乘以 w+1 的二维数组,对空间的消耗比较多。所以有时候,其实动态规划是一种空间换时间的解决思路。
1public static int knapsack2(int[] items, int n, int w) {
2 boolean[] states = new boolean[w+1]; // 默认值 false
3 states[0] = true; // 第一行的数据特殊处理
4 states[items[0]] = true;
5 for (int i = 1; i < n; ++i) { // 动态规划
6 for (int j = w-items[i]; j >= 0; --j) {// 把第 i 个物品放入背包
7 if (states[j]==true) states[j+items[i]] = true;
8 }
9 }
10 for (int i = w; i >= 0; --i) { // 输出结果
11 if (states[i] == true) return i;
12 }
13 return 0;
14}
背包问题——升级版
那么我们进一步,将背包问题中引入物品价值因素。也就是说对于一组不同重量、不同价值、不可分割的物品,我们选择将某些物品装入背包,在满足背包最大重量限制的前提下,背包中可装入物品的总价值最大是多少呢?
1private int maxV = Integer.MIN_VALUE; //结果放到 maxV 中
2private int[] items = {2,2,4,6,3}; //物品的重量
3private int[] value = {3,4,8,9,6}; //物品的价值
4private int n = 5; //物品个数
5private int w = 9; //背包承受的最大重量
6public void f(int i, int cw, int cv) { //调用 f(0, 0, 0)
7 if (cw == w || i == n) { // cw==w 表示装满了,i==n 表示物品都考察完了
8 if (cv > maxV) maxV = cv;
9 return;
10 }
11 f(i+1, cw, cv); //选择不装第 i 个物品
12 if (cw + weight[i] <= w) {
13 f(i+1,cw+weight[i], cv+value[i]); //选择装第 i 个物品
14 }
15}
那我们再想一下,如何通过动态规划解决这个问题呢?
同样的,还是把整个求解过程分为 n 个阶段,每个阶段会决策一个物品是否放到背包中。每个阶段决策完之后,背包中的物品的总重量以及总价值,会有多种情况,也就是会达到多种不同的状态。在这里,可以用一个二维数组 states[n][w+1],来记录每层可以达到的不同状态。不过这里数组存储的值不再是布尔类型的了,而是当前状态对应的最大总价值。我们把每一层中 (i, cw) 重复的状态(节点)合并,只记录 cv 值最大的那个状态,然后基于这些状态来推导下一层的状态。
1public static int knapsack3(int[] weight, int[] value, int n, int w) {
2 int[][] states = new int[n][w+1];
3 for (int i = 0; i < n; ++i) { //初始化 states
4 for (int j = 0; j < w+1; ++j) {
5 states[i][j] = -1;
6 }
7 }
8 states[0][0] = 0;
9 states[0][weight[0]] = value[0];
10 for (int i = 1; i < n; ++i) { //动态规划,状态转移
11 for (int j = 0; j <= w; ++j) { //不选择第 i 个物品
12 if (states[i-1][j] >= 0) states[i][j] = states[i-1][j];
13 }
14 for (int j = 0; j <= w-weight[i]; ++j) { //选择第 i 个物品
15 if (states[i-1][j] >= 0) {
16 int v = states[i-1][j] + value[i];
17 if (v > states[i][j+weight[i]]) {
18 states[i][j+weight[i]] = v;
19 }
20 }
21 }
22 }
23 // 找出最大值
24 int maxvalue = -1;
25 for (int j = 0; j <= w; ++j) {
26 if (states[n-1][j] > maxvalue) maxvalue = states[n-1][j];
27 }
28 return maxvalue;
29}
理论讲解“动态规划”
现在,相信对于动态规划已经有了一个初步的认识。那么问题又来了,动态规划能解决的问题有什么规律可循呢?我们再来认真看下。
其实,动态规划作为一个已经十分成熟的算法,已经被前辈们总结为以下三个特征——最优子结构、无后效性和重复子问题。
将问题以及上面的三个特征,可以抽象为一个模型,也就是“多阶段决策最优解模型”,该模型一定符合以上的三个特征。
1、状态转移表法
一般能用动态规划解决的问题,都可以使用回溯算法暴力解决。所以,当我们拿到问题的时候,我们可以先用简单的回溯算法解决,然后定义状态,通过自行举例推导,是否存在重复子问题(通常的动态规划问题大概率都是有的),以及重复子问题是如何产生的。以此来寻找规律,看是否能用动态规划解决。
找到重复子问题之后,接下来,我们有两种处理思路,第一种是直接用回溯加上“打小抄”的方法,来避免重复计算子问题。第二种是使用动态规划的解决方法,状态转移表法。
我们先画出一个状态表。状态表一般都是二维的,所以你可以把它想象成二维数组。其中,每个状态包含三个变量,行、列、数组值。我们根据决策的先后过程,从前往后,根据递推关系,分阶段填充状态表中的每个状态。最后,我们将这个递推填表的过程,翻译成代码,就是动态规划代码了。
2、 状态转移方程法
状态转移方程法比较类似递归。我们需要先分析,某个问题如何通过子问题来递归求解,也就是所谓的最优子结构。根据最优子结构,写出递归公式,也就是所谓的状态转移方程(可以理解为是高中数学中数列的递推公式)。
一般情况下,对于代码的实现同样有两种方法,一种是递归加“打小抄”,另一种是迭代递推。方法核心与前一种较为类似,水水就不展开了。
总结一下,其实动态规划的两种解决问题的方法思路用一句话来讲就是——状态转移表法解题思路大致可以概括为,回溯算法实现 - 定义状态 - 画递归树 - 找重复子问题 - 画状态转移表 - 根据递推关系填表- 将填表过程翻译成代码;状态转移方程法的大致思路可以概括为,找最优子结构 - 写状态转移方程 - 将状态转移方程翻译成代码。
那么初实践和理论都讲解完毕后,最合适就是——多练习啦~对于目前一般的动态规划问题,应该都是在以上所讲的基础上稍微变形,都是可以解决的!那么动态规划入门之路就到这里结束啦~
■ Over ■
本次动态规划专题
就完整结束啦
笔芯.gif
附寒假专题所有推送扫码直达
我是水水
我们下期再见
祝学习愉快鸭
历史
史
文
章
有什么问题的话欢迎在后台输入框中回复哦~
以上是关于完结撒花常见算法——动态规划的主要内容,如果未能解决你的问题,请参考以下文章
台大李宏毅《机器学习》2021课程撒花完结!除了视频PPT,还有人汇编了一本答疑书...