聊一聊动态规划

一、问题
看一个经常被引用的问题

例子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]];

物品\容量012345678
0000000000
1(2,3)003333333
2(3,4)003447777
3(4,500345?   
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];
    }
}

最后我们在回过头来什么是动态规划:

动态规划就是解决最优化问题的方法,通常用于求解具有某种最优性质,子问题重叠的问题,求解出子问题的最优解,再结合子问题的最优解求出整个问题的最优解。

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值