动态规划

https://blog.csdn.net/qq_40963043/article/details/100765212
https://blog.csdn.net/kongmin_123/article/details/82430985

动态规划的基本思想:

  • 保存子问题的结果,避免重复计算
  • 用额外的数据结构保存
  • 空间换时间
  • 大问题可以由子问题推出(状态转移)

dp是没有递归,因为用数组存储前置结果,用这些结果可以进行递推。所以不用递归,但是也在递推。

例子1:买卖股票的最佳时机

输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。

题解:

  • 状态定义:f(n)表示在第n天卖出,有最大利润

  • 状态转移方程:
    f(n)=max(f(n-1),price[n]-mincost),为什么写成这样
    因为第n天的最大利润,可能是当天的价格-前面几天的最低价格
    也可能是当天的价格-前几天的最低价格,没有前一天-最低价格的利润高
    比如:【1,2,3,4,2】

  • 边界条件:
    f(0)=0 因为第一天,肯定是买入肯定是没有利润的
    mincost=prices[0] 设置mincost为第一天的股票价格

class Solution {
    /**
        状态定义:f(n)表示在第n天卖出,有最大利润
        状态转移方程:
            f(n)=max(f(n-1),price[n]-mincost),为什么写成这样
            因为第n天的最大利润,可能是当天的价格-前面几天的最低价格
            也可能是当天的价格-前几天的最低价格,没有前一天-最低价格的利润高
            比如:【1,2,3,4,2】
        边界条件:
            f(0)=0 因为第一天,肯定是买入肯定是没有利润的
            mincost=prices[0] 设置mincost为第一天的股票价格         
    */
    public int maxProfit(int[] prices) {
        if(prices.length==0){
            return 0;
        }
        //状态定义:定义数组保存存储前置结果
        int[] temp=new int[prices.length];
        //边界条件
        temp[0]=0;
        int mincost=prices[0];

        for(int i=1;i<prices.length;i++){
            mincost=Math.min(mincost,prices[i]);
            //状态转义方程
            temp[i]=Math.max(temp[i-1],prices[i]-mincost);
        }
        //直接取最后一个元素即可,因为Math.max,必然取出来的是最大的利润的
        return temp[prices.length-1];
    }
}

例子2:最大子序和

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

