一、问题
看一个经常被引用的问题
例子1:1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1=?
例子2:1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1=?
问你例子1是多少时,你一个个算后,结果是20
接着问你例子2时,你会马上说出21,为什么第二次这么快?
因为我们将前面已经计算好的结果保存下来,后面计算的时候再次使用,就不需要重复计算了。(一种典型的用空间换时间思想)
二、什么是动态规划(dynamic programming)
动态规划在寻找有重叠子问题的最佳解时,将问题重新组合成子问题,为避免多次解决这些子问题,它们的结果都被计算并存储,先求解出子问题的最优解,再结合子问题的最优解求出整个问题的最优解。
2.1、什么时候用动态规划
动态规划算法通常用于求解具有某种最优性质,子问题重叠的问题。
2.2、如果使用动态规划?主要思考三个问题。
1.定义dp数组,找出dp数组的含义
2.找出 dp[n]和dp[n-1]之间的关系
3.找出数组的初始值dp[0]=? dp[1] = ?
三、通过例子来理解动态规划
1、先来看一个斐波那契数列,求它的第N项:
斐波那契数列的定义如下:
F(0) = 0, F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1.
求解一:
用递归去求解,树形递归的时间复杂度是O(2^n)。
求解二:
用动态规划求解:
/**
* 1.dp[n]就表示第n项的值
* 2.数组关系很明显,dp[n] = dp[n-1]+dp[n-2];
* 3.初始值dp[0] = 0 ;dp[1] = 1;
*/
public class Solution{
public int fib(int n) {
if(n<2){
return n;
}
int[] dp = new int[n+1];
dp[0] = 0;
dp[1] = 1;
for(int i=2;i<=n;i++){ //求解动态数组
dp[i] = dp[i-1]+dp[i-2];
}
return dp[n];
}
}
找出三个主要的关系后,问题的解也迎刃而解。时间复杂度为O(n)。
2.求连续子数组的最大和(LeetCode的42题)
输入一个整型数组,数组里有正数也有负数。数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。
要求时间复杂度为O(n)。
示例1:
输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
考虑三个问题
①dp数组的含义,这里dp[n]我们表示输入数组,第n项结尾的连续数组的最大和。
比如输入nums = [4,-2] ,dp[1] = 2;存的是4+(-2)=2而非4,因为是以n项结尾的连续数组。
②找出数组的之间的关系
<1> 根据数组含义,不难推出
dp[n] = dp[n-1] + nums[n];
<2>如果上式中dp[n-1]<0;那么dp[n] = nums[n]即可
③初始值,dp[0] = nums[0]
求解的所有子数组的和的最大值,从dp数组中遍历即可。
class Solution {
public int maxSubArray(int[] nums) {
if(nums == null) return 0;
int[] dp = new int[nums.length];
dp[0] = nums[0];
int max = dp[0];
for(int i=1;i<dp.length;i++){
if(dp[i-1]<0){
dp[i] = nums[i];
}else{
dp[i] = dp[i-1] + nums[i];
}
max = Math.max(max,dp[i]); //找出dp数组的最大值
}
return max;
}
}
3、最长上升子序列(LeetCode 300)
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
①dp数组含义,dp[n]表示输入数组第n项结尾的最长上升子序列的长度
②数组关系
<1>如果数组第n项的值小于它之前的每一项,它的最长上升子序列的就是它自己,长度为1。
<2>如果数组第n项的值大于它前的某一项 j,即可以接在i项后面,就可以形成一个上升子序列,长度可以在dp[j]上加1;
需要注意的是,我们要接在上升子序列长度最长(dp[j]最大)的后面。
比如nums = [2,3,5] ,根据关系,我们不难得出,dp[0] = 1,dp[1] = 2
而5可以接在2后面,也可以接在3后面,我们肯定接在上升子序列长度最长的后面,即mxa(dp[0],dp[1])后面
③dp数组初始值,dp[0] = 1;(数组第一项只有它自己,长度为1)
最后我们遍历dp数组,即可以找到最长上升子序列的长度。
class Solution {
public int lengthOfLIS(int[] nums) {
if(nums.length == 0) return 0;
int[] dp = new int[nums.length];
dp[0] = 1;
int resMax = dp[0];
for(int i =1;i<dp.length;i++){
int subMaxDp = 0;
for(int j=0;j<i;j++){ //第i项,和i之前的所有项比较
if(nums[i] > nums[j]){
subMaxDp = Math.max(subMaxDp,dp[j]); //找到长度最长的那个
}
}
dp[i] = subMaxDp +1; //要么长度是1,要么在上升子序列最长的后面+1;
resMax = Math.max(resMax,dp[i]); //dp数组中最大值,也即最长的上升子序列
}
return resMax;
}
}
4.01背包问题
有N件物品和一个容量为V的背包。第i件物品的重量是w[i],价值是v[i]。求解将哪些物品装入背包可使价值总和最大
比如有4个物品,背包容量capacity= 8,重量分别为 w = [2,3,4,5],价值分别 v = [3,4,5,6],
输出: 10
解释: 装重量为3和5的两个物品,价值等于10.
之所以叫01背包,是物品要么拿,那么不拿,不存在拿一半的情况(物品完整)。
①定义dp数组,dp[n][c],表示n个物品时,容量为c的最大价值。
②数组关系
<1>如果包的容量比物品小,装不下,价值就等于前n-1个物品。
dp[n][c] = dp[n-1][c]; 等于装与不装。
<2>如果包可以装下该物品,那它的价值就等于该物品价值v[n]加上装过该物品剩余容量(c-w[n])还能装下的最大价值(子问题的解,dp[n-1][c-w[n]])。
即dp[n][c] = v[n]+dp[n-1][c-w[n]];
物品\容量 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1(2,3) | 0 | 0 | 3 | 3 | 3 | 3 | 3 | 3 | 3 |
2(3,4) | 0 | 0 | 3 | 4 | 4 | 7 | 7 | 7 | 7 |
3(4,5) | 0 | 0 | 3 | 4 | 5 | ? | |||
4(5,6) |
说明:第二行0个物品,很明显,不管包的容量为多少,价值都为0;其他根据数组关系不难填出其他。
我们发现装第三个(4,5)物品,包容量为5时,dp[3][5] = v[3] + dp[2][1] = 5 + 0 = 5,价值还不如不装(dp[n-1][c])的价值.
所以取最优解 dp[n][c] = Math.max(v[n]+dp[n-1][c-w[n]],dp[n-1][c]);
<3>初始值
dp[0][c] = 0;//物品为0时,包容量再大,价值也是为0
dp[n][0] = 0;//包容量为0,物品再多也装不了,价值为0
理解这些后,代码也就出来了
public class Solution{
public int ZeroOnePack(int c,int[] w,int[] v){
if(w == null || w.length == 0) return 0;
int[][] dp = new int[w.length][c+1];
for(int i = 1;i<dp.length;i++){ //物品0时,价值为0,从1开始
for(int j = 1;j<=c;j++){ //容量
if(j < w[i]){
/**
* 容量小于物品的重量,装不下
* 我这里w[1] 就是第一个物品,输入的[0,2,3,4,5]
* 如果你输入的[2,3,4,5] ,第一个物品是w[0],注意下标即可
*/
dp[i][j] = dp[i-1][j];
}else { //装的下,取最优解
dp[i][j] = Math.max(v[i]+dp[i-1][j-w[i]],dp[i-1][j]);
}
}
}
return dp[w.length-1][c];
}
}
最后我们在回过头来什么是动态规划:
动态规划就是解决最优化问题的方法,通常用于求解具有某种最优性质,子问题重叠的问题,求解出子问题的最优解,再结合子问题的最优解求出整个问题的最优解。