进军动态规划(持续更新):

动态规划的定义:

它的思想是将问题分解成小问题,并先解决这些小问题;

理解动态规划的路线:因为这个动态规划仅靠语言我感觉很难讲清楚,我干脆吧自己的理解路线写出来你们自己去看资料,这篇博文主要更新例题:

学习路线:
1.先去看算法导论的动态规划篇:重点理解背包问题
2.通过bilibili的视频完成理解背包问题:链接如下 https://www.bilibili.com/video/BV1K4411X766
3.看《挑战程序算法竞赛》的代码:来理解动态规划的代码,并进一步理解记忆化搜索

我对背包问题的理解: 1.每次再放东西之前你都要比较一下将不将该东西放进背包,如果放进背包的化就要为该物体预留足够的空间,并且如果有剩余的空间就找出剩余空间可装入的商品的最大价值,这个地方就体会了为什么要先解决小问题的好处,如果不将该物品装入背包则背包的价值不变,跟上一个物品一样

动态规划与分治算法的区别:
分治法与动态规划主要共同点:

  1. 二者都要求原问题具有最优子结构性质,都是将原问题分而治之,分解成若干个规模较小(小到很容易解决的程序)的子问题.然后将子问题的解合并,形成原问题的解.
  2. 分治法与动态规划实现方法:
    ① 分治法通常利用递归求解.
    ② 动态规划通常利用迭代法自底向上求解,但也能用具有记忆功能的递归法自顶向下求解.
  3. 分治法与动态规划主要区别:
    ① 分治法将分解后的子问题看成相互独立的.
    ② 动态规划将分解后的子问题理解为相互间有联系,有重叠部分.

好了其他的不多说了,上例题:
例题一: 给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。(力扣:53)
在这里插入图片描述
官方解释:
在这里插入图片描述
我的理解及相关代码细节:

//开始进军动态规划,其实这道题也是一道类似解决背包问题的一部分的题目,就是判断f(i-1)要不要加上num[i]的问题
//思路:如何判断是否要加上就是比较f[i-1]+num[i]与num[i]的大小问题,如果f[i-1]+num[i]<nums[i]就说明之前的片段是累赘
//可以从该片段开始重新找子序列
class Solution {
    public int maxSubArray(int[] nums) {
       //创建一个变量保存前i的节点的和
       int presum=0;
       //窜关键一个变量来记录最大值
       int max=nums[0];
       for(int i=0;i<nums.length;i++){
           if(presum+nums[i]>=nums[i]){
               presum=presum+nums[i];
           }
           else{
               presum=nums[i];
           }
           if(presum>max){
               max=presum;
           }
       }
    //    presum=Math.max(presum+nums[i],nums[i]);
    //    max=Math.max(presum,max);
    //    }
       return max;
    }
}

例题2:假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?注意:给定 n 是一个正整数。(力扣:70)
在这里插入图片描述

//理解:如果将该问题转化为数学问题的话:就是斐波数列,可如何理解?
//0阶为起点
//一阶楼梯:一种方法:0-1
//二阶楼梯:两种方法:0-1-2;0-2
//三阶楼梯:方法我们不管到达三阶楼梯 只能:1-3;2-3也就是说到达三阶楼梯的方法为到达一阶与及到达2阶之和
//以此类推退:可以得到一个动态转移方程:f(n)=f(n-1)+f(n-2)
//转化为数学问题就是:斐波那契数列
/*
class Solution {
    public int climbStairs(int n) {
        int p=0;
        int q=1;
        int result=0;
      for(int i=0;i<n;i++){
         result=p+q;
         p=q;
         q=result;
      }
      return result;
    }
}
*/
//可能上面的代码没有体现到动态规划的精髓
class Solution {
    public int climbStairs(int n) {
        //防止数组越界
        if(n<=1){
            return n;
        }
        int[] dp=new int[n+1];
        dp[0]=1;
        dp[1]=1;
        dp[2]=2;
        for(int i=3;i<n+1;i++){
            dp[i]=dp[i-1]+dp[i-2];
        }
        return dp[n];
    }
    }

例子3:给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
(力扣:121)
在这里插入图片描述

//使用动态规划的思路:
//首先遇到这种问题我们可以先想想当前问题的结果,与之前的某个过程的的状态是否有关
//就例如这题:如果当天没有持股有两种可能 1.昨天也没有股票但手里肯定有>=0的现金数
//                                    2.昨天刚进手的股票,今天给卖了
//所以这天最大的利益:就是昨天所持有的现金数与今天卖出股票后所赚取的先进数的最大值
//             如果当天持有股票也有量种可能:1.昨天就有股票,当天什么都没干(当天的现金数就和昨天一样)
//                                        2.昨天刚买进的股票 (现金数为昨天的股票值的相反数)   
//那么当天的这两种状态如何选择:当然是选择现金数多的状态
//首先是当天不持股的状态: 将前一天不持股的状态所拥有的现金数与前一天持股今天卖股所获得的现金数相比较
//当天持股的状态:前一天持股所欠的钱,与前一天不持股今天买股所欠的钱相比较,谁欠的少就取谁                          
 class Solution {
    public int maxProfit(int[] prices) {
        //如果只有一天那利润肯定只能为0
        if(prices.length<=1){
            return 0;
        }
        //初始化第一天的状态
        int[][] dp=new int[prices.length][2];
        //第一天不持有股票现金数为0
        dp[0][0]=0;
        //第一天持有股票
        dp[0][1]=-prices[0];
        //由第一天的状态推出其他天的状态
        for(int i=1;i<prices.length;i++){
            //第i天不持股的状态,通过昨天不持股的状态的现金数和今天卖股所获的现金数想比较
            dp[i][0]=Math.max(dp[i-1][0],dp[i-1][1]+prices[i]);
            //上面已经由很详细的解释了,这里不过多解释
            dp[i][1]=Math.max(dp[i-1][1],-prices[i]);
        }
        //返回最后一天手上不持股的现金数
        return dp[prices.length-1][0];
    }
 }

