[DP]
原文再续书接上一回,上一篇文章的背包讲的有点过于狭隘,我就某一类问题解释一种算法的思想:动态规划(Dynamic Programming)。简称DP。
我们在生活中不免会遇到一类问题,求什么什么的最大,什么什么的最小。在这类问题中,如果有算法基础的同学可能会知道贪心算法。按个人理解,贪心算法就是DP算法中的一类。
那么DP是什么?我们直接用一道题目来举例子去解释DP。
这里采用的例子引用自CLRS中的钢条分割问题。我只做一个概括性复述。有兴趣的同学可以去看看原著(这里强力吹一波CLRS,如果能够静下心来看,这本书还是相当的不错。
长度为n英寸的钢条,若给出一个数组p[n],该数组是指不同长度下钢条所能给出的价格,例如:

有一种很蠢但是很好理解的算法,就是我一共有n个元素(长度为n的钢条分割最小单元为1的情况下,我最多有n个元素)。所以我一共有2^(n-1)个方案(含重复的)我每个方案都算一个价格出来,在其中取最大值不就可以了吗?
当然可以,但是开销也是蹭蹭蹭地往上跑。

我们先提出一种递归的解法。
我们来分析最初的情况,我们从最左端开始看
我们在一开始,在i=1的情况下,我们可以选择切/不切,我们也能推广到一般条件,当i=k时候切还是不切。
如图:


那么我们只需要外层嵌套一次for循环,我们有长度为N的一段钢条,那么只切一刀的情况下,我们一共有n-1种切法;切割完之后依次是

我们的每一次操作,都是直接把左边的钢条直接转换成价值。且设每一种切割得到的价值为Cut-Steel[n]i用来代表在第i个位置,切割长度为n的钢条所得到的价值
那么我们每一次切割得到的价值就有如下的规律

我们这里等号右边的Cut-Steel[n-i]并没有指明切割的位置,为什么?我们往后看。
那我根据这条式子我该如何得到该问题整体的最优解?其实关键还是我们切的位置。
那我们就来求这个切的位置
其实啊,很简单,在长度为n的钢条中(首次切割),我们不是有n-1种方案嘛,那我们设置一个求max的过程,不就能求出n-1种方案里面,最优解是什么吗。
什么?你还是不懂?那我们想想,假设我们切割位置是k哈。那我们当前切割完能拿到手的钱就是p[k]嘛。
那我就不管我前面的部分咯,我后面只剩下n-k长的钢条,那我们现在的问题又回来了,我们要求长度为n-k的钢条的最高总价值嘛。那不就成了,我们问题回到了原点,只不过长度变短了罢了。我只需要在递归的时候,将长度为0设置返回值为0就可以了,那是不是就保证递归策略有出口了。
也就有最优子结构为如下

那我们接下来写一下伪码表示吧,随后我贴上Cpp的递归代码,应该就一目了然了。

Function CUT-STEEL(p,n)//这里p表示的是价值数组,n表示的是钢条长度if(n==0)return 0;//这里表示递归策略的出口q=-∞i from 1 to nq=max(q,p[i]+CUT-STEEL(p,n-i))//每一层循环就是在求切一次的最优return q;

建议这里可以根据伪码和自己开发用的趁手的语言去实现一下。
我这里写算法主要还是用C++,Cpp的递归代码如下

#include<iostream>
using namespace std;
int max(int a, int b) {return a > b ? a : b;
}
int value(int valuesTable[], int n) {if (n == 0)return 0;int q = -1;for (int i = 1; i <= n; i++) {q = max(q, valuesTable[i] + value(valuesTable, n - i));}return q;
}
int main() {int lengthOfSteel;while (cin >> lengthOfSteel) {int* valuesTable;valuesTable = new int[lengthOfSteel + 1];for (int count = 1; count <= lengthOfSteel; count++)cin >> valuesTable[count];//inputcout << "Output is " << value(valuesTable, lengthOfSteel)<<endl;}
}

我这里包含了输入输出,看得懂的同学可以忽略main函数。
当我们用max_values_length表示这个长度能拿到的最大利益时,我们就有一个推导方程。(不是什么高深的公式,就是从上述推导出来的结论)
我们这里的关键在于

q = max(q, valuesTable[i] + value(valuesTable, n - i));

我们把钢铁剩余部分的价值(减去前i长度后的长度的价值)交给递归的程序去计算,我们不需要手动去算,这就是递归代码的优势–容易理解,且容易实现。
可是
这一段代码不比我们一开始罗列出所有的方案,然后找最大值要好,为什么?我们画树状图进行分析

我们从树状图中可以看到,在这样递归的运算中,我们每一次求第n项,必须访问1到n-项。就是说,第n项的值取决于前n-1项的值,而在这个过程中,我们会反复求解于底层的某一个数。例如在该图中,1就被访问了3次(图中只画出了三次,但其实2扩展之后还会再访问一次了)。也就是说0会被访问很多次。

这样其实增加了很多的开销,我们DP的思想就是在递归程序中每次取值的时候我们都能有一个地址,让我能够判断这个值是否被算过,如果被算过了那么我们就直接调用就可以了,而不是像传统的递归程序需要重新计算一次。如果没算过,那就算一次之后进行存储就好了。以便下一次调用的时候能够直接通过DP_Table数组进行访问就可以了(DP_Table就是一个动态规划表,用来存储每一个状态的值。避免过度的运算)

我们先实现自顶向下的带备忘的递归写法。说的很高大上吼,其实就是多给了一个存储数组,去记录每个数,避免反复计算罢了。这个DP_Table在带备忘的算法里面,其实相当于一个存储器而已,如果说没有优化的算法是一个傻子,算完就忘,那么优化过的算法就是一个聪明人,能记住自己做过什么。

#include<iostream>
using namespace std;
int max(int a, int b) {return a > b ? a : b;
}
int value(int valuesTable[], int n,int DP_Table[]) {if (DP_Table[n] >= 0)return DP_Table[n];else {int temp = -1;for (int i = 1; i  <= n; i++) {temp = max(temp, valuesTable[i] + value(valuesTable, n - i, DP_Table));}DP_Table[n] = temp;return temp;}
}
int main() {int lengthOfSteel;while (cin >> lengthOfSteel) {int* valuesTable;valuesTable = new int[lengthOfSteel + 1];int* DP_table = new int[lengthOfSteel + 1];memset(DP_table, -1, lengthOfSteel + 1);DP_table[0] = 0;for (int count = 1; count <= lengthOfSteel; count++)cin >> valuesTable[count];//inputcout << "Output is " << value(valuesTable, lengthOfSteel,DP_table)<<endl;}
}

如果此时你理解了不带备忘效果的递归代码,那么我相信这段代码你看起来也不会吃力。

尽管带备忘效果的递归代码已经能够满足我们的时间复杂度的要求了,但是我们的计算机是非常不愿意跑你的递归代码的,当n的数量级上去了,压栈的时间也不能忽略,还有可能会发生把栈压爆而非正常终止的情况。那么我们就顺顺计算机的意愿:写一个自底向上的迭代程序就能避免这方面的问题了。
为了方便理解,我先贴代码再解释

#include<iostream>
using namespace std;
int max(int a, int b) {return a > b ? a : b;
}
int value(int valuesTable[], int n,int DP_Table[]) {DP_Table[0] = 0;int temp = -1;for (int i = 1; i <= n; i++) {temp = -1;for (int j = 1; j <= i; j++) {temp = max(temp, valuesTable[j] + DP_Table[i - j]);}DP_Table[i] = temp;}return DP_Table[n];
}
int main() {int lengthOfSteel;while (cin >> lengthOfSteel) {int* valuesTable;valuesTable = new int[lengthOfSteel + 1];int* DP_table = new int[lengthOfSteel + 1];for (int count = 1; count <= lengthOfSteel; count++)cin >> valuesTable[count];//inputcout << value(valuesTable, lengthOfSteel, DP_table)<<endl;}
}

主函数中的代码依旧是没什么好说了,就都是一个调用Solution的过程,这里主要说一下value函数
在Value函数中,我每一次的循环开始我都定义一个临时变量temp,用来记录该切点左边一段的钢条能卖最贵是多少,并把它放在DP数组的对应索引下,而我们每次在最外层循环结束,都能依次得到,前面长度为i的钢条最贵能卖多少钱。而我们后面的最优解又依赖着前面的各个最优解的值(注意这里的只,这决定了没有后效性的问题,后文会对后效性进行详解),所以就能算出整体的最优解,因为我们能保证每一个局部都是最优解,自然出来的结果也是最优解

好了,我们三段代码,三种方法解决了这个钢条切割的问题,我们来总结一下吧。我们解决问题的顺序依次是,我们要找到一个能够解决该问题的递归代码。而该递归代码依赖着我们的分段分析,这也称为最优子结构,在钢条切割问题上,我们的最优子结构就是,我们要不要切,我们切了之后是分成两段多长的钢条,对于切下来的部分可以直接转换成价值,而剩下的钢条可以重新看成一个新的钢条切割问题,只是此时的sum不再是0,长度变成n-i

而在这个钢条分割问题中,我们显然发现他们除了存在一种最优子结构以外,还会因为递归的代码而产生一种重叠子问题(Overlapped Subproblem),而顺口提一嘴上面提到的无后效性,这直接决定了我们能不能用动态规划算法求解某问题,如果一个问题不存在无后效性,例如我们图论方面的部分问题。
可能这里提出的无后效性听不懂,但是没关系,这并不影响你解决这个问题,我后续还会继续出一些DP的文章,可能或许大概应该会详细讲吧哈哈哈哈哈哈哈哈哈哈
在我后续关于DP的文章中还会多次重复地提到
1.重叠子问题
2.无后效性
3.最优子结构

这三者。

感谢你能看到这里。

【DP1】钢条分割详解相关推荐

  1. SLIC超像素分割详解

    SLIC超像素分割详解(一) 超像素概念是2003年Xiaofeng Ren提出和发展起来的图像分割技术,是指具有相似纹理.颜色.亮度等特征的相邻像素构成的有一定视觉意义的不规则像素块.它利用像素之间 ...

  2. SLIC 超像素分割详解(三):应用

    看过上面的介绍后,我们应该思考一下:分割好的超像素有什么用?怎么用?用到哪里? 首先,超像素可以用来做跟踪,可以参考卢湖川课题组发表在IEEE TIP上的<Robust superpixeltr ...

  3. 【转】 SLIC超像素分割详解(一):简介

    http://blog.csdn.net/electech6/article/details/45509779 转载于:https://www.cnblogs.com/nfydream/p/57749 ...

  4. Mask RCNN算法详解(总结)

    Mask RCNN:目标检测+实例分割 作用:可以完成目标分类,目标检测,语义分割,实例分割,人体姿态识别等多种任务. 1.实例分割与语义分割的区别和关系? 通常的目标分割是指语义分割,实例分割是从目 ...

  5. 【OpenCV 4开发详解】分割图像——分水岭法

    本文首发于"小白学视觉"微信公众号,欢迎关注公众号 本文作者为小白,版权归人民邮电出版社发行所有,禁止转载,侵权必究! 经过几个月的努力,小白终于完成了市面上第一本OpenCV 4 ...

  6. 【OpenCV 4开发详解】分割图像——Mean-Shift分割算法

    本文首发于"小白学视觉"微信公众号,欢迎关注公众号 本文作者为小白,版权归人民邮电出版社发行所有,禁止转载,侵权必究! 经过几个月的努力,小白终于完成了市面上第一本OpenCV 4 ...

  7. 【OpenCV 4开发详解】分割图像——Grabcut图像分割

    本文首发于"小白学视觉"微信公众号,欢迎关注公众号 本文作者为小白,版权归人民邮电出版社发行所有,禁止转载,侵权必究! 经过几个月的努力,小白终于完成了市面上第一本OpenCV 4 ...

  8. 全卷积神经网路【U-net项目实战】语义分割之U-Net详解

    文章目录 1.简介 2.U-net典型应用 3. U-net详解 4.参考文献 1.简介 语义分割(Semantic Segmentation)是图像处理和机器视觉一个重要分支.与分类任务不同,语义分 ...

  9. sklearn 随机分割数据_sklearn.ensemble.RandomForestClassifier 随机深林参数详解

    随机森林是一种元估计量,它适合数据集各个子样本上的许多决策树分类器,并使用平均数来提高预测准确性和控制过度拟合.子样本大小由max_samples参数bootstrap=True (default)控 ...

最新文章

  1. MapReduce设计模式
  2. 在IIS6中FLV不能播放
  3. java snmp walk_snmpwalk用法
  4. vitess源码阅读笔记cache系列之用go实现通用资源池
  5. 使用EMR Spark Relational Cache跨集群同步数据
  6. 为什么计算机中0.2+0.1不等于0.3!?
  7. 备份不等于归档,在智能归档中备份资产!
  8. 【Python成长之路】python 基础篇 -- 装饰器【华为云分享】
  9. linux ftp使用相关
  10. socket 和 TCP/IP 协议的关系
  11. linux 命令行修改root密码
  12. Windows Server已可安装Docker,Azure开始支持Mesosphere
  13. 复变函数——一到三章总结
  14. 《影响力》- 作者 Robert B. Cialdini 罗伯特·西奥迪尼 读后感
  15. flash游戏开发02_引入flixel框架的helloworld
  16. 经纬度换算数值_常用经纬度转换
  17. android ui设计最新字体,ui用什么字体_安卓ui设计用什么字体
  18. Day3:现金流三拷问---投资环节
  19. Linux源码安装apache
  20. c++ std::exception,std::logic_error 异常的使用方法

热门文章

  1. GitHub开源:支持100多种语言的OCR文字识别
  2. vue-live2d 看板娘
  3. 薪火相传 点亮企业数智化舞台—— CDEC2021中国数字智能生态大会西安收官
  4. 计算机中汉字的顺序用什么牌,中国汉字的写做顺序,你知道吗?
  5. html战棋游戏战棋游戏,六款不可错过的战棋游戏 将领才华始于纸上谈兵
  6. 山东省农村信用计算机社考试,山东省农村信用社计算机专业考试题
  7. 第一型曲线积分与第一型曲面积分、第二型曲线积分与格林公式
  8. 643、子数组最大平均数 I
  9. 【Java+MySQL】随机添加测试数据栗子
  10. 判断触发器是否被禁用