示例:
输入: [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6

题解:

  • 状态定义:f(n) 表示第n个元素的子序和最大
  • 状态定义方程:f(n)=max(nums[n],f(n-1)+nums[n]),表示比较当前【元素比较大】还是【当前元素+当前元素之前的子序】哪个比较大
  • 边界条件:f(0)=nums[0]
   public int maxSubArray(int[] nums) {
        //状态定义
        int[] temp =new int[nums.length];
        //边界条件
        temp[0]=nums[0];

        //定义一个变量方便获取最大值
        int maxValue=nums[0];

        for(int i=1;i<nums.length;i++){
            //状态定义方程
            temp[i]=Math.max(nums[i],temp[i-1]+nums[i]);
            if(maxValue<temp[i]){
                maxValue=temp[i];
            }
        }
        return maxValue;
    }

例子3:青蛙跳台阶问题

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

示例 1:
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1.  1+ 12.  2

题解:

  • 状态定义:f(n) 表示n阶台阶,有f(n)种方法爬到
  • 状态定义方程:
    f(n)=f(n-1)+f(n-2),为什么会是这个表达式呢?
    推导出来的:
    1=>1
    2=>2
    3=>3
    4=>5

    f(n)=f(n-1)+f(n-2)
  • 边界条件:
    f(0)=1
    f(1)=2
class Solution {

    /**

    */
    public int climbStairs(int n) {
        if(n==1 || n==2){
            return n;
        }
        //状态定义
        int[] temp=new int[n];
        //边界条件
        temp[0]=1;
        temp[1]=2;
        for(int i=2 ; i<n ;i++){
            //状态定义方程
            temp[i]=temp[i-1]+temp[i-2];
        }
        return temp[n-1];
    }
}

例子4:把数字翻译成字符串

给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 “a” ,1 翻译成 “b”,……,11 翻译成 “l”,……,25 翻译成 “z”。一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。

示例 1:
输入: 12258
输出: 5
解释: 122585种不同的翻译,分别是"bccfi", "bwfi", "bczi", "mcfi""mzi"

解题思路
用 dp[i] 来表示前 i 个数一共有多少种翻译方法。
假如第 i 个数单独翻译,那么 dp[i] = dp[i - 1]。
假如第 i 个数与第 i - 1 个数组合翻译,那么有两种情况:
两个数的组合处于 [10, 25] 的区间,那么既可以组合翻译,又可以单独翻译,则 dp[i] = dp[i - 2] + dp[i - 1]。
两个数的组合不在 [10, 25] 的区间,那么组合失败,还是得单独翻译,也就是与第 2 点一样。所以 dp[i] = dp[i - 1]。
综上所述,当两个数的组合处于 [10, 25] 的区间,dp[i] = dp[i - 2] + dp[i - 1];当两个数的组合不在 [10, 25] 的区间,dp[i] = dp[i - 1]。

题解:

  • 状态定义:f(n) 表示数字n有f(n)种翻译

  • 状态定义方程:

    • n-1与n两数的组合在【10,25】区间时:
      f(n)=f(n-1)+f(n-2)
      当组合翻译时,次数为f(n-2)
      但n单独翻译时,次数为f(n-1)
      相加,就是总的翻译次数
    • n-1与n两数的组合不在【10,25】区间时:
      f(n)=f(n-1)
  • 边界条件:
    f(0)=1 表示翻译第0个元素,什么也不做也是一种方式
    f(1)=1 表示翻译第1个元素
    例如:
    假设 num = 12,10<12<25,那么 dp[1] = 1,dp[2] = 2,他们的组合处于[10, 25] 的区间,
    所以 dp[2] = dp[1] + dp[0]。很明显 num = 12 有两种翻译方法,而 dp[1] = 1。
    所以 1 + dp[0] = 2,故 dp[0] = 1。

public int translateNum(int num) {
        String numStr=String.valueOf(num);
        //状态定义:长度为numStr还要+1,因为条件边界里的0占用一个
        int[] temp=new int[numStr.length()+1];
        //条件边界
        temp[0]=1;
        temp[1]=1;
        
        for(int i=2;i<=numStr.length();i++){
            String s=numStr.substring(i-2,i);
            int z=Integer.valueOf(s);

            //状态定义方程
            if(10<=z && z<=25){
                temp[i]=temp[i-1]+temp[i-2];
            }
            else{
                temp[i]=temp[i-1];
            }
        }
        return temp[numStr.length()];
    }

例子5:礼物的最大价值

在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?

示例 1:

输入: 
[
  [1,3,1],
  [1,5,1],
  [4,2,1]
]
输出: 12
解释: 路径 13521 可以拿到最多价值的礼物

题解:

  • 状态定义:定义dp为二维数组,dp[i][j],表示从 grid[0][0] 到 grid[i][j] 得到礼物的最大价值

  • 状态转移方程:
    i=0 && j=0 时,为起始元素,dp[0][0] = grid[0][0]
    i=0 && j!=0 时,为矩阵第一行元素,只可从左边到达,
    dp[i][j] = grid[i][j] + dp[i][j-1](当前元素+当前元素的左边元素)
    i!=0 && j=0时,为矩阵第一列元素,只可从上边到达,
    dp[i][j] = grid[i][j] + dp[i-1][j](当前元素+当前元素的上一个元素)
    i!=0 && j!=0时,可从左边或上边到达,
    dp[i][j] = grid[i][j] + max(dp[i-1][j], dp[i][j-1])先比较出左边元素比较大还是上边元素比较大,再加当前元素

  • 边界条件:
    dp[0][0]=grid[0][0] ,即到达单元格(0,0)时能拿到礼物的最大累计价值为 grid[0][0]

class Solution {
    public int maxValue(int[][] grid) {
        //状态定义方程
        int dp[][]=new int[grid.length][grid[0].length];
        //边界条件
        dp[0][0]=grid[0][0];
        //定义一个指针,方便找出最大元素
        int maxValue=grid[0][0];

        for(int i=0;i<grid.length;i++){
            for(int j=0;j<grid[0].length;j++){
                //状态定义方程
                //起始元素
                if(i==0 && j==0){
                   dp[i][j]=grid[0][0]; 
                }
                //为矩阵第一行元素
                else if(i==0 && j!=0){
                    //当前元素+左边元素
                    dp[i][j]=grid[i][j]+dp[i][j-1];
                }//为矩阵第一列元素
                else if(i!=0 && j==0){
                    //当前元素+上面元素
                    dp[i][j]=grid[i][j]+dp[i-1][j];
                }else{
                    //比较出左边元素比较大还是上边元素比较大,再加当前元素
                    dp[i][j]=grid[i][j]+Math.max(dp[i-1][j],dp[i][j-1]);
                }
                //获取最大值
                if(maxValue<dp[i][j]){
                   maxValue=dp[i][j];
                }
            }
        }
        return maxValue;
    }
}

例子6:丑数

我们把只包含质因子 2、3 和 5 的数称作丑数(Ugly Number)。求按从小到大的顺序的第 n 个丑数。

示例:

输入: n = 10
输出: 12
解释: 1, 2, 3, 4, 5, 6, 8, 9, 10, 12 是前 10 个丑数。

说明:
1 是丑数。
n 不超过1690。

因子:255=255*1 1和255都是因子
质数:质数是指在大于1的自然数中,除了1和它本身以外不再有其他因数的自然数。
质因子:因子中是质数的
丑数:只包含质因子 2、3 和 5 的数
例如:6 因子={1,6,2,3},质因子={2,3},因此,6是丑数

题解:

  • 状态定义: dp[n] 表示第 n 个丑数

  • 状态定义方程:
    a 表示 2 倍数字的索引用于 dp[a]*2,
    b 表示 3 倍数字的索引用于 dp[b]*3,
    c 表示 5 倍数字的索引用于 dp[c]*5
    dp[n]=min(dp[a]∗2,dp[b]∗3,dp[c]∗5)
    每次计算之后,如果 2 倍的数字最小,则 a++,如果 3 倍的数字最小,则 b++,
    如果 5 倍的数字最小,则 c++

  • 怎样推导出来的?
    除了第一个丑数外,所有的丑数都是某一个丑数的 2、3 或 5 倍的数字.
    因此,dp[n]是最接近dp[n-1]的一个丑数,索引a,b,c 需满足以下条件:
    dp[a]*2>dp[n-1]>=dp[a-1]*2,即dp[a]为首个乘以2后大于dp[n-1]的丑数
    dp[b]*2>dp[n-1]>=dp[b-1]*3,即dp[b]为首个乘以3后大于dp[n-1]的丑数
    dp[c]*2>dp[n-1]>=dp[c-1]*5,即dp[c]为首个乘以5后大于dp[n-1]的丑数
    所以:dp[n]=min(dp[a]∗2,dp[b]∗3,dp[c]∗5)
    因为要从小到大求第 n 个丑数,所以需要按照顺序每次获取下一个最小的丑数,最终获得第 n 个

  • 条件边界:dp[0]=1,因为第一个丑数是 1

class Solution {
    public int nthUglyNumber(int n) {
        //状态定义
        int[] dp=new int[n];
        //条件边界:
        dp[0]=1;

        //定义三个指针指向dp数组
        int a=0;
        int b=0;
        int c=0;

        for(int i=1;i<n;i++){
            int n1=dp[a]*2;
            int n2=dp[b]*3;
            int n3=dp[c]*5;

            //状态转移方程
            dp[i]=Math.min(Math.min(n1,n2),n3);
			
			//如果当前丑数等于nx,对应的指针应该后移
            if(dp[i]==n1){
                a++;
            } 
            if(dp[i]==n2){
                b++;
            } 
            if(dp[i]==n3){
                c++;
            }
        }
        return dp[n-1];
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值