兄弟们,我又找到好题目了,下面这两道题对于理解动态规划属实是有很大的帮助:

例子4:三步问题。有个小孩正在上楼梯,楼梯有n阶台阶,小孩一次可以上1阶、2阶或3阶。实现一种方法,计算小孩有多少种上楼梯的方式。结果可能很大,你需要对结果模1000000007。(面试题 08.01. 三步问题)
在这里插入图片描述

第一眼印象:标准的动态规划问题,与解决二步上楼的思路是一致的,唯一的坑爹的地方就是,整数相加溢出的问题,所以我们在进行两数相加后一定要进行取模运算

代码及细节如下:

//跟之前的楼梯问题应该是一个思路
//设置一个数组保存每层阶梯的方法    dp[0]=0;
//当n=1 直接一步上楼  dp[1]=1
//当n=2 有两种方法,从一阶跨一步或者直接两步  dp[2]=2
//当n=3时,1:直接一步上楼 2:从一阶跨两部 3:从2阶跨一步 dp[3]=1+dp[2]+dp[1]=4
//再次设想dp[4] 1:从dp[1]跨3级 2:从dp[2]跨两级 3:从dp[3] 跨一级  所以dp[4]=d[3]+dp[2]+dp[1]
//由此类推出整道题的动态规划转移方程:f(n)=f(n-1)+f(n-2)+f(n-3)
//这道题就类似于之前的那一道
//这里我们直接使用迭代求解
class Solution {
    public int waysToStep(int n) {
        //避免出现异常
        if(n<=2){
            return n;
        }

        //设置一个数组保存每个阶级的状态
        int[] dp=new int[n+1];
        //设置初值
        dp[0]=0;
        dp[1]=1;
        dp[2]=2;
        dp[3]=4;
        for(int i=4;i<=n;i++){
            //这里坑爹的就是避免两数相加越界
          dp[i]=(dp[i-1]+dp[i-2])%1000000007+dp[i-3];
          dp[i]=dp[i]%1000000007;
        }
        return dp[n];
    }
}

例子5:数组的每个下标作为一个阶梯,第 i 个阶梯对应着一个非负数的体力花费值 cost[i](下标从 0 开始)。每当你爬上一个阶梯你都要花费对应的体力值,一旦支付了相应的体力值,你就可以选择向上爬一个阶梯或者爬两个阶梯。请你找出达到楼层顶部的最低花费。在开始时,你可以选择从下标为 0 或 1 的元素作为初始阶梯。
在这里插入图片描述
第一印象:楼梯问题结合背包问题的思路,如何体现楼梯问题的思路:因为每次只能跨一步或者两步上楼,所以到达该楼层的方式可以由之前的dp[状态推出],其次就是花费最小问题就体会了背包问题的思路:每次可以跨一阶或二阶,跨那个,所以前一个,前两个,踩那个?那个花费小踩那个,怎么算,不就是背包问题吗?

代码及细节如下

//同样十分典型的动态规划问题
//下面我们先来整理一下思路
//当阶梯数少于2个时,我们要登顶就选择cost[0]和cost[1]我们的花费为0
//当有3个阶梯时,我们可以从cot[0]跨两步上楼,也可以从cost[1]跨一步上楼,关键就看谁的花销小
//当有四个阶梯时,我们可以选择从cost[1]跨一步上楼,也可以选择从cost[2]跨一步到楼顶,如何选择min(dp[1]+cost[1],dp[2]+cost[2])
class Solution {
    public int minCostClimbingStairs(int[] cost) {
        //同样设置一个动态规划数组来保存到达每个阶梯所需要的最小体力值
        int n=cost.length;
        int[] dp=new int[n+1];
        //为dp数组设置初值
        dp[0]=0;
        dp[1]=0;
        for(int i=2;i<=n;i++){
            dp[i]=Math.min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);
        }
        return dp[n];
    }
}

例题六:给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。(力扣:322)
在这里插入图片描述

理解:动态规划的核心思想是利用小问题的最优解,来得到大问题的最优解,就例如这道硬币问题11=5+dp[6],或者11=1+dp[10],或者11=2+dp[9]
这就是动态规划转移方程,如何选择,通过比较呗

