动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题。
动态规划的过程是
:每次决策依赖于当前状态,又随即引起状态的转移。一个决策序列就是在变化的状态中产生出来的,所以,这种多阶段最优化决策解决问题的过程就称为动态规划。
动态规划常常适用于有重叠子问题和最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法。
最优化原理:
假设问题的最优解所包括的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。无后效性:
即某阶段状态一旦确定。就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响曾经的状态。仅仅与当前状态有关。有重叠子问题:
即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到(该性质并非动态规划适用的必要条件,可是假设没有这条性质。动态规划算法同其它算法相比就不具备优势)。动态规划简单来说就是,利用历史记录,来避免我们的重复计算。而这些历史记录,我们得需要一些变量来保存,一般是用一维数组或者二维数组来保存。下面我们先来讲下做动态规划题很重要的三大基本要素:
一般,只要解决问题的阶段、状态和状态转移决策确定了,就可以写出状态转移方程(包括边界条件)。
根据动态规划的三大基本要素可以设计解题步骤如下:
斐波那契数列(Fibonacci sequence),又称黄金分割数列、因数学家莱昂纳多·斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:0、1、1、2、3、5、8、13、21、34、……
斐波那契数列以如下被以递推的方法定义:
F ( 1 ) = 1 , F ( 2 ) = 1 , F ( n ) = F ( n − 1 ) + F ( n − 2 ) ( n ≥ 3 , n ∈ N ∗ ) F(1)=1,F(2)=1, F(n)=F(n - 1)+F(n - 2)(n ≥ 3,n ∈ N^*) F(1)=1,F(2)=1,F(n)=F(n−1)+F(n−2)(n≥3,n∈N∗)
由上篇文章递归算法递归算法详解——递归算法的三要素以及例题分析
.
可以写出递归形式的求解为
class Solution {
private final int model = 1000000007;
public int fib(int n) {
if (n < 2){
return n;
}
return ((fib(n - 1) % model + fib(n - 2) % model )) % model;
}
}
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。防止溢出。
若用递归法提交答案后可以看出会超出时间限制。
分析可以看出,在递归时会重复计算,如下所示,以F(6)为例:
复杂度分析
空间复杂度优化
若新建长度为 n n n 的 d p dp dp 列表,则空间复杂度为 O ( N ) O(N) O(N) 。
由于 d p dp dp 列表第 i i i 项只与第 i − 1 i−1 i−1 和第 i − 2 i-2 i−2 项有关,因此只需要初始化三个整形变量 s u m , a , b sum, a, b sum,a,b ,利用辅助变量 s u m sum sum 使 a , b a, b a,b 两数字交替前进即可 (具体实现见代码) 。
节省了 d p dp dp 列表空间,因此空间复杂度降至 O ( 1 ) O(1) O(1) 。
class Solution {
public int fib(int n) {
int a = 0, b = 1, sum;
for(int i = 0; i < n; i++){
sum = (a + b) % 1000000007;
a = b;
b = sum;
}
return a;
}
}
复杂度分析
题目描述
输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。
要求时间复杂度为 O ( n ) O(n) O(n)。
示例1:
输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
动态规划解析:
状态定义: 设动态规划列表 d p dp dp , d p [ i ] dp[i] dp[i] 代表以元素 n u m s [ i ] nums[i] nums[i]为结尾的连续子数组最大和。
\qquad 为何定义最大和 d p [ i ] dp[i] dp[i] 中必须包含元素 n u m s [ i ] nums[i] nums[i] :保证 d p [ i ] dp[i] dp[i] 递推到 d p [ i + 1 ] dp[i+1] dp[i+1] 的正确性;如果不包含 n u m s [ i ] nums[i] nums[i] ,递推时则不满足题目的 连续子数组 要求。
转移方程: 若 d p [ i − 1 ] ≤ 0 dp[i-1] \leq 0 dp[i−1]≤0,说明 d p [ i − 1 ] dp[i - 1] dp[i−1] 对 d p [ i ] dp[i] dp[i] 产生负贡献,即 d p [ i − 1 ] + n u m s [ i ] dp[i-1] + nums[i] dp[i−1]+nums[i] 还不如 n u m s [ i ] nums[i] nums[i] 本身大。
\qquad 当 d p [ i − 1 ] > 0 dp[i - 1] > 0 dp[i−1]>0 时:执行$ dp[i] = dp[i-1] + nums[i]$ ;
\qquad 当 d p [ i − 1 ] ≤ 0 dp[i - 1] \leq 0 dp[i−1]≤0 时:执行 d p [ i ] = n u m s [ i ] dp[i] = nums[i] dp[i]=nums[i];
初始状态: d p [ 0 ] = n u m s [ 0 ] dp[0] = nums[0] dp[0]=nums[0],即以 n u m s [ 0 ] nums[0] nums[0] 结尾的连续子数组最大和为 n u m s [ 0 ] nums[0] nums[0] 。
返回值: 返回 d p dp dp 列表中的最大值,代表全局最大值。
class Solution {
public int maxSubArray(int[] nums) {
int res = nums[0];
for(int i = 1; i < nums.length; i++) {
nums[i] += Math.max(nums[i - 1], 0);
res = Math.max(res, nums[i]);
}
return res;
}
}
复杂度分析
:
题目描述
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。
输入输出描述
输入: [0,1,0,2,1,0,1,3,2,1,2,1]
输出: 6
图解模型
直观思想
在暴力方法中,我们仅仅为了找到最大值每次都要向左和向右扫描一次。但是我们可以提前存储这个值。因此,可以通过动态编程解决。
算法流程
代码实现
public int trap(int[] height) {
int len = height.length;
if (len < 2) return 0;
int ans = 0;
int[] max_left = new int[len];
int[] max_right = new int[len];
max_left[0] = height[0];
for (int i = 1; i < len; i++) {
max_left[i] = Math.max(height[i], max_left[i - 1]);
}
max_right[len - 1] = height[len - 1];
for (int i = len - 2; i >= 0; i--) {
max_right[i] = Math.max(height[i], max_right[i + 1]);
}
for (int i = 1; i < len - 1; i++) {
ans += Math.min(max_right[i], max_left[i]) - height[i];
}
return ans;
}
复杂度分析: