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

news2024/10/1 4:57:15

前言

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

动态规划的英语是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.coloradmin.cn/o/2181488.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java 死锁及避免讲解和案例示范

在大型分布式系统中&#xff0c;死锁是一种常见但难以排查的并发问题。特别是在 Java 领域&#xff0c;死锁问题可能导致系统崩溃或卡顿。本文将以电商交易系统为例&#xff0c;详细讲解如何识别和避免 Java 程序中的死锁问题&#xff0c;确保系统高效运行。 1. 什么是死锁&am…

如何初步部署自己的服务器,达到生信分析的及格线2(待更新)

参考我的上一篇博客https://blog.csdn.net/weixin_62528784/article/details/142621762?spm1001.2014.3001.5501&#xff0c; 现在我们已经有了一个能够跑一些基础任务的、基本没有配置的服务器了&#xff0c;接下来要做的任务就是&#xff1a; &#xff08;1&#xff09;进一…

centos一些常用命令

文章目录 查看磁盘信息使用 df 命令使用 du 命令 查看磁盘信息 使用 df 命令 df&#xff08;disk free&#xff09;命令用于显示文件系统的磁盘空间占用情况。 查看所有挂载点的磁盘使用情况&#xff1a; df -h选项说明&#xff1a; -h 参数表示以人类可读的格式&#xff0…

开发微信小程序 基础02

WX模板 1.对比 ①标签名称不同 ②属性节点不同 ③提供类似vue的模板语法 2.模板语法 2.1数据动态绑定 2.1.1在data种定义数据 在页面对应的.js文件中&#xff0c;把数据定义到data对象中即可 例---data &#xff1a; { info : init data , msList : [{msg : hello}, { ms…

开发微信小程序 基础03

WXSS(类似CSS) 定义&#xff1a; WXSS (WeiXin Style Sheets)是一套样式语言&#xff0c;用于描述 WXML的组件样式&#xff0c;类似于网页开发中的 CSS。 分类&#xff1a; 全局样式&#xff1a;定义在 app.wxss 中的样式为全局样式&#xff0c;作用于每一个页面 局部样式&…

解决 Android WebView 无法加载 H5 页面常见问题的实用指南

目录 1. WebView 简介 2. 常见问题 3. 网络权限设置 4. 启用 JavaScript 5. DOM Storage 的重要性 6. 处理 HTTPS 问题 7. 设置 WebViewClient 8. 调试工具 9. 其他调试技巧 10. 结论 相关推荐 1. WebView 简介 Android WebView 是一种视图组件&#xff0c;使得 And…

基于SSM+小程序的电影院订票选座管理系统(电影2)(源码+sql脚本+视频导入教程+文档)

&#x1f449;文末查看项目功能视频演示获取源码sql脚本视频导入教程视频 1、项目介绍 基于SSM的电影院订票选座小程序管理系统实现了管理员和用户二个角色。管理员实现了用户管理、影院信息管理、电影类型管理、电影信息管理、系统管理、订单管理等。用户实现了影院信息、电…

【论文笔记】Flamingo: a Visual Language Model for Few-Shot Learning

&#x1f34e;个人主页&#xff1a;小嗷犬的个人主页 &#x1f34a;个人网站&#xff1a;小嗷犬的技术小站 &#x1f96d;个人信条&#xff1a;为天地立心&#xff0c;为生民立命&#xff0c;为往圣继绝学&#xff0c;为万世开太平。 基本信息 标题: Flamingo: a Visual Langu…

16.安卓逆向-frida基础-HOOK类方法2

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 内容参考于&#xff1a;图灵Python学院 本人写的内容纯属胡编乱造&#xff0c;全都是合成造假&#xff0c;仅仅只是为了娱乐&#xff0c;请不要盲目相信。 工…

链表的基础知识

文章目录 概要整体架构流程 小结 概要 链表是一种常见的数据结构&#xff0c;它通过节点之间的连接关系实现数据的存储和访问。链表由一系列节点&#xff08;Node&#xff09;组成&#xff0c;每个节点包含数据和指向下一个节点的指针。链表的特点是物理存储单元上非连续、非顺…