//写着是广度搜索,实际是动态规划
//第一感觉也是动态动态规划就不追求其他的算法了
class Solution {
    public int coinChange(int[] coins, int amount) {
    //首先设置一个动态规划数组保存从0到amout所需的最小银币数
    int[] dp=new int[amount+1];
    //为每个dp赋初值,设为amout+1因为即使有1,amout+1也是最多的硬币数量了
    //这里涉及了一个新的api我之前都没用过,如果要为数组设置不为0的初值可以使用Arrays.fill(int[],int);
    Arrays.fill(dp,amount+1);
    //初始化动态规划数组
    dp[0]=0;
    //最外层循环是为了为每个amout都赋值
    for(int i=1;i<dp.length;i++){
        for(int j=0;j<coins.length;j++){
            if(i>=coins[j]){
                dp[i]=Math.min(dp[i],dp[i-coins[j]]+1);
            }
        }
    }
    return dp[amount]==amount+1?-1:dp[amount];
    }
}

例题7:给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, …)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
给你一个整数 n ,返回和为 n 的完全平方数的 最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。(力扣:279)
在这里插入图片描述

//看了看题解的确又是动态规划的,是银币问题的另一幅面孔,只不银币换成了1到sqrt(n)的平方数
class Solution {
    public int numSquares(int n) {
       int max=(int)Math.sqrt(n);
       //设置一个动态规划数组
       int[] dp=new int[n+1];
       //将初值都设置为n+1
       Arrays.fill(dp,n+1);
       dp[0]=0;
       for(int i=1;i<dp.length;i++){
           for(int j=1;j<=max;j++){
               int temp=(int)Math.pow(j, 2);
               if(i>=temp){
                   dp[i]=Math.min(dp[i],dp[i-temp]+1);
               }
           }
       }
       return dp[n];
    }
}

例题8:小扣打算给自己的 VS code 安装使用插件,初始状态下带宽每分钟可以完成 1 个插件的下载。假定每分钟选择以下两种策略之一:

使用当前带宽下载插件
将带宽加倍(下载插件数量随之加倍)
请返回小扣完成下载 n 个插件最少需要多少分钟。

注意:实际的下载的插件数量可以超过 n 个

在这里插入图片描述

// 思路:一开始还没有理解题目,后来才理解了,加倍的意思就是那一分钟不加倍,下一分钟的下载量翻倍
// 所以直接翻倍直到最后下载量超越了要下载的插件,然后化一分钟下载即可,然后我就想了想那肯定是翻倍
// 然后再下载,谁会一个一个的下载?
// 解法1:直接使用数学方法
// class Solution {
//     public int leastMinutes(int n) {
//     //   求出翻倍超越插件下载数量所需要的时间
//     int load =1;
//     int time=0;
//     while(load<n){
//       load=load*2;
//       time++;
//     }
//     return time+1;
//     }
// }
// 解法2:这道题既然安排到了动态规划这里,哪我也使用动态规划的思路意思一下吧
// 首先我先大致讲讲动态规划的大致思路:就拿四个下载量来举个例子
// 下载四个插件的方法有:一个一个的下载:所以dp[4]=dp[3]+1
// 翻倍后下载:第一天不下载:下载量2个  第2天不下载:下载量4个     第3天直接下载
// 也就是下载量为n/2的那天不下载然后花一天下载  dp[4]=dp[2]+1
// 所以先加倍的方程为:零件数目减半所花的时间加上1天 因为/是整除所以下加上1 可以想象3个零件减半所化的时间应该是2个零件而且不是一个 依次类推 dp[i]=d[(i+1)/2]+1;
// 所以就可以的到动态规划转移方程:dp[i]=Math.min(dp[i-1]+1,dp[(i+1)/2]+1)
class Solution {
    public int leastMinutes(int n) {
        // 设置一个动态规划数组
        int[] dp=new int[n+1];
        dp[1]=1;
        for(int i=2;i<=n;i++){
           dp[i]=Math.min(dp[i-1]+1,dp[(i+1)/2]+1);
        }
        return dp[n];
    }
}

例题9:一个有名的按摩师会收到源源不断的预约请求,每个预约都可以选择接或不接。在每次预约服务之间要有休息时间,因此她不能接受相邻的预约。给定一个预约请求序列,替按摩师找到最优的预约集合(总预约时间最长),返回总的分钟数。(力扣:面试题17.16)

注意:本题相对原题稍作改动
在这里插入图片描述
解法1:动态规划思路+暴力解法

// 思考了一番:同样是动态规划思路不过我没有找到动态规划方程,直接暴力代替动态规划方程
//算法真的是一种奇妙的东西,不同的思路就会诞生不同的解法,也不知道我上面的解法算不算一种变异动态规划
//我的dp[i]则表示包含用户i的最长用户时长
class Solution {
    public int massage(int[] nums) {
        // 避免出现异常
        if(nums.length==0){
            return 0;
        }
        if(nums.length==1){
            return nums[0];
        }
    //  创建一个动态规划数组:dp[i] 表示包含顾客i的最长预约时常
    int[] dp=new int[nums.length];
    // 初始化动态规划数组
    // 为了统一我让dp[0] 表示第一位顾客
     dp[0]=nums[0];
     dp[1]=nums[1];
     for(int i=2;i<nums.length;i++){
         int maxtime=0;
        //  找出前i-1个用户中的最长预约时长
        //  这里为什么是i-1?因为相邻的用户不能选
         for(int j=0;j<i-1;j++){
             if(dp[j]>maxtime){
                 maxtime=dp[j];
             }
         }
         dp[i]=maxtime+nums[i];
     }
    //  找出最长的预约时长
    int max=dp[0];
     for(int i=0;i<dp.length;i++){
         if(dp[i]>max){
             max=dp[i];
         }
     }
     return max;
    }
}

