认识动态规划算法和实践(java)

前言

动态规划算法里面最有意思的一个东西之一。动态规划初学肯定会有一定晦涩难懂。如果我们去网上搜索,动态规划的资料,它一开始都是将很多的理论,导致会认为很难,但是这个东西实际上是有套路的。

动态规划的英语是Dynamic Programming(规划),一般来说我们都简称为dp动态规划,简称DP,dp算法就是动态规划的意思

  • 求解最优化问题的一种常用策略(什么是最优化问题呢?比如我们要找零钱,硬币个数最少就是最优化,最大连续子序列号,也是最优化问题。在带最字的问题求解中,就有可能用到这个动态规划去解决)

  • 通常的使用套路
    1.暴力递归(自顶向下,出现了重叠子问题)
    2.记忆化搜索(自顶向下)
    3.递推(自底向上)

一步一步优化,也就是说我们如果经验不是很足,我们很有可能不会一上来就知道用动态规划的解法去解,很有可能是一步一步的去优化,如果是一步一步的去优化,最后是如何优化到动态规划的这个做法的呢?现先只是去了解,理解不清楚也没关系,后面会反复的讲解这个问题,我们要先大概有一个初步的印象。

一般来说,我们想使用动态规划来解决最优化问题,我们最有可能想到的是暴力递归,文章后面会深刻体会到这一点,如果暴力递归不行,有很多重复的计算,出现了许多的重叠的问题。这个时候,我们可以采取记忆化搜索,什么是记忆化搜索,下面会继续学到。

采取记忆化搜索发现还是不行,效率还是不行, 或者说效率虽然提高了,但是还是有一些不必要的地方存在。这个时候需要通过递推解决。其实我们的动态规划最终就是采取**递推**的方式去做的。
上面的这些东西对刚接触的人都很抽象,不要紧,下面就能够明白这三个概念是什么意思了。我们通过下面一道例题就明白了。


案例实践

找零钱:假设有25分、20分、5分、1分的硬币,现要找给客户41分的零钱,如何办到硬币个数最少?

  • 使用贪心策略得到的并非最优解(贪心得到的是5枚硬币:1个25分、3个5分、1个分)
  • 如果上面的问题使用的是动态规划,这个时候我们就可以解出来最优的值也就是:2个20分、1个1分。

动态规划最适合用来解决最优化的问题,我们应该如何使用它呢?动态规划使用的熟悉了,我们就会知道:

  • 首先要定义状态(状态是原问题,子问题的解),比如定义dp(i)的含义。

  • 其次设置初始状态(边界),比如设置dp(0)的值

  • 最后确定状态的转移方程,比如确定dp(i)和dp(i-1)的关系

我们最后发现动态规划算法无论咋办都是这个套路,上面的状态啥的,我们先不管,我们先解决一道题,然后再将啥是动态规划。


在使用动态规划前我们都会使用一个假设,我们先来假设一个dp(n)是凑到n分需要最少硬币的个数
dp(41)=凑到41分的最少硬币个数
dp(20)=凑到20分的最少硬币个数
dp(n)=凑到n分钱的最少硬币个数

我们现在要求dp(41),但是现在还求一个dp(n),那意思是不是我们现在还要求dp(1)、dp(2)、dp(3)、dp(4)这些吗?

答案是的,我们要求这些,那我们为什么要求这些呢?我们先来思考,我们假设现在要凑n分,n可能就是我们最终要的41,也有可能是100或者200都有可能。我们要凑齐n分都是从25分、20分、5分、1分的硬币里面去凑,而dp(n)是凑到最少的 硬币个数。也就是说这个已经是最优解了。我们现在要思考第一个问题,第一个硬币应该怎么去凑呢?我们的第一枚硬币应该有四种选择,假设我们的n是大于25的
假设我们的n是足够的,我们现在四枚硬币选啥都行。假设我第一枚,也就是第一次选择了25分的硬币。我们第一次选择了25分之后,现在还差多少分呢?现在还差n-25分 那现在我们考虑一个问题,d(n-25)是什么意思,它的意思是凑到n-25分之后,需要最少硬币个数.如果说我们第一次选择了25分的硬币,那么dp(n)=dp(n-25)+1,为什么是加1,因为我们选择了一枚硬币。剩下的n-25每要怎么去求,就由后面的dp(n-25)去求就行。

代码建议粘到IDE查看:

package com.lut.dynamic_programming;public class CoinChange {/*** 暴力递归,它是一个自顶向下的调用,什么叫做自定向下,可以理解为较大的数,不断地* 调用较小的数,较小的数又调用其它更小的数,这个就叫做一个自顶向下的一个调用过程* 暴力递归就是一个自顶向下的一个调用过程,它的问题是出现了重叠子问题* <p>* 什么叫做重叠子问题:问题的规模最开始为n,接下来调用coins(n-5),coins(n-1)...,它* 的规模就变小了,想当于大问题在调用它的子问题。我们下面的四个都算是n的子问题。* <p>* 而且这些子问题是有重复的,也就是很多的子问题进行了重复的计算。出现了重叠子问题的时候* 我们会想到一个解决的方法就算记忆化搜索,什么叫做记忆化搜索呢?看下面的另一个方法*/static int coins1(int n) {/**每一次选择硬币都有四种选择,我们要选择里面dp(n)最小的,然后dp(n)=dp(n-k)+1* 所以现在需要通过三个Math.min选择出,四个选择中选出次数最小的,每一次选择出最* 小后需要+1,也就是dp(n)=dp(n-k)+1。* 需要注意的是如果硬币等于 25分 or 20分 or 5分 or 1分的时候,直接return 1就行* 因为上面的这四种情况出现时,只需要一枚硬币,这个是递归的终止条件。* 需要注意的是,如果硬币是19等特殊数字的时候,n-20会产生一个负数。所以为了* 使得产生的负数直接取消比较资格,我们需要另一个边界条件。就是如果传入的n小于0* 直接return一个特别大的数就行,也就是直接return Integer.MAX_VALUE** 下面这个代码存在的问题,下面的这个代码其实是一个递归的写法,它的复杂度是2^n* 它的调用和斐波那契数列的调用方式很像* int fib(int n){*      if(n<=2) return 1;*      return fib(n-1)+f(n-2);* }* 它里面存在一些重复的计算的调用,斐波那契额数列也存在大量重复的计算* 比如我们要求5*     fib(5) = fib(4)+fb(3)=fib(3)+fib(2)+fib(2)+f(1)=*     f(2)+f(1)+f(2)+f(2)+f(1)=5**     f(4) = fib(3)+f(2)=fib(2)+fib(1)+fib(2)=3**     f(10) = f(9)+f(8)=f(8)+f(7)+f(7)+f(6)=f(7)+f(6)+f(6)+f(5)+f(6)+f(5)+f(5)+f(4)=*     f(6)+f(5)+f(5)+f(4)+f(5)+f(4)+f(5)+f(5)+f(4)+f(5)+f(5)+f(4)=*     f(5)+f(4)+f(5)+f(5)+f(4)+f(5)+f(4)+f(5)+f(5)+f(4)+f(5)+f(5)+f(4)=40+15=55*而我们的这个找硬币的方法同样也存在大量重复的计算*     假设我们现在传入的n是6,也就是coins(6)*     coins(6)->coins(5)+coins(1)->coins(0)+coins(4)+coins(1)->coins(1)->coins(0)*     如果n的传入的数字非常大,它存在很多重复的情况。** 因为暴力递归存在很多重复的情况,我们现在要考虑如何去优化它呢。* 我们刚一开始想使用动态规划,一上来就会想到暴力递归,下面的方法就算暴力递归*/if (n < 1) return Integer.MAX_VALUE;if (n == 25 || n == 20 || n == 5 || n == 1) return 1;int min1 = Math.min(coins1(n - 25), coins1(n - 20));int min2 = Math.min(coins1(n - 5), coins1(n - 1));return Math.min(min1, min2) + 1;}static int fib(int n) {if (n <= 2) return 1;return fib(n - 1) + fib(n - 2);}/*** 记忆化搜索(自顶向下的调用)* 记忆化搜索就是把曾经算过的那个值,曾经求解过的那个子问题的解给存起来,这个就叫做记忆化* 其实它就是缓存,我们可以考虑使用数组来解决这个事情。所谓记忆化就是把出现了的值放到内存* 里面去* <p>* 我们应该如何去实现呢?我们先要引入一个数组,因为是动态思想,这个数组,一般我们把它命名* dp,问题是这个数组是干嘛用的,这个数组的作用是:假如我们调用了coins(6),它算出来的值是凑* 齐6分至少要花费最少硬币的个数是多少。一旦我们算出来的值是4枚硬币,这个时候我们就会把* dp[16]赋值为4。所以dp数组的意思是,如果是dp[16]就代表要凑够16分的话,它所需的最下硬币* 个数我们把它存起来。就是这么一个道理。* dp[n]就是凑够n分需要的最小硬币个数。为了能够直接dp[n]所以我们在给这个数组申请内存的时候* 传入的长度是n+1,因为数组的索引是从0开始的。 长度只有是n+1,才能够直接放n* <p>* 这个方法还是存在递归调用,但是每一个dp[n]=只会进入一次递归,有了值就不会再次进行递归取值* 操作。* <p>* 记忆化搜索依然是自定向下调用,还是一个较大的值调用一个较小的值。但是性能相比暴力搜索有较大的* 提升。*/static int coins2(int n) {if (n < 1) return -1;/*** 因为传入n时如果n为19这样的特殊元素,可能会出现19-25=-6这样的数据,我* 们可以进一步对其进行做处理*/int[] dp = new int[n + 1];int[] faces = {1, 5, 20, 25};for (int face : faces) {if (n < face) break;dp[face] = 1;}return coins2(n, dp);}static int coins2(int n, int[] dp) {if (n < 1) return Integer.MAX_VALUE;//        if (n == 1 || n == 5 || n == 20 || n == 25) {//初始化操作
//            dp[n] = 1;
//        }if (dp[n] == 0) {int min1 = Math.min(coins2(n - 25, dp), coins2(n - 20, dp));int min2 = Math.min(coins2(n - 5, dp), coins2(n - 1, dp));dp[n] = Math.min(min1, min2) + 1;} else {return dp[n];}return dp[n];}/*** 上面了做了记忆化搜索,动态规划的最后一步叫做递推。我们只有写到递推这一步才能叫做* 真正的动态规划,递推和前面的暴力递归和记忆化搜索都是不一样的,它是自底向上的。前* 面的两步仅仅是前奏,我们刚开始发现能够暴力递归,然后我们发现能够使用记忆化搜索来* 优化,但是使用了记忆化搜索优化之后,发现还可以优化,那就算什么优化呢?就是递推。* 通过递推的形式,递推说的广泛一点,就是大家认为的非递归,也就是递推,当然有人也把* 叫做迭代,但是这里叫做递推比较好。就是一步一步的往前面推,这个叫做递推。*//*** 递推(自底向上)* 我们发现前面记忆化搜索的时候,定义了一个dp数组,用于记录搜索过的值。* 比如我们计算完了凑够n分最少要凑够多少枚硬币计算出来了,这个时候我们* 就将这个数放在这个数组里面去,dp(n)=凑足n分需要的最少的硬币数目。* 我们前面是由大到小的调用,从41到往下掉 16 21 26 40 然后这四个* 数字右会分成4个叉往下掉,一直往下,得到的dp[n]的值是先把d(n-25)的值* dp(n-20),dp(n-5),dp(n-1)的值算出来。只有将这四个值都算出来了,最终* 才能知道这个n的值是多少。* 说白了,现在我们要求凑够41分的硬币个数,要先求出凑够16的硬币最小个数* 凑够21分的硬币最小个数,凑够36分和凑够40分的最小硬币个数。也就是我们要凑够比较* 大的硬币的分数,必然要先求出凑够哪些比较小的分需要的硬币个数。小的算出来了,才有办法* 求出比较大的,所以我们完全有理由相信我们可以由小算到大,因为本来这个式子就可以看得出来* 肯定要先算出小的。才能算出这个n比较大的* 所以我们这个dp[]这个数组完全可以由小往大的方向去算。我们通过下面的for循环来测试看* 这个结果能不能行,for(int i = 1; i <= n; i++){ dp[i] = ;}我们可以先求dp[1]一直到* dp[n]就可以了。这样就是由小到大的去算。那么我们的方法的返回值应该写啥呢?这个方法我们直接* 返回一个dp[n]就行了。问题是dp[i]我们要咋求呢?* 我们知道求dp[i]我们肯定是在dp[i-25],dp[i-20],dp[i-5],dp[i-1]中最小的值拿出来再* 加1,也就是反着通过不断地i+1、i+5,i+20,i+25来达到i+k=n 问题是我们的i是从1开始的,1-25,* 1-20...这些数据肯定是不合理的,i等于1的时候1-1好像* 也没有什么意义。这个时候我们就得去判断,首先来一个int min = Integer.Math_VALUE;然后去判断* 哪一个合适也就是。现在我们就直接通过比较直接获取最大得值,因为19-25小于0会导致索引越界。所以* 我们需要在前面加上一个越界判断条件。*/static int coins3(int n) {if (n < 1) return -1;int[] dp = new int[n + 1];for (int i = 1; i <= n; i++) {
//            int min = Integer.MAX_VALUE;
//            if (i >= 1) min = Math.min(dp[i - 1], min);int min = dp[i - 1];if (i >= 5) min = Math.min(dp[i - 5], min);if (i >= 20) min = Math.min(dp[i - 20], min);if (i >= 25) min = Math.min(dp[i - 25], min);dp[i] = min + 1;}return dp[n];}/*** 为啥这个叫做动态规划?未啥叫做动态规划,它的具体含义是啥,我们先不去了解* 先通过做题去领悟。** 我们需要知道的是,我们现在不断地给代码做优化,现在上面代码的时间复杂度是o(n)*//*** 思考题:请输出找零钱的具体方案(最少硬币,具体是使用了哪些面值的硬币)* 我们应该怎么做呢?我们现在就在递推代码来进行实现。* 我们现在要凑够n分,它具体使用哪些面值。* <p>* 为了充分理解,我们还是先写成前面的代码,* if (n < 1) return -1;* int[] dp = new int[n + 1];* for (int i = 1; i <= n; i++) {* int min = Integer.MAX_VALUE;* if (i >= 1) min = Math.min(dp[i - 1], min);* if (i >= 5) min = Math.min(dp[i - 5], min);* if (i >= 20) min = Math.min(dp[i - 20], min);* if (i >= 25) min = Math.min(dp[i - 25], min);* dp[i] = min + 1;* }* return dp[n];* 首先我们dp[i]是指的凑够i分钱,最少需要多少枚硬币。所以我们就先考虑* 凑够i-1,i-5,i-20,i-25看一下这些凑够这些分数,哪一个硬币数是最小的* 把它们最小的值挑出来然后+1,这个1就是指的是最后一次选择了一枚硬币,它* 可能是1分、5分、20分、25分。* 我们的关键点就是在进入了那个if判断就可以知道它最后一次选择了哪一枚,* 举个例子,我们首先得搞清楚,min这个东西肯定是这四个当中的最小值。如果* 我们这一轮比较中发现dp[i-1],dp[i-5],dp[i-20],dp[i-25]中dp[i-20]是* 最小的,它是这四个当中最小的,那毫无疑问这个min就算dp[i-20],这就意味着* 你这一次选择了一枚20分的,也就是最后一次选择了一枚20分。意味着这一次min+1* 这个1代表,选择了一枚20分的硬币。* 只有我们这次选的是20,才会去使用dp[i-20]这个值,所以答案就出来了,我们要* 想知道这一次选择了那一枚,我们只需要看一下这个min它来自于那个if判断,如果我们* 这个min来自于if (i >= 20) min = Math.min(dp[i - 20], min),也就是一枚20* 分的,如果min来自于if (i >= 25) min = Math.min(dp[i - 25], min)说明选择* 的是一枚25分的,我们可以去记录一下。* 应该如何去记录呢?因为我们这里是一个for循环,肯定是选择了很多次,所以我们可* 以考虑这么做,int[] faces = new int[dp.length];* 这个faces[i]是什么意思呢?它的意思代表+1选择了哪一枚,比如假如我们的min是选择* 的dp[i-20],说白了这一次的加1,代表选择了20分的,才会去考虑i-20凑够i-20需要多少硬币* 所以这一次选20的话,这个faces[i]=20,它存下来有啥作用呢?我们可以充分利用这个东西了,* 我们现在改写一下原来的代码* for(int i=1; i <= n;i++){* if(n<1) return -1;* int[] dp = new int[n+1];* int[] faces = new int[dp.length];* for(int i=1;i <= n;i++){* int min = Integer.MAX_VALUE;* if(i >= 1 && dp[i-1] < min){* min = dp[i-1];* faces[i] = 1;* }* if(i >= 5 && dp[i-5] < min){* min = dp[i-5];* faces[i] = 5;* }* if(i >= 20 && dp[i-20] <min){* min = dp[i-20];* faces[i] = 20;* }* if(i >= 25 && dp[i-25] <min){* min = dp[i-25];* faces[i] = 25;* }* }* }* 改写后的代码,最开始min=Integer.MAX_VALUE。当我们如果发现这个时候i>=1的* 并且dp[i-1]又是比这个min小,说白了我们发现一个更小的值。所以我们应该把这个值给存起来* 一旦最小值使用了这个方案,也就是说,这一次,我们是选择了一枚一分的,如果i>=5而且比* dp[i-5]<min,也就是min小于刚刚算的那个最小值还要小,那说明现在发现的最小值min=dp[i-5],* 如果我们这一次使用的最小值使用的是dp[i-5]说明这一次要选择必然是5分的。我们现在已经求完* 前面的,现在我们又发现i>=20,并且dp[i-20]<min,那么我们就让最小值是min=dp[i-20],一旦* 我们采取的是让min作为我们的最小值,意味着我们最后一次一定是选择的一枚20分的硬币。依次类推* 如果我们发现dp[i-25]还要更小,我们最小值就采取它,说白了这一次选择的硬币必然是25分的。我们* 才会考虑i-25分的最小值是多少。* 理解这个问题的关键就在faces[i]的存储,这个答案就求存来了,这样我们就能够知道每一次* 拿到的硬币数是谁。求出这个东西之后有啥用,肯定是利用这个faces[]数组去搞事情*/static int coins4(int n) {if (n < 1) return -1;int[] dp = new int[n + 1];int[] faces = new int[dp.length];for (int i = 1; i <= n; i++) {int min = Integer.MAX_VALUE;if (i >= 1 && dp[i - 1] < min) {min = dp[i - 1];faces[i] = 1;}if (i >= 5 && dp[i - 5] < min) {min = dp[i - 5];faces[i] = 5;}if (i >= 20 && dp[i - 20] < min) {min = dp[i - 20];faces[i] = 20;}if (i >= 25 && dp[i - 25] < min) {min = dp[i - 25];faces[i] = 25;}dp[i] = min + 1;print(faces, i, dp);}
//        print(faces,n);return dp[n];}private static void print(int[] faces, int n, int[] dp) {System.out.println("凑齐" + n + "分至少需要" + dp[n] + "硬币");while (n > 0) {System.out.println("需要" + faces[n] + "分1枚");n -= faces[n];}}/*** 这里我们没有具体了解啥是动态规划,只是先理解啥是暴力递归,记忆化搜索,递推* 最后我们打印了一下这个具体的面值组成。* <p>* 下面我们需要给这个找零钱写一个通用的实现,就是可以由别人传面值进来,我们之前* 的面值都是固定死了的,我们可以搞一个由别人传面值进来,所以这里可以咋做呢?*/static int coins5(int n,int[] faces) {if(n < 1 || faces == null || faces.length == 0) return -1;int[] dp = new int[n+1];for (int i = 1; i <= n; i++){int min = Integer.MAX_VALUE;for (int face : faces) {if(i < face || dp[i-face]<0) continue;min = Math.min(dp[i-face],min);}if(min == Integer.MAX_VALUE){dp[i] = -1;}else{dp[i] = min + 1;}}return dp[n];}public static void main(String[] args) {System.out.println(coins5(41,new int[]{5,20,25}));}
}

难点:

我们现在要思考的是,我们的第一次可能不是选25分,也有可能选择的是20分,如果我们选择了20分的硬币的话
那么dp(n) = dp(n-20)+1。如果第一次选择了5分的硬币,那么dp(n)=dp(n-5)+1。如果第一次选择了1分的
硬币,那么dp(n)=dp(n-1)+1。但是这四种情况都是有可能发生的,因为我第一次选哪个硬币是不确定的。

我们现在要干嘛呢?要求的是dp(n)的最小硬币数。
其实就是这四个值当中的最小值。也就是dp(n) = min{dp(n-25),dp(n-20),dp(n-5),dp(n-1)}+1
所以我们就是从四个中挑出最小的那个。然后+1

我们在四个情况中取出最小的,就算所有中最小的,所以动态规划它能求出最优解,为啥它能求出最优解,是因为
它把所有的情况都考虑到了,为啥贪心不能呢?因为贪心它只看眼前的局部利益,这个就贪心算法的一个问题所在

⚪假设dp(n)是凑到n分需要的最少的硬币个数
⚪如果第一次选择了25分的硬币,那么dp(n)=dp(n-25)+1
⚪如果第一次选择了20分的硬币,那么dp(n)=dp(n-20)+1
⚪如果第一次选择了5分的硬币,那么dp(n)=dp(n-5)+1
⚪如果第一次选择了1分的硬币,那么dp(n)=dp(n-1)+1
⚪所以dp(n)=min{dp(n-25),dp(n-20),dp(n-5),dp(n-1)}+1


分析找零钱案例:

学习完了找零钱,它的解法就是动态规划,现在我们就来学习到底啥是动态规划,我们要学习动态规划,有一个前提我们要知道找零钱最开始是咋做的,我们一开始是进行暴力递归。我们要凑够n分,就看n-1,n-5,n-20,n-25从它们
四个中挑一个最小的,然后再来一个加1。后来我们发现递归的很多子问题中,有很多重复,我们就使用了一个记忆化搜索,但是这个记忆化搜索,仅仅是搞
了一个数组,里面记录着凑够某个面值有的最小硬币个数,就是使用这个dp这个数组,去记录。但是这个记忆化搜索依旧是从上往下的调用的,依然还存在着递归调用。后面我们发现,完全可以通过一个递推的形式,就是自底向上,先求出较小的值,进而再往上推,推出值比较大的,也就是我们发现我们的这个dp数组完全可以从dp[1]开始,到dp[2],dp[3],dp[4],d[5]一直往上面求,所以我们dp[i]的i一直在增
长,所以的话是由小推到大,所以这样的话,我们直接把递归调用给省略掉了。这个过程就可以理解为在执行动态规划的一个过程。

动态规划它究竟是个什么呢?我们来回顾一下,它有一些抽象,但是不难理解。动态规划是求解最优化问题的一种常用的策略,比如我们讲到找零钱兑换,它就是看最少硬币个数多少,最少这个就是一个最优化问题,它通常的套路是啥呢?

我们先来一个暴力递归,然后接下来发现有重复的,这个时候我们来一个记忆化搜索,前面两种都是递归,都是自顶向下的我们后来发现可以自底向上,也就是从小到大使用记忆化搜索,可以去推出这个值。

为啥这个就叫做动态规划呢?这个时候我们就可以利用比较专业的术语来说这一个问题。动态规划中的"动态"可以理解为是"可以变化的状态"

  1. 定义状态(状态是原问题、子问题的解)
    比如dp[i]的定义

  2. 设置初始状态(边界)

    比如设置dp[0]的值

  3. 确定状态转移方程

    比如确定dp(i)和dp(i-1的关系)

动态规划的常规步骤,第一个步骤定义状态,什么是状态,状态就是原问题,子问题的解,比如定义一下dp(i)的含义,这个就叫做定义状态,举个例子,我们之前在讲零钱兑换的时候,我们定义了一个东西——dp(n),dp(n)就是凑到n分最少的
硬币个数,如果我们这里写一个dp(41)就是凑够41需要的最少硬币个数。如果是dp(4)那就是凑够4分所需的最少硬币个数所以我们这里的dp(n)就代表定义状态,这个定义状态的意思就算要得到这个解,因为这个dp(n)就是我们最终想要得到的
一个解,到时候n是41,dp(41)就是我们最终要的这个解。 所以动态规划里面定义状态,这个状态一般来说就是原问题的解,也是子问题的解,这句话啥意思呢?我们现在虽然定义了个dp(n),确实是我们最重要的一个解,当我们的这个n=41的时候,它就是我们想要的解。当规模更小了就变成子问题了,这个时候dp(n-25)就是一个子问题的解,就算凑够n-25分需要的最少硬币数,它就算子问题的解。动态规划一上来就是定义规划,我们定义的这个状态,一般就是这个问题最终的解,也是子问题要的一个解。

定义完这个状态的时候我们接下来要设置一些边界条件,也就是初始状态,比如设置一下dp(0)的值,当然不一定是dp[0],它有可能是dp(1),dp(2),dp(3)这些都是初始值,这些就是设置一些初始状态,那些值是可以直接确定的,哪些子问题的解我们是可以直接马上确定的,我们就给它马上设置一个初始值。

比如以找零钱的代码为例子,我们先搞了一个dp[]数组,也就是状态数组,我们做了一个啥初始化,dp[face]=1,说白了凑够1肯定1枚,凑够5、20、25的话,肯定也是一枚,凑够5和凑够25的话,最少硬币就是一枚。像这种就是初始值,所以初始值就是这个。但是有很多问题里面,就是直接初始化dp[0],这个也叫做边界条件。这个根据条件而定,不同题目的边界条件是不一样的,这个dp[i]的状态定义出来了,我们把这个解是怎样的一个形式定义出来了,初始值也有了,也就是边界条件也有了,接下来就要开始递推了。

开始递推,也就是从dp(0)一直推到dp(i),一直往后推,这个推的过程,专业来说,叫做确定状态转移方程,为啥叫做状态转移方程,前面我们清楚的知道了,dp[i]代表状态,我们会将dp(i)从小推到大,说白了先求出dp(1)的值,然后推出dp(2)的值是什么就相当于从1这个状态转移到2这个状态,我们现在从小求到大,就相当于由以前的状态要推出一个新的状态,由旧的状态,要推导出一个新的状态,所以这个专业来讲,叫做状态转移方程。

简单来说,状态转移方程,就是确定我们dp(i)和dp(i-1)的关系,就是我们知道dp(i-1)怎么知道dp(i),一旦知道了这dp(i)怎么推出dp(i+1),这个叫做状态转移方程,也就是确定哪个递推式,确定那个递推关系。这个就是动态规划的三大步骤。

我们会影响深刻的,定义完状态,有一些初始状态我们要去设置一下,也就是边界条件,这个时候我们就需要一个状态转移方程,看一下然后知道这个dp(i-1)怎么推导出dp(i)呢?怎么由dp(i)推导出dp(i+1),这个就叫做状态转移方程。

动态规划的一个解释:

Dynamic Programming is a method for solving a complex problem by breaking it down into a collection of simpler subProblem,solving each of those subProblems just once,and storing theirsolutions

也就是说动态规划,就是把原问题分解成一系列的更简单的子问题,并且把每一个子问题只解决一次,并且把它的值给存储。
⚪将复杂的问题拆解成若干个简单的子问题
⚪每个子问题仅仅解决一次,并保存它们的解
⚪最后再推导出原问题的解

⚪可以用动态规划来解决的问题,通常具备2个特点
⚪最优子结构(最优化原理):通过求解子问题的最优解,可以获得原问题的最优解(如果符合这个特点,这就叫做最优子结构)
⚪无后效性(动态规划不仅仅要有最优化子结构,还得满足无后效性,必须满足两个条件才能通过动态规划来解决)

  • 某阶段的状态一旦确定,则此 后过程的演变不再受此前各状态的决策影响(未来与过去无关)
  • 在推导后面阶段的状态时;只关系前面阶段的具体状态值,不关心这个状态是怎么一步一步推导出来的

无后效性(举例)

现在有一个5*5的一个格子,从起点(0,0)走到终点(4,4)一共有多少种走法?只能向右、向下走。
在这里插入图片描述

如果我们要使用动态规划解决这个问题,我们首先第一件事就是去定义状态,这个时候我们发现我们现在要定义的状态是2维的了,不再是1维的了,之前只有dp(i),现在我们定义了一个dp(i,j),dp(i,j)的意思是从(0,0)走到(i,j)的走法,也就是dp(i,j),也就是dp(4,4),这个问题既是子问题的解,又是原问题的解。那么初始值是什么,我们定义完状态肯定有一个初始状态的值,我们现在要看一下那一些是初始值,dp(0,0)是1还是0,因为从自己走到自己是1种走法,但是我们关注的重点不是这个,除了dp(0,0),还有哪些值是可以确定的,其实还有dp(i,0)和dp(0,j) 这几种状态都是只有一种走法,所以dp(i,0)=dp(0,j)=1我们现在把水平方向用i表示,我们把竖直方向定义为j。现在问题来了,状态转移方程应该怎么写。

  • 假设dp(i,j)是从(0,0)走到(i,j)的走法
  • dp(i,0)=dp(0,j)=1
  • dp(i,j)=dp(i,j-1)+dp(i-1,j)

(状态转移方程怎么写,也就是我们现在要求(i,j)要从哪个点推导过来,也就是可以从哪些地方走过来,只能向下向右走。它只能从(i-1,j)往右走,或者从(j,j-1)往下走。所以dp(i,j)=dp(i,j-1)+dp(i-1,j),前面的公式代表着,从(0,0)到(i,j)的走法,
说白了就是dp(i,j-1)和dp(i-1,j)的走法之和。无后效性)

无后效性,推导dp(i,j)时只需要用到dp(i,j-1)、dp(i-1,j)的值,不需要关心dp(i,j-1)、dp(i-1,j)的值是怎么求出来的,我们只需要有多少种走法,并不关心如何走的。而且也不影响使用这个值去推导别的值

上面只是解释了什么是无后效性,现在我们要去看一个相反的情况,就是什么是有后效性,只有理解了什么是有后效性,才能去理解什么是**无后效性**,这个问题比较抽象,我们先来看一个有后效性的例子。还是前面的5*5的一个格子,不过现在可以向左、向右、向上、向下走,并且同一个格子不能走两次,这个问题就有后效性了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/440174.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MySQL之分库分表后带来的“副作用”你是怎么解决的?

目录标题 一、垂直分表后带来的隐患二、水平分表后带来的问题1.多表联查问题2.增删改数据问题3.聚合操作问题 三、垂直分库后产生的问题1.跨库join问题2.分布式事务问题3.部分业务库依然存在的性能问题 四、水平分库后需要解决的问题1.聚合操作和连表问题2.数据分页问题3.ID主键…

10款好用的开源 HarmonyOS 工具库

大家好&#xff0c;我是 V 哥&#xff0c;今天给大家分享10款好用的 HarmonyOS的工具库&#xff0c;在开发鸿蒙应用时可以用下&#xff0c;好用的工具可以简化代码&#xff0c;让你写出优雅的应用来。废话不多说&#xff0c;马上开整。 1. efTool efTool是一个功能丰富且易用…

全球IP归属地查询-IP地址查询-IP城市查询-IP地址归属地-IP地址解析-IP位置查询-IP地址查询API接口

IP地址城市版查询接口 API是指能够根据IP地址查询其所在城市等地理位置信息的API接口。这类接口在网络安全、数据分析、广告投放等多个领域有广泛应用。以下是一些可用的IP地址城市版查询接口API及其简要介绍 1. 快证 IP归属地查询API 特点&#xff1a;支持IPv4 提供高精版、…

k8s 中微服务之 MetailLB 搭配 ingress-nginx 实现七层负载

目录 1 MetailLB 搭建 1.1 MetalLB 的作用和原理 1.2 MetalLB功能 1.3 部署 MetalLB 1.3.1 创建deployment控制器和创建一个服务 1.3.2 下载MealLB清单文件 1.3.3 使用 docker 对镜像进行拉取 1.3.4 将镜像上传至私人仓库 1.3.5 将官方仓库地址修改为本地私人地址 1.3.6 运行清…

【前端】-音乐播放器(源代码和结构讲解,大家可以将自己喜欢的歌曲添加到数据当中,js实现页面动态显示音乐)

前言&#xff1a;音乐播放器是前端开发中的一个经典项目&#xff0c;通过它可以掌握很多核心技术&#xff0c;如音频处理、DOM操作、事件监听、动画效果等。这个项目不仅能提升前端开发的技能&#xff0c;还能让开发者深入理解JavaScript与HTML的协同作用。 页面展示&#xff1…

Web安全 - 文件上传漏洞(File Upload Vulnerability)

文章目录 OWASP 2023 TOP 10导图定义攻击场景1. 上传恶意脚本2. 目录遍历3. 覆盖现有文件4. 文件上传结合社会工程攻击 防御措施1. 文件类型验证2. 文件名限制3. 文件存储位置4. 文件权限设置5. 文件内容检测6. 访问控制7. 服务器配置 文件类型验证实现Hutool的FileTypeUtil使用…

Python中的机器学习:从入门到实战

机器学习是人工智能领域的一个重要分支&#xff0c;它通过构建模型来使计算机从数据中学习并做出预测或决策。Python凭借其丰富的库和强大的生态系统&#xff0c;成为了机器学习的首选语言。本文将从基础到实战&#xff0c;详细介绍如何使用Python进行机器学习&#xff0c;涵盖…

【汇编语言】寄存器(CPU工作原理)(二)—— 汇编指令的基础操作

文章目录 前言正文——&#xff08;一气呵成解决本文内容&#xff09;结语 前言 &#x1f4cc; 汇编语言是很多相关课程&#xff08;如数据结构、操作系统、微机原理&#xff09;的重要基础。但仅仅从课程的角度出发就太片面了&#xff0c;其实学习汇编语言可以深入理解计算机底…

Android Framework AMS(02)AMS启动及相关初始化5-8

该系列文章总纲链接&#xff1a;专题总纲目录 Android Framework 总纲 本章关键点总结 & 说明&#xff1a; 说明&#xff1a;本章节主要涉及systemserver启动AMS及初始化AMS相关操作。同时由于该部分内容过多&#xff0c;因此拆成2个章节&#xff0c;本章节是第二章节&…

LabVIEW提高开发效率技巧----使用动态事件

在LabVIEW开发过程中&#xff0c;用户交互行为可能是多样且不可预知的。为应对这些变化&#xff0c;使用动态事件是一种有效的策略。本文将从多个角度详细介绍动态事件的概念及其在LabVIEW开发中的应用技巧&#xff0c;并结合实际案例&#xff0c;说明如何通过动态事件提高程序…

Vector不清晰点学习易错点

什么是迭代器 是一个广义指针它可以是指针&#xff0c;也可以是一个可对其执行类似指针得操作-如解除引用&#xff08;如operator*()&#xff09;和递增&#xff08;operator()&#xff09;STL中每个容器类都定义了一个合适的迭代器&#xff0c;该迭代器的类型是一个名为itera…

【Python游戏开发】贪吃蛇游戏demo拓展

拓展上一项目【Python游戏开发】贪吃蛇 实现穿墙效果 # 检测游戏是否结束 def check_gameover():global finished# 移除蛇头位置超过窗口判断for n in range(len(body) - 1):if(body[n].x snake_head.x and body[n].y snake_head.y):finished True # 状态检测 def ch…

html5 + css3(下)

目录 CSS基础基础认识体验cssCSS引入方式 基础选择器选择器-标签选择器-类选择器-id选择器-通配符 字体和文本样式1.1 字体大小1.2 字体粗细1.3 字体样式&#xff08;是否倾斜&#xff09;1.4 常见字体系列&#xff08;了解&#xff09;1.5 字体系列拓展-层叠性font复合属性文本…

oh-crop: OpenHarmony/HarmonyOS上的简单的图片剪裁库,可用于头像剪裁等常见场景。

&#x1f4da; 简介 oh-crop: OpenHarmony/HarmonyOS上的简单的图片剪裁库&#xff0c;可用于头像剪裁等常见场景。 代码仓库&#xff1a;oh-crop &#x1f4da; 下载安装 ohpm i xinyansoft/oh-cropOpenHarmony ohpm 环境配置等更多内容&#xff0c;请参考: 下载安装三方库…

一个值得关注的3D生成新算法:速度和图像生成平齐,能生成合理的展开贴图和高质量mesh

今天跟大家介绍的GIMDiffusion是一种新的Text-to-3D模型&#xff0c;利用几何图像&#xff08;Geometry Images&#xff09;来高效地表示3D形状&#xff0c;避免了复杂的3D架构。通过结合现有的Text-to-Image模型如Stable Diffusion的2D先验知识&#xff0c;GIMDiffusion能够在…

【数据结构】【链表代码】相交链表

/*** Definition for singly-linked list.* struct ListNode {* int val;* struct ListNode *next;* };*/typedef struct ListNode ListNode; struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {//先求出两个链表的长度ListNode…

Unity 编辑器多开

开发多人联机的功能时大多数会遇到测试机不方便的问题。想多开同一个项目Uinty又禁止。。。因为在使用Unity Editor打开一个项目时&#xff0c;Unity Editor会在项目目录建立一个Temp目录&#xff0c;同时对里面的一个UnityLockfile文件进行加锁。SO...可以使用以下方法进行多开…

【easypoi 一对多导入解决方案】

easypoi 一对多导入解决方案 1.需求2.复现问题2.1校验时获取不到一对多中多的完整数据2.2控制台报错 Cannot add merged region B5:B7 to sheet because it overlaps with an existing merged region (B3:B5). 3.如何解决第二个问题处理&#xff1a; Cannot add merged region …

ISO IEC 18004 2015 PDF 文字版下载

ISO_IEC_18004_2015_en-US - 道客巴巴 (doc88.com)https://www.doc88.com/p-67816330893254.html

Kafka和RabbitMQ区别

RabbitMQ的消息延迟是微秒级&#xff0c;Kafka是毫秒级&#xff08;1毫秒1000微秒&#xff09; 延迟消息是指生产者发送消息发送消息后&#xff0c;不能立刻被消费者消费&#xff0c;需要等待指定的时间后才可以被消费。 Kafka的单机呑吐量是十万级&#xff0c;RabbitMQ是万级…