动态规划应该用于最优化问题算法
最优化问题指的是,解决一个问题可能有多种可行的值来解决问题,可是咱们须要一个最优的(最大或者最小)值数组
动态规划适用于子问题不是独立的状况,即各个子问题之间包含公共的子问题。动态规划对每一个子问题只计算一次,保存其计算结果到"一张表",重复利用,从而优化执行。bash
分治法则是把一个大的问题划分红一些独立的子问题,递归求解子问题的状况;贪心算法则是会先选择当时看起来是最优的选择,而后再求解一个结果的子问题post
以斐波那契数列为例。通常的求解方式为递归,运行时间为 (
),空间为O(1)优化
fib(n):
if n<=2:f=1;
else: f= fib(n-1)+fib(n-2);
return f;
复制代码
能够简要分析下这个执行过程:要去求解 fib(n),首先要知道fib(n-1)和fib(n-2),要计算fib(n-1)则须要知道fib(n-2)和fib(n-3),要计算fib(n-2)则须要知道fib(n-3)和fib(n-4),依此类推。
很明显能够看到:若是计算出了fib(n-1)的子问题和fib(n-2)的子问题存在依赖性
,要计算fib(n-1)必然要计算fib(n-2);同时若是复用了fib(n-2)那么当计算fib(n-1)就很是简单,直接相加便可。这也就是复用子问题处理结果
。ui
fib(n):
memo={}
if n in memo:return memo[n];
if n<=2:f=1;
else f=fib(n-1)+fib(n-2);
memo[n]=f;
return f;
复制代码
分析可知:memo的存在使得实际产生调用的只有 fib(1) .... fib(n),共n次,其他的直接从memo中获取,使用常量的时间。可得运行时间为,空间为O(n)。这种方式仍然能够优化到使用常量的空间,由于实际上只须要记住最初的两个值便可。spa
int fib(int n){
if(n<=2){
return 1;
}
int f1=1,f2=1;
for(int i=3;i<=n;i++){
int f=f1+f2;
f1=f2;
f2=f;
}
return f2;
}
复制代码
它的运行时间为,空间O(1);
这种计算方式,它其实是至关于进行了拓扑排序,即我只有先执行完fib(n-2),再执行fib(n-1),而后才执行fib(n) code
最终去解决了原来的问题,总耗时为
子问题个数 * 每一个子问题的处理时间=O(n)
cdn
斐波那契数列中的拓扑排序,它本质上相似拓扑排序的DAG最短路径blog
每条边都访问了一遍,而后初始化了每一个顶点的值,它的运行时间为O(V+E)
计算过程能够看到:
SABCDEFG
的拓扑顺序进行处理通常的图拓扑排序为
就是须要递归调用处理的部分
对于DAG:每一个子问题的处理时间为 indegree(t)+O(1)
indegree(t):入度数也就是相似(u,t)边的数量,须要去遍历全部t的入边
O(1):判断是否是有入边
总共的执行时间为
当图中有环的时候求最短路径产生的问题
要求s到v的最短路径
,首选须要去求
,而后是
,到b节点有两条路径:
和
,此时去memo中查
是不存在的,又会这回查询,致使了一个死循环
![]()
解决图中有环的时候求最短路径的问题
方式是去环,将原来的图一层一层的展开。
假设从s到v须要的路径为k步,那么能够获得=
,当k递减到0的时候,其实也就是从s到s自己
所须要的展开层数为:|V|-1 对于求最短路径来说,最长不能超过|V|-1,不然就是成环,会形成循环的状况(从0开始的计数),这就是为何Bellman-Ford的外层循环是 |V|-1 ![]()
每层的节点数为全部的节点。那么总共的节点数为|V'|=|V|(|V|-1)+1=O(
),边数是|E'|=|E|(|V|-2)+1=O(VE)。转换后的图是DAG图,那么实际上的时间为O(V'+E')=O(VE)。这也就是
从动态规划的角度去看Bellman-Ford算法
节点的数目是1个源点,边的数目是每多一层实际上就多了加了一遍全部的边。
从斐波那契和最短路径的例子看出,要使用最短路径,须要确保子问题之间是互相依赖的,这样可以重复利用子问题产生的结果,而要去重复利用子问题,那首要条件是找到子问题是什么?而后在多个子问题之间选择最优的结果,并按照拓扑排序的顺序进行计算
计算子问题的数量
计算选择的数量
计算单个子问题所须要处理的时间
计算总耗时
最终解决原有的问题,它消耗的时间为: 子问题的数量 * 每一个子问题处理所须要时间
总的来讲就是:尝试全部可能的子问题的结果,将最好的可能子结果存储下来,而后重复利用已经解决的子问题,递归去解决全部的问题(思考+记忆+递归)
给定一个整数数组 nums ,找到一个具备最大和的连续子数组(子数组最少包含一个元素),返回其最大和。好比
输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
复制代码
乍看之下,要求连续最大和,首先得计算出子串的最大和,才能去计算原始数组的最大和,也就是说
public int maxSubArray(int[] nums) {
int max=Integer.MIN_VALUE;
Map<Integer,Integer> mem=new HashMap<>();
for(int i=0;i<nums.length;i++){
//按照必定顺序执行
int v=dp(i,nums,mem);
if(v>max){
max=v;
}
}
return max;
}
public int dp(int i,int[] nums,Map<Integer,Integer> mem){
Integer v=mem.get(i);
if(v!=null){
//重用子问题的结果
return v;
}
int sum = nums[i];
if(i==nums.length-1){
mem.put(i,sum);
return sum;
}
//先计算子问题
int cSum = dp(i+1,nums,mem);
int tempV = sum+cSum;
//明确父子问题之间的关系,存储新的子问题结果
if(tempV<sum){
mem.put(i,sum);
return sum;
}
mem.put(i,tempV);
return tempV;
}
复制代码
分析能够看到,它的执行为须要遍历一遍整个的数组,而后要去计算的子问题包括 n-1,n-2,..,1,耗时为 O(n+n-1)=O(2n)=O(n),同时须要一个O(n)的空间存储。
public int maxSubArray(int[] nums) {
int max=nums[0];
int currMax=nums[0];
for(int i=1;i<nums.length;i++){
int temp=nums[i]+currMax;
if(temp>nums[i]){
currMax=temp;
}else{
currMax=nums[i];
}
if(max<currMax){
max=currMax;
}
}
return max;
}
复制代码
经过这种方式,使用的空间为O(1),时间就是O(n)。 因而可知动态规划自己只是一种解决问题的思想,并非说动态规划获得的最优解就是解决问题的最佳方案