解法2:
正经动态规划:

//算法真的是一种奇妙的东西,不同的思路就会诞生不同的解法,也不知道我上面的解法算不算一种变异动态规划
// 现在我们再来看一看正经的动态规划:首先与我解法的最大的区别就是dp所代表的含义不同
// 这里动态规划的dp[i]表示前n个用户所能预约到的最长时长,而我的dp[i]则表示包含用户i的最长用户时长
// 他的动态规划转移方程:dp[i]=Math.max(dp[i-1],dp[i-2]+nums[i]);
//怎么理解:首先获取前n个用户的最长预约时长的方法有两种:
// 1:不预约第i个用户,那么前i个用户的最长时长就为dp[i-1]
// 2:预约第i个用户,那么第i-1个用户就不能预约了,最长的预约时长就为:dp[i-2]+nums[i]
// 哪我到底是预约还是不预约?比较一番就行,由此得到动态规划转移方程
class Solution {
    public int massage(int[] nums) {
        // 避免出现异常
        if(nums.length==0){
            return 0;
        }
        if(nums.length==1){
            return nums[0];
        }
        // 创建一个动态规划数组
        int[] dp=new int[nums.length];
        // 初始化动态规划数组
        dp[0]=nums[0];
        dp[1]=Math.max(nums[0],nums[1]);
        for(int i=2;i<nums.length;i++){
            dp[i]=Math.max(dp[i-1],dp[i-2]+nums[i]);
        }
        return dp[nums.length-1];
    }
}

例题10:你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。(力扣:198)
在这里插入图片描述

// 有没有兄弟和我一样,按摩完了。过来偷东西
class Solution {
    public int rob(int[] nums) {
        // 避免异常
        if(nums.length==0){
            return 0;
        }
        if(nums.length==1){
            return nums[0];
        }
    // 设置一个动态规划数组dp[i] 表示前i间房子所能偷到的最大金额
    int[] dp=new int[nums.length];
    // 初始化动态规划数组
    dp[0]=nums[0];
    dp[1]=Math.max(nums[0],nums[1]);
    for(int i=2;i<nums.length;i++){
        dp[i]=Math.max(dp[i-1],dp[i-2]+nums[i]);
    }
    return dp[nums.length-1];
    }
}

例题11
给你一个非负整数数组 nums ,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
你的目标是使用最少的跳跃次数到达数组的最后一个位置。
假设你总是可以到达数组的最后一个位置。
在这里插入图片描述

// 跳跃问题:真的是十分典型的动态规划题,其实这道题就类似于硬币问题与简单阶梯问题的结合
// nums = [2,3,1,1,4]就拿这个例子说明一下思路:
// 到达第0阶梯,无需跳跃,所以dp[0]=0;
// 到达第1阶梯,只能从第0阶梯跨一步,所以dp[1]=1;
// 到达第2阶梯,因为0阶可以跨2步,1阶可以跨3步,所以从0 , 1都能到达2,怎么选dp[2]=Math.min(dp[0],dp[1])
// 到达第3阶梯,1阶可以跨3步,所以从1,2都能到达3,怎么选dp[3]=Math.min(dp[1],dp[2])
// 所以总而言之,如何取得i阶梯的dp?就是从能到达i阶梯的途径中选出跳跃数最少的路径
class Solution {
    public int jump(int[] nums) {
        // 避免异常的产生
        if(nums.length==1){
            return 0;
        }
        // 创建一个动态规划数组记录到达该位置所需要的最小跳跃数
        int[] dp=new int[nums.length];
        // 初始化动态规划数组
        dp[0]=0;
        dp[1]=1;
        for(int i=2;i<nums.length;i++){
            // 创建一个变量来记录最小跳跃数
            int min=Integer.MAX_VALUE;
            for(int j=0;j<i;j++){
                // 找出j阶梯与i阶梯的差距,方便判断从j阶梯是否能到i阶梯
                int cj=i-j;
                // 判断从j阶梯是否能到i阶梯
                if(cj<=nums[j]){
                   min=Math.min(dp[j]+1,min);
                }
            }
            dp[i]=min;
        }
        return dp[nums.length-1];
    }
}

例题12:一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

问总共有多少条不同的路径?(力扣:62)
在这里插入图片描述
在这里插入图片描述

理解:这道算法的主要细节在于直接初始话第一行和第一列的dp数组的值

/*
// 这种题目老子一律暴搜招呼:dfs
class Solution {
    int desr;
    int desc;
    // 创建一个变量来记录,路径的数量
    int path=0;
    public int uniquePaths(int m, int n) {
        this.desr=m;
        this.desc=n;
        // 创建一个数组方便进行dfs
        int[][] a=new int[m][n];
        dfs(a,0,0);
        return path;
    }
    public void dfs(int[][] a,int sr,int sc){
        // 递归的出口
        if(sr<0||sr>=desr||sc<0||sc>=desc){
            return ;
        }
        if(sr==desr-1&&sc==desc-1){
            path++;
            return ;
        }
        dfs(a,sr+1,sc);
        dfs(a,sr,sc+1);
    }
}
*/
// 递归超时了,使用动态规划,好像思路也挺清晰的
//思路:到达每个点都有两种方法:1:从该点的正上方到达该点     2.从该点的正左边到达该点
// 那么该点的dp不久为这两个方向的dp之和
class Solution {
    public int uniquePaths(int m, int n) {
    // 创建一个动态规划数组,保存到达该点的方法
       int[][] dp=new int[m][n];
    //  初始化动态规划数组,因为第一行和第一列的dp肯定都为1,所以可以直接赋初值
        Arrays.fill(dp[0],1);
        for(int i=0;i<m;i++){
            dp[i][0]=1;
        }
       for(int i=1;i<m;i++){
           for(int j=1;j<n;j++){
                  dp[i][j]=dp[i][j-1]+dp[i-1][j];
           }
       }
       return dp[m-1][n-1];
    }
}

例题13:一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。

现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用 1 和 0 来表示。(力扣:63)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
理解:这道算法的主要细节在于直接初始话第一行和第一列的dp数组的值
同事如果该点是障碍,使其dp值为0,这样就不会影响其他的dp数组,也不用分类讨论

/*
// 这种题目老子一律暴搜招呼:dfs
// 还是超出时间限制了
class Solution {
    int desr;
    int desc;
    // 创建一个变量来记录,路径的数量
    int path=0;
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        this.desr= obstacleGrid.length;
        this.desc= obstacleGrid[0].length;
        dfs(obstacleGrid,0,0);
         return path;
    }
        public void dfs(int[][] a,int sr,int sc){
        // 递归的出口
        if(sr<0||sr>=desr||sc<0||sc>=desc){
            return ;
        }
        if(a[sr][sc]==1){
            return;
        }
        if(sr==desr-1&&sc==desc-1){
            path++;
            return ;
        }
        dfs(a,sr+1,sc);
        dfs(a,sr,sc+1);
     }
}
*/
// 关键时候还是得看我动态哥
class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int row=obstacleGrid.length;
        int col=obstacleGrid[0].length;
        //  避免特殊情况
        if(obstacleGrid[0][0]==1){
            return 0;
        }
        // 创建一个动态规划,保存到达该点的方法方法
        int[][] dp=new int[row][col];
        // 初始化动态规划数组第一行,在第一行中如果出现那么后面的dp都为0
        int i=0;
        while(i<col&&obstacleGrid[0][i]!=1){
           dp[0][i]=1;
            i++;
        }
        while(i<col){
            obstacleGrid[0][i]=0;
            i++;
        }
        //  初始化动态规划数组第一列
        int k=0;
        while(k<row&&obstacleGrid[k][0]!=1){
            dp[k][0]=1;
            k++;
        }
        while(k<row){
           dp[k][0]=0;
            k++;
         }
        // 初始化完动态规划数组后为其他的dp赋值
        for(i=1;i<row;i++){
            for(int j=1;j<col;j++){
            //如果该点是障碍,就不设置到达该点的路劲数,这样就不会影响其他
            //点的路径数同时也不许要分类讨论
                if(obstacleGrid[i][j]!=1)
                dp[i][j]=dp[i-1][j]+dp[i][j-1];
            }
        }
         return dp[row-1][col-1];
    } 
}

例题14:给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

说明:每次只能向下或者向右移动一步。(力扣:64)

在这里插入图片描述

//典型的动态规划数组
// 到达每个方格都有两条路径选谁?
// 肯定是谁小选谁,所以dp[i][j] 是与dp[i-1][j] dp[i][j-1]相关的
// 所以动态规划转移方程: dp[i][j]=Math.min(dp[i-1][j],dp[i][j-1])+grid[i][j];
class Solution {
    public int minPathSum(int[][] grid) {
        int row =grid.length;
        int col=grid[0].length;
    //  创建一个动态规划数组
        int[][] dp=new int[row][col];
    // 初始化动态规划数组
        dp[0][0]=grid[0][0];
        for(int i=1;i<col;i++){
            dp[0][i]=dp[0][i-1]+grid[0][i];
        }
        for(int i=1;i<row;i++){
            dp[i][0]=dp[i-1][0]+grid[i][0];
        }
    //为dp数组的其他方格赋值
    for(int i=1;i<row;i++){
        for(int j=1;j<col;j++){
            dp[i][j]=Math.min(dp[i-1][j],dp[i][j-1])+grid[i][j];
        }
    }
    return dp[row-1][col-1];
    }
}

例题15:给你一个整数 n ,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。(力扣:96)
在这里插入图片描述
理解:充分的利用了二叉搜索树的性质

/*
//妈的sb超时了玩尼玛
//这道题的解题思路肯定是与昨天那个求不同额插树的思路是相同的,就是逐渐改变左右二叉树节点的个数,想了想就链返回值也一样
class Solution {
    public int numTrees(int n) {
       List <TreeNode> list=creatTree(1,n);
       return list.size();
    }
public  List<TreeNode> creatTree(int start,int end){
        //创建一个集合用于存储每个二叉树的节点,为什麽要定义为局部变量.而且为什么要放在这个地方,是因为他最后返回的是递
        //归 第一层,也就是包含所有的节点的二叉树,同时将七设为局部变量的原因是它的每一层递归都需要保存
        List <TreeNode> list=new ArrayList<TreeNode>();
        //如果left<right的话,说明该子树没有一个节点但你依旧要添加一个空节点过去为什么,因为要方便后面的节点组合
        if(start>end){
           list.add(null);
           return list;
        }
        for(int i=start;i<=end;i++){
            List <TreeNode> left=creatTree(start,i-1);
            List <TreeNode> right=creatTree(i+1,end);
            for(TreeNode l:left){
                for(TreeNode r:right){
                   TreeNode root=new TreeNode(i);
                   root.left=l;
                   root.right=r;
                   list.add(root);
                }
            }         
        }
         return list;
    }
}
*/
// 又利用到了二叉搜索树的性质,以后要好好利用一下搜索树的性质,这个性质也是这道题为什么能用dp的原因
// 你想一想4,5,6,7形成的二叉搜索树的数量,与1,2,3,4形成的二叉搜索树的数量时一致的,所以形成的二叉搜索树的数量
// 是与有序序列的长度有关,那么我们在来思考一下在1,2,3,4,5,6,7中以3为根节点的二叉搜索数的数量
// 是不是等于1,2形成的二叉树的数量*4,5,6,7形成的二叉树的数量,那么长度为7的序列所能行成的二叉树的数量应该
// 就是以每个数为节点形成的二叉搜索树的数量的总和,如果你理解了这些,那么这道题应该就能做出来了
class Solution {
    public int numTrees(int n) {
        // 设置一个动态规划数组,记录表示每个程度所能生成的二叉树的数量
        int[] dp=new int[n+1];
        // 初始化动态规划数组
        // 为什么0长度是1,因为空节点也是树的组陈部分
        dp[0]=1;
        dp[1]=1;
        // 最外层循环表示序列的长度
        for(int i=2;i<=n;i++){
            int sum=0;
            // 这层循环表示在固定总长度的情况下,以不同的根节点所能形成的二叉树
            for(int j=1;j<=i;j++){
                sum=sum+dp[j-1]*dp[i-j];
            }
            dp[i]=sum;
        }
        return dp[n];
    }
}

例题16:给定一个三角形 triangle ,找出自顶向下的最小路径和。

每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。也就是说,如果正位于当前行的下标 i ,那么下一步可以移动到下一行的下标 i 或 i + 1 。(力扣:120)

在这里插入图片描述

/*
官解:直接将第一列元素,与每列的最后一个元素额外处理,确实也挺简单的,我的做法的限制条件难找
        for (int i = 1; i < n; ++i) {
            f[i][0] = f[i - 1][0] + triangle.get(i).get(0);
            for (int j = 1; j < i; ++j) {
                f[i][j] = Math.min(f[i - 1][j - 1], f[i - 1][j]) + triangle.get(i).get(j);
            }
            f[i][i] = f[i - 1][i - 1] + triangle.get(i).get(i);
        }
*/
class Solution {
    public int minimumTotal(List<List<Integer>> triangle) {
        //  将集合中的元素放入到动态规划数组中去
        int r=triangle.size();
        int c=triangle.get(r-1).size();
        int[][] tar=new int[r][c];
        // 将集合中的元素放进数组中去  
         int k=0;    
        for(List<Integer> temp:triangle){          
            int j=0;
            for(int temp1:temp){
                tar[k][j]=temp1;
                j++;
            }
            k++;
        }
        // 创建一个动态规划数组,保存到该点的最小路径和
        int[][] dp=new int[r][c];
        // 初始化动态规划数组
        dp[0][0]=triangle.get(0).get(0);
        for(int i=1;i<r;i++){
           // 因为是三角形,并且为了不影响其他元素的dp,所以j要<i
            for(int j=0;j<=i;j++){
                int a=Integer.MAX_VALUE;
                int b=Integer.MAX_VALUE;
                // 注意j-1这个条件十分重要,它可以避免dp的初始值0参与比较
                if(i-1>=0&&j>=0&&j<=i-1){
                   a=dp[i-1][j];
                }
                if(i-1>=0&&j-1>=0&&j-1<=i-1){
                   b=dp[i-1][j-1];     
                }
              dp[i][j]=Math.min(a,b)+tar[i][j];
            }
        }
        int min=Integer.MAX_VALUE;
        // 到达最后一行的最短路径为,tar最后一行的最小值
        for(int i=0;i<c;i++){
           if(dp[r-1][i]<min){
               min=dp[r-1][i];
           }
        }
        return min;
    }
}