《基于多视角深度学习技术的乳腺X线分类:图网络与Transformer架构的研究》|文献速递-基于多模态-半监督深度学习的病理学诊断与病灶分割

Title 题目 Mammography classification with multi-view deep learning techniques:Investigating graph and transformer-based architectures 《基于多视角深度学习技术的乳腺X线分类&#xff1a;图网络与Transformer架构的研究》 01 文献速递介绍 乳腺X线摄影是乳腺癌…

鸿蒙开发(NEXT/API 12)【请求用户授权】手机侧应用开发

为保护用户隐私&#xff0c;Wear Engine的API需要用户授权才可以正常访问。建议开发者在用户首次调用Wear Engine开放能力的时候执行本章节操作。 申请用户穿戴设备权限 应用拉起华为账号登录和授权界面&#xff0c;由用户授权相应的数据访问权限。用户可以自主选择授权的数据…

828华为云征文|华为云Flexus云服务器X实例——uniapp功能开发、搭建股票系统选择用什么服务器比较好?

在二次开发、安装搭建股票系统时&#xff0c;选择华为云Flexus X服务器是一个值得考虑的优质选项。以下是一些具体的建议&#xff1a; 测试环境&#xff1a;Linux系统CentOS7.6、宝塔、PHP7.3、MySQL5.7&#xff0c;根目录public&#xff0c;伪静态thinkphp&#xff0c;开启ssl…

1、深入理解Redis线程模型

文章目录 一、Redis是什么&#xff1f;有什么用&#xff1f;1、Redis是什么&#xff1f;2、2024年的Redis是什么样的&#xff1f; 二、Redis到底是单线程还是多线程&#xff1f;三、Redis如何保证指令原子性1、复合指令2、Redis事务3、Pipeline4、lua脚本5、Redis Function6、R…

CTFshow信息搜集web1~web20详解

目录 1、web1 源码泄露 2、web2 页面源代码泄露 3、 web3 响应头泄露 4、web4 robots协议 5、web5 phps源码泄露 6、web6 源码压缩包泄露 7、web7 GIT泄露 8、web8 SVN泄露 9、web9 vim缓存 10、web10 cookie 11、web11 域名解析 12、web12 网站公开信息 13、web13 技…

python4_画方格

python4_画方格 import turtledef cell():# 画第一个方格# 设置画笔宽度为1turtle.width(1)# 下笔,这样&#xff0c;路径就会画出来turtle.pendown()# 前进30px像素turtle.forward(30)# 设置为黑色turtle.color("black")# 方向转90度turtle.left(90)# 前进30px像素t…

HTML+CSS基础 第二季课堂笔记

一、列表 列表都不是单打独斗的&#xff0c;通常都是一组标签组成 1 无序列表 作用&#xff1a;定义一个没有顺序的列表结构 由两个标签组成&#xff0c;ul&#xff08;容器级标签&#xff09;&#xff0c;li&#xff08;容器级&#xff09; ul&#xff1a;英文ulordered …

828华为云征文 | 华为云Flexus云服务器X实例搭建企业内部VPN私有隧道,以实现安全远程办公

VPN虚拟专用网络适用于企业内部人员流动频繁和远程办公的情况&#xff0c;出差员工或在家办公的员工利用当地ISP就可以和企业的VPN网关建立私有的隧道连接。 通过拨入当地的ISP进入Internet再连接企业的VPN网关&#xff0c;在用户和VPN网关之间建立一个安全的“隧道”&#xff…

探索顶级低代码开发平台,实现创新

文章盘点ZohoCreator、OutSystems等10款顶尖低代码开发平台&#xff0c;各平台以快速开发、集成、数据安全等为主要特点&#xff0c;适用于不同企业需求&#xff0c;助力数字化转型。 一、Zoho Creator Zoho Creator 是一个低代码开发平台&#xff0c;它简化了应用开发中的复杂…

解决MySQL命令行中出现乱码问题

在MySQL命令行中遇到乱码问题通常是由于字符编码设置不正确导致的。以下是一些解决步骤&#xff1a; 1. **检查和设置字符集**&#xff1a; 首先&#xff0c;您需要确保MySQL服务器、客户端和数据库使用的是正确的字符集。您可以通过执行以下命令来查看当前的字符集设置&…