动态规划中包含三个重要概念:最优子结构、边界、状态转移公式。
题目一:上台阶问题:
(1)问题建模:
F(10)=F(9)+F(8);
F(9)和F(8)是F(10)的最优子结构;
F(1)和F(2)是问题的边界,如果一个问题没有边界将永远无法得到有限的结果。
F(n)=F(n-1)+F(n-2)是阶段与阶段之间的【状态转移方程】。这是动态规划的核心,决定了问题的每一个阶段和下一阶段的关系。
(2)求解问题:
法一:递归求解
int getWays(int n){
if(n<1){
return 0;
}
if(n==1){
return 1;
}
if(n==2){
return 2;
}
return getWays(n-1)+getWays(n-2);
}
时间复杂度分析:
树的节点个数就是递归方法所需要计算的次数。
不能难看出,这颗二叉树的高度是N-1,节点个数接近2的N-1次方。所以时间复杂度可以近似地看作是O(2^N)。
优化,避免重复计算
方法二:备忘录算法
int getWays(int n,HashMap<Integer,Integer> map){
if(n<1){
return 0;
}
if(n==1){
return 1;
}
if(n==2){
return 2;
}
if(map.contains(n)){
return map.get(n);
}else{
int value =getWays(n-1)+getWays(n-2);
map.put(n,value);
return value;
}
}
}
在以上代码中,集合map是一个备忘录,当每次需要计算F(N)的时候,会首先从map中寻找匹配元素,如果map中存在,就直接返回结果,如果map中不存在,就计算出结果,存入备忘录中。
时间复杂度分析:从F(1)到F(N)一共有N个不同的输入,在哈希表里存了N-2个结果,所以时间复杂度和空间复杂度都是O(N)。
继续优化,缩小空间复杂度。
备忘录是自顶向下做递归,动态规划是自底向上迭代
方法三:动态规划
每一次迭代过程中,只要保留之前的两个状态,就可以推导出新的状态,而不需要像备忘录算法那样保留全部的子状态。
int getWays(int n){
if(n<1){
return 0;
}
if(n==1){
return 1;
}
if(n==2){
return 2;
}
int a=1;
int b=2;
int temp=0;
for(int i=3;i<=n;i++){
temp=a+b;
a=b;
b=temp;
}
return temp;
}
时间复杂度为O(n),空间复杂度O(1)
题目二:国王和金矿
有一个国家发现了5座金矿,每座金矿的黄金储量不同,需要参与挖掘的工人数也不同。参与挖矿工人的总数是10人。每座金矿要么全挖,要么不挖,不能派出一半人挖取一半金矿。要求用程序求解出,要想得到尽可能多的黄金,应该选择挖取哪几座金矿?
总共10名工人
200金/3人 300金/4人 350金/3人
400金/5人 500金/5人
方法一:排列组合
每一座金矿都有挖与不挖两种选择,如果有N座金矿,排列组合起来有2^N种选择。对所有可能性做遍历,排除那些使用工人数超过10的选择,在剩下的选择里找出获得金币数最多的选择。
(1)问题建模
动态规划有三个核心元素:最优子结构、边界、状态转移方程式。
最优子结构:有两个。一,当第五个金矿选择不挖时,最优子结构应该是10个工人4个金矿时,挖出最多黄金的选择。二,当第五个金矿选择挖,会占用掉一部分工人,那么前四个金矿所分配的工人数量就是【10-第5个金矿所需人数】,而不再是10。
最优子结构和最终结果的关系:
为了便于描述,把金矿数量设为N,工人数设为W,金矿的黄金量设为数组G【】,金矿的用工量设为数组P【】。
那么5座金矿和4座金矿的最优选择之间存在这样的关系:F(5,10)=
MAX(F(4,10),F(4,10-P[4]) + G[4])
边界:
两种情况:
只有一座金矿,当工人数量够挖取第一座金矿时,得到的黄金数就是G【0】
当工人数量不够挖取第一座金矿时,得到的金矿数黄金数为0.
用公式来表达:
当N=1,W>=P[0],F(N,W)=G[0];
当N=1,W<P[0] 时,F(N,W)=0.
状态转移方程式:
F(n,w)=0(n<=1,w<p[0]);
F(n,w)=g[0] (n==1,w>=p[0] );
F(n,w)=F(n-1,w) (n>1,w<p[n-1])
F(n,w)=max(F(n-1,w),F(n-1,w-p[n-1])+g[n-1]) (n>1,w>=p[n-1])
(2)实现方法:简单递归、备忘录算法、动态规划
方法二:简单递归
把状态转移方程式翻译成递归程序,递归的结束的条件就是方程式当中的边界。因为每个状态有两个最优子结构,所以递归的执行流程类似于一颗高度为N的二叉树。时间复杂度为O(2^N).
方法三:备忘录算法
在简单递归的基础上增加一个HashMap备忘录,用来存储中间结果。HashMap的Key是一个包含金矿数N和工人数W的对象,Value是最优选择获得的黄金数。
规律:除了第1行以外,每个格子都是前一行的一个或两个格子推导而来。
比如3金矿8工人的结果,就来自与2金矿8工人和2金矿5工人加3金矿3工人的最大值,Max(500,500+200)=700.
用程序实现的时候,也可以像这样从左至右,从上到下一格一格推导出最终结果。而且不需要存储整个表格,只需要存储前一行的结果,就可以推导出新的一行
方法四:动态规划
方法利用两层迭代,来逐步推导出最终结果。在外层的每一次迭代,也就是对表格每一行的迭代过程中,都会保留上一行的结果数组preResults,并循环计算当前行的结果数组results。
方法的时间复杂度是O(n*w),空间复杂度是(w).需要注意的是,金矿只有5座的时候,动态规划的性能优势还未显示出来。