例题17:给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。

你可以认为每种硬币的数量是无限的。(力扣:322)

在这里插入图片描述

很久就说过了动态规划:1.关键是要理解动态规划数组的含义 2.找出动态规划方程
首先动态规划数组:dp[i] 表示当前金额能兑换的最少硬币数

输入:coins = [1, 2, 5], amount = 11
输出:3 
解释:11 = 5 + 5 + 1

就拿上面的这个例子来说明一下:
首先:amont==0 dp[0]=0
amout=1 dp[i]=1
amout=2 dp[i]=1(直接环一枚2)或者dp[1]加上1(两枚) 取硬币数少的
amout=3 dp[3]=dp[1]+2或者dp[2]+1
换个写法可能更好理解dp[3]=1+dp[2]或者dp[3]=2+dp[1] 取硬币数较少的方法
最后看一看当硬币数dp[5]=1+dp[0] (直接换一个5元的) dp[5]=2+dp[3] dp[5]=1+dp[4]取硬币数较少的方法,者就是整个动态规范转移过程

代码如下:

//写着是广度搜索,实际是动态规划
//第一感觉也是动态动态规划就不追求其他的算法了
class Solution {
    public int coinChange(int[] coins, int amount) {
    //首先设置一个动态规划数组保存从0到amout所需的最小银币数
    int[] dp=new int[amount+1];
    //为每个dp赋初值,设为amout+1因为即使有1,amout+1也是最多的硬币数量了
    //这里涉及了一个新的api我之前都没用过,如果要为数组设置不为0的初值可以使用Arrays.fill(int[],int);
    Arrays.fill(dp,amount+1);
    //初始化动态规划数组
    dp[0]=0;
    //最外层循环是为了为每个amout都赋值
    for(int i=1;i<dp.length;i++){
        for(int j=0;j<coins.length;j++){
            if(i>=coins[j]){
                dp[i]=Math.min(dp[i],dp[i-coins[j]]+1);
            }
        }
    }
    return dp[amount]==amount+1?-1:dp[amount];
    }
}

例题18:给定一个非负整数 n,计算各位数字都不同的数字 x 的个数,其中 0 ≤ x < 10n 。(力扣:357)
这道题使用动态规划挺难的希望好好理解,要理解这道题的一个点它是从前到后添加数字的

/*
// 第一思路先写出一个算法判断该数字是否有重复的数字
// 可惜超出时间限制了最后两个案例过不了
class Solution {
    public int countNumbersWithUniqueDigits(int n) {
        int cout=0;
        int num=(int)Math.pow(10,n);
        for(int i=0;i<num;i++){
            if(!pd(i)){
                cout++;
            }
        }
        return cout;
    }

//  写一个函数判断里面是否含有重复的数字
    public boolean pd(int k){
    //   先将该数字转化为字符串
    String str=k+"";
    HashSet<Character> set=new HashSet<Character>();
    for(int i=0;i<str.length();i++){
        set.add(str.charAt(i));
    }
    if(set.size()<str.length()){
        return true;
    }
    return false;
    }
}
*/

class Solution {
    public int countNumbersWithUniqueDigits(int n) {
        //各位数字都不同。
        //来详解一下
        //dp[i]=dp[i-1]+(dp[i-1]-dp[i-2])*(10-(i-1));
        //加上dp[i-1]没什么可说的,加上之前的数字
        //dp[i-1]-dp[i-2]的意思是我们上一次较上上一次多出来的各位不重复的数字。以n=3为例,n=2已经计算了0-99之间不重复的数字了,我们需要判断的是100-999之间不重复的数字,那也就只能用10-99之间的不重复的数去组成三位数,而不能使用0-9之间的不重复的数,因为他们也组成不了3位数。而10-99之间不重复的数等于dp[2]-dp[1]。
        //当i=2时,说明之前选取的数字只有
        //1位,那么我们只要与这一位不重复即可,所以其实有9(10-1)种情况(比如1,后面可以跟0,2,3,4,5,6,7,8,9)。
        //当i=3时,说明之前选取的数字有2位,那么我们需要与2位不重复,所以剩余的
        //有8(10-2)种(比如12,后面可以跟0,3,4,5,6,7,8,9)
        if(n==0)
            return 1;
        int []dp=new int [n+1];
        dp[0]=1;
         dp[1]=10;
        for(int i=2;i<=n;i++)
        {
            dp[i]=dp[i-1]+(dp[i-1]-dp[i-2])*(10-(i-1));
        }
        return dp[n];
    }
}

例题19:在一个由 ‘0’ 和 ‘1’ 组成的二维矩阵内,找到只包含 ‘1’ 的最大正方形,并返回其面积。(力扣:221)
在这里插入图片描述
在这里插入图片描述

// 没得办法又是看题解的一天,其实就我个人感觉动态规划中最为重要的一步就是要理解
// 动态规划方程组所代表的含义:就这道题来说:我看了看题解理解的一种动态规划方程组的含义是
// 以该点作为左下角所能形成的最大的正方形的边长,其次我们就要理解动态规划转移方程,
// 首先最为简单的一种如果改点的值为0;那就想都不用想了,以他为左下角的最大的正方形的面积肯定为0
// 如果该点为1:那么该点所形成的正方形的最大值应该与他相邻的左边,上边,左上角的点的dp有关,
// 而且为他们dp的最小值,为什么是最小值,因为他要形成的是正方形,所以必须为方方正正的那没办法
// dp大的正方形只能迁就dp小的不然没法形成正方形,好了理解到这里应该能下手了
class Solution {
    int row;
    int col;
    // 为了避免完成dp后还要遍历一遍dp数组,直接创建一个变量来记录最大的正方形边长
    int maxside=0;
    public int maximalSquare(char[][] matrix) {
        this.row=matrix.length;
        this.col=matrix[0].length;
    //   首先创建一个动态规划数组来记录,以该点为左下角所能形成的正方形的最大的边长
    int[][] dp=new int[row][col];
    // 初始化动态规划数组,因为第一行和第一列是可以直接确定的所以可以直接初始化
    // 先初始化数组的第一行
    for(int i=0;i<col;i++){
        if(matrix[0][i]=='1'){
            dp[0][i]=1;
        }
        else{
            dp[0][i]=0;
        }
        if(dp[0][i]>maxside){
            maxside=dp[0][i];
        }
    }
    // 初始化动态规划数组的第一列
    for(int i=0;i<row;i++){
        if(matrix[i][0]=='1'){
            dp[i][0]=1;
        }
        else{
            dp[i][0]=0;
        }
        if(dp[i][0]>maxside){
            maxside=dp[i][0];
        }
    }
    // 以当前已经初始化的dp来为其他的dp赋值
    for(int i=1;i<row;i++){
        for(int j=1;j<col;j++){
        //    如果改点的值为0的话,直接改点的dp为0
        if(matrix[i][j]=='0'){
            dp[i][j]=0;
            continue;
        }
        else{
            int min1=Math.min(dp[i-1][j],dp[i][j-1]);
            dp[i][j]=Math.min(min1,dp[i-1][j-1])+1;
        }
        if(dp[i][j]>maxside){
            maxside=dp[i][j];
        }
        }
    }
    return maxside*maxside;
    }
}

例题20: 2100. 适合打劫银行的日子
在这里插入图片描述
在这里插入图片描述

// 我去我在这想了半天非递增的可能性
// 也就是是i这天要满足前time天递减,后time天递增
// 这题感觉就是动态规划,什么前缀和就是脱裤子放屁
class Solution {
    // 这题其实使用两个动态规划数组即可解决
    public List<Integer> goodDaysToRobBank(int[] security, int time) {
     int len=security.length;
    //  用来记录该天之前递减的天数
     int[] dpleft=new int[len];
    //  用来记录该天之后递增的天数
     int[] dpright=new int[len];

    //  使用循环分别求出dpleft,dpright
    // 第一个数,左边它的大肯定为0个
    dpleft[0]=0;
    for(int i=1;i<len;i++){
        dpleft[i]=security[i]<=security[i-1]?dpleft[i-1]+1:0;
    }
    // 同理
    dpleft[len-1]=0;
    for(int i=len-2;i>=0;i--){
        dpright[i]=security[i+1]>=security[i]?dpright[i+1]+1:0;
    }
    // 创建一个集合保存结果
    List<Integer> list=new ArrayList<>();
    // 再使用一个循环求解出满足条件的i,即dpleft[i]>=time&&dpright[i]>=time
    // 所以我们的循环范围为time,len-1-time
    // 因为有卡边界的情况,所以我直接从零开始
    for(int i=0;i<len;i++){
         if(dpleft[i]>=time&&dpright[i]>=time){
             list.add(i);
         }
    }
    return list;
    }
}

背包问题

例题:洛谷1910
在这里插入图片描述
在这里插入图片描述

package 每天打卡;

import java.util.Scanner;

//思路:使用动态规划:2维背包问题:对于不同资源的状态下,加入于不加入当前物品所能取得的最大值
public class P1910_2 {
  public static void main(String[] args) {
	  Scanner sc=new Scanner(System.in);
	  int N=sc.nextInt();
	  int M=sc.nextInt();
	  int X=sc.nextInt();
//	  创建三个数组保存每个间谍的信息
	  int[] A=new int[N];
	  int[] B=new int[N];
	  int[] C=new int[N];
//	  输入每个数组的值
	  for(int i=0;i<N;i++) {
		  A[i]=sc.nextInt();
		  B[i]=sc.nextInt();
		  C[i]=sc.nextInt();
	  }
//	  创建一个动态规划数组
	  int[][] dp=new int[M+1][X+1];
//	  首先列举没意见物品
	  for(int i=0;i<N;i++) {
//		  对不同状态下是否加入当前物品所能取得的最大大信息进行处理
		  for(int j=M;j>=B[i];j--) {
			  for(int k=X;k>=C[i];k--){
				  dp[j][k]=Math.max(dp[j][k], dp[j-B[i]][k-C[i]]+A[i]);
			  }
		  }
	  }
      System.out.print(dp[M][X]);
  }
}

LAST

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值