经典算法学习之动态规划
动态规划方法通常用来求解最优化问题。
适合使用动态规划求解最优化问题应具备的两个要素:
1、最优子结构:如果一个问题的最优解包含子问题的最优解,那么该问题就具有最优子结构。
2、子问题重叠(如果子问题不重叠就可以用递归的方法解决了)
具备上述两个要素的问题之所以用动态规划而不用分治算法是因为分治算法会反复的调用重叠的子问题导致,效率低下,而动态规划使用了运用了空间置换时间的思想,将每一个已解决的子问题保存起来,这样重复的子问题只需要计算1次,所以时间效率较高。
动态规划算法设计步骤:
1.刻画一个最优解的结构特征。
2.递归定义最优解的值。
3.计算最优解的值,通常采用自底向上的方法。
4.利用计算出的信息构造一个最优解。
其中发掘最优子结构的过程遵循下面的通用模式:
1. 证明问题最优解的第一个组成部分是做出一个选择。 2. 对于给定问题,在其可能的第一步选择中,假定已经知道哪种选择才会得到最优解,但并不关心这种选择具体是如何得到的,只是假定已经知道了这种选择。 3. 给定可获得最优解的选择后,确定这次选择会产生哪些子问题,以及如何最好地刻画子问题空间。 4. 利用 “剪切-粘贴”(cut-and-paste)技术证明:作为构成原问题最优解的组成部分,每个子问题的解就是它本身的最优解(利用反证法)。
动态规划的实现方法:
带备忘的自顶向下法:此方法仍按自然的递归形式编写过程,但过程会保存每个子问题的解(通常保存在一个数组或散列表中)。当需要一个子问题的解时,过程首先检查是否已经保存过此解。如果是,则直接返回保存的值,从而提高时间效率。
自底向上法:这种方法一般需要恰当定义子问题“规模”的概念,使得任何子问题的求解都依赖于“更小的”子问题的求解。因而我们可以将子问题按规模排序,按由小至大的顺序进行求解。当求解某个子问题时,它所依赖的那些更小的子问题都已经求解完毕,结果已经保存。每个子问题只需要求解一次,当我们求解它(也是第一次遇到它)时,它的所有前提子问题都已求解完成。
下面应用动态规划解决一个问题
serling公司购买长钢条,将其切割为锻钢条出售。切割工序没有成本。先给出出售一段长度为i的钢条的价格为p(i),对应关系如下表,求给一段长度为n(n<=10)的钢条,要切割多少次才能以最高的价格卖出?
长度i |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
---|---|---|---|---|---|---|---|---|---|---|
价格p(i) |
1 |
5 |
8 |
9 |
10 |
17 |
17 |
20 |
24 |
30 |
首先这个问题求解要多少次才能以最高的价格卖出,是一个求最优化的问题,接下来分析下该问题是否具有适用于最优化问题的两个特征,假设n=5的话,要求解长度为5的钢条怎么切割才能获得最优解,首先我们知道将长度为5的钢条进行切割,第一刀可以选择0+5、1+4、2+3三种切割方案,然后在每一种切割方案中,将剩下的继续选择最优方案(和递归的思想一样,将原问题转换成更小的子问题),可以列出下列关系式:r(5)=max{p(1)+r(5-1),p(2)+r(5-2),p(3)+r(5-3),p(4)+r(5-4),p(5)+r(0)} 式中r(i)表示总长度为i时的最大收益。可以看出求解r(5)的最优解包含了求解其子问题r(4)、r(3)、r(2)、r(1)、r(0)的最优解,
更为通用的表达式就是r(n)=max{p(1)+r(n-1),p(2)+r(n-2),......p(n-1)+r(1),p(n)+r(0)}可以看出通用的表达式里面的最优解包含了其子问题的最优解,所以该问题符合最优子结构的特征,然后再看有没有第二个特征,还是以n=5进行分析,下图显示了求解子问题的递归树
从递归树中可以看出有大量的求解都是重叠的,所以也满足动态规划的第二个特征,那么这个问题选择用动态规划的方法来求解很可能是一个很好的办法!
经过分析已经得出递归最优子结构:r(n)=max{p(1)+r(n-1),p(2)+r(n-2),......p(n-1)+r(1),p(n)+r(0)}
带备忘的自顶向下法伪代码:
1 memoized_cut(p,n)
2 let r[0...n]be a new array
3 for i=0 to n
4 r[i]=-1
5 return memoized_cut_digui(p,n,r)
6
7
8
9 memoized_cut_digui(p,n,r)
10 if(r[n]>=0)
11 return r[n]
12 if(0==n)
13 temp=0
14 else
15 temp=-1
16 for i=1 to n
17 if(p[i]+memoized_cut_digui(p,n-i,r)>temp)
18 temp=p[i]+memoized_cut_digui(p,n-i,r)
19 r[n]=temp
20 return temp
21
22
带备忘的自顶向下法C++程序:
1 #include<iostream>
2 using namespace std;
3 int memoized_cut_digui(int *p,int n,int *r)
4 {
5 int temp;
6 if(r[n]>=0)
7 {
8 return r[n];
9 }
10 if(n==0)
11 {
12 temp=0;
13 }
14 else temp=-1;
15 for(int i=1;i<=n;i++)
16 {
17 if((p[i]+memoized_cut_digui(p,n-i,r))>temp)
18 {
19 temp=p[i]+memoized_cut_digui(p,n-i,r);
20 }
21 }
22 r[n]=temp;
23 return temp;
24 }
25 int memoized_cut(int *p,int n)
26 {
27 int *r=new int[n];
28 memset(r,-1,n); //将r数组全部赋值为-1
29 return memoized_cut_digui(p,n,r);
30 }
31 int main()
32 {
33 int p[11]={0,1,5,8,9,10,17,17,20,24,30};
34 int n;
35 cin>>n;
36 cout<<memoized_cut(p,n);
37 return 0;
38 }
自底向上法伪代码:
1 memoized_cut(p,n)
2 let r[0...n]be a new array
3 r[0]=0
4 for i=1 to n
5 temp=-1
6 for j=1 to i
7 if(p[j]+r[i-j]>temp)
8 temp=p[j]+r[i-j]
9 r[i]=temp
10 return r[n]
自底向上法C++代码
1 #include<iostream>
2 using namespace std;
3 int memoized_cut(int *p,int n)
4 {
5 int *r=new int[n+1];
6 r[0]=0;
7 //从r[1]逐次求解一直到r[n]
8 for(int i=1;i<=n;i++)
9 {
10 int temp=-1;
11 //求解每一个r[i]的时候都需要将它及它之前的每一段先切第一刀的可能性都遍历一遍,然后求这次遍历中得到的最大值为这个i下的最优解
12 for(int j=1;j<=i;j++)
13 {
14 if(p[j]+r[i-j]>temp)
15 {
16 temp=p[j]+r[i-j];
17 }
18
19 }
20 r[i]=temp;
21 }
22 return r[n];
23 }
24
25 int main()
26 {
27 int p[11]={0,1,5,8,9,10,17,17,20,24,30};
28 int n;
29 cin>>n;
30 cout<<memoized_cut(p,n);
31 return 0;
32 }
重构解 前面只求出了最优的收益值,并没有返回解的本身(没有给出最优的情况下,应该分成每个子段的长度值),为此我们可以在动态规划保存最优解的同时保存切割方案,然后对最优方案进行输出。
伪代码如下:
1 memoized_cut(p,n)
2 let r[0...n]be a new array
3 r[0]=0
4 for i=1 to n
5 temp=-1
6 for j=1 to i
7 if(p[j]+r[i-j]>temp)
8 temp=p[j]+r[i-j]
9 s[i]=j//保存最优解
10 r[i]=temp
11 return r[n] and s
12
13
14
15
16 print_zuiyoujie(s,n)
17 while n>0
18 cout s[n]
19 n=n-s[n]
C++程序如下:
1 #include<iostream>
2 using namespace std;
3 int memoized_cut(int *p,int n,int *s)
4 {
5 int *r=new int[n+1];
6 r[0]=0;
7 //从r[1]逐次求解一直到r[n]
8 for(int i=1;i<=n;i++)
9 {
10 int temp=-1;
11 //求解每一个r[i]的时候都需要将它及它之前的每一段先切第一刀的可能性都遍历一遍,然后求这次遍历中得到的最大值为这个i下的最优解
12 for(int j=1;j<=i;j++)
13 {
14 if(p[j]+r[i-j]>temp)
15 {
16 temp=p[j]+r[i-j];
17 *(s+i)=j;
18 }
19
20 }
21 r[i]=temp;
22 }
23 return r[n];
24 }
25
26 int main()
27 {
28 int p[11]={0,1,5,8,9,10,17,17,20,24,30};
29 int *s=NULL;
30 s=new int [11];
31 int n;
32 cin>>n;
33 cout<<"最大的收益为"<<memoized_cut(p,n,s)<<endl;
34 cout<<"最佳切割方案是:"<<endl;
35 while(n)
36 {
37 cout<<s[n]<<endl;
38 n=n-s[n];
39 }
40 return 0;
41 }
- 使用Yeoman创建ASP.NET Core项目
- Golang语言捕获panic异常并转化为error
- 在Windows下安装TensorFlow
- JavaFX——(第一篇:介绍篇)
- 自编码器是什么?有什么用?这里有一份入门指南(附代码)
- UWP基础教程 - {x:DeferLoadStrategy}
- UWP基础教程 - 重启应用
- html5打开摄像头
- UWP基础教程 - App多语言支持
- Golang实现Fibonacii的几种算法
- 【译】使用 dotnet watch 开发 ASP.NET Core 应用
- vmware安装ubuntu12.04嵌套安装xen server(实现嵌套虚拟化)
- Golang语言切片slice的线程协程安全问题
- ASP.NET Core 在 Azure 开启 HTTPS
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- ggplot2|从0开始绘制箱线图
- ggplot2|从0开始绘制直方图
- ggplot2|从0开始绘制PCA图
- Python字典
- 如何用R语言绘制生成正态分布图表
- ggplot2-plotly|让你的火山图“活”过来
- 小数据| 描述性统计(Python/R 实现)
- ggplot2|发散性“正负”图
- R语言蒙特卡洛计算和快速傅立叶变换计算矩生成函数
- pheatmap|暴雨暂歇,“热图”来袭!!!
- 统一服务消息接口报48001错误
- ggplot2|ggpubr进行“paper”组图合并
- PostgreSQL drop table 空间不释放的问题解决
- R语言预测人口死亡率:用李·卡特模型、非线性模型进行平滑估计
- Dockerfile 指令