zoukankan      html  css  js  c++  java
  • 数据结构和算法-五大常用算法:动态规划算法

    参考:

    https://blog.csdn.net/weixin_38278878/article/details/80037455

    https://blog.csdn.net/zw6161080123/article/details/80639932

    https://blog.csdn.net/hollis_chuang/article/details/103045322

    https://blog.csdn.net/ailaojie/article/details/83014821

    https://blog.csdn.net/hollis_chuang/article/details/103045322

    https://blog.csdn.net/zw6161080123/article/details/80639932

    https://blog.csdn.net/memory_cood/article/details/88414601

    https://blog.csdn.net/sun_wangdong/article/details/82380806

    leetcode动态规划题目:https://leetcode-cn.com/problemset/leetcode-hot-100/?topicSlugs=dynamic-programming

    动态规划入门篇

    学习动态规划,愚认为,就是解决以下的三个问题:
    什么是动态规划?什么时候要用动态规划?怎么使用动态规划?

    让我们一个一个来解决!

    1、什么是动态规划?
    这里参考百度百科,动态规划是求解决策过程最优化的数学方法。把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解,创立了解决这类过程优化问题的新方法——动态规划。

    2、什么时候要用动态规划?
    如果要求一个问题的最优解(通常是最大值或者最小值),而且该问题能够分解成若干个子问题,并且小问题之间也存在重叠的子问题,则考虑采用动态规划。

    3、怎么使用动态规划?
    我把下面称为动态规划五部曲:
    1. 判题题意是否为找出一个问题的最优解
    2. 从上往下分析问题,大问题可以分解为子问题,子问题中还有更小的子问题
    3. 从下往上分析问题 ,找出这些问题之间的关联(状态转移方程)
    4. 讨论底层的边界问题
    5. 解决问题(通常使用数组进行迭代求出最优解)



    纸上得来终觉浅,绝知此事要躬行。举几个例子:
    例子1:
    剑指Offer(第二版)面试题14:剪绳子
    给你一根长度为n的绳子,请把绳子剪成m段 (m和n都是整数,n>1并且m>1)每段绳子的长度记为k[0],k[1],…,k[m].请问k[0]k[1]…*k[m]可能的最大乘积是多少?

    例如,当绳子的长度为8时,我们把它剪成长度分别为2,3,3的三段,此时得到的最大乘积是18.
    看完题目,我们按照上面提到的“动态规划五部”解决问题
    1、判题题意是否为找出一个问题的最优解
    看到字眼是“可能的最大乘积是多少”,判断是求最优解问题,可以用动态规划解决;

    2、从上往下分析问题,大问题可以分解为子问题,子问题中还有更小的子问题
    题目中举了个例子:当绳子的长度为8时,我们把它剪成长度分别为2,3,3的三段,此时得到的最大乘积是18;我们可以从这里开始突破,把长度为8绳子的最大乘积分解为数个子问题,长度为8我们可以把它看成长度为1和7的绳子的和,或者长度 为2和6的绳子的和,或者长度为3和5的绳子的和and so on!
    到这里,相信大家已经看到一丝真理了吧?

    3. 从下往上分析问题 ,找出这些问题之间的关联(状态转移方程)
    在第二点时,我们已经从上到下分析问题了,现在我们要从下往上分析问题了。分析可知,
    f(8) 的值就是f(1)*f(7),f(2)*f(6),f(3)*f(5),f(4)*f(4)它们之中的最小值,即f(8) = Max{f(1)*f(7),f(2)*f(6),f(3)*f(5),f(4)*f(4)}
    只要知道f(1)到f(7)的值就能求出f(8);对于f(7),只要知道f(1)到f(6)的值就能求出f(6);对于f(6),只要知道f(1)到f(5)的值就能求出f(6);以些类推,我们只要知道前几个边界的值,就能一步步迭代出后续的结果!
    状态转移方程: f(n)=Max{f(n-i)*f(i)} i={1,2,3,…,n/2}

    4. 讨论底层的边界问题
    底层的边界问题说的就是最小的前几个数值的f(n)的值,本题中就是f(0)、f(1)、f(2)、f(3)的值
    对于f(0),长度为0的绳子,没办法剪,没有意义
    对于f(1),长度为1的绳子,没办法剪,设为1
    对于f(2),长度为2的绳子,只有一种剪法,剪成两段长度为1的绳子,但剪后的乘积为1,比自身更小;如果不是求自身的值,要求乘积最大值的话就没必要剪。
    对于f(3),长度为3的绳子,只有一种剪法,剪成两段长度为1和2的绳子,但剪后的乘积为2,比自身更小;如果不是求自身的值,要求乘积最大值的话也没必要剪。

    5、解决问题
    这一部就是写代码了

    public static int cutting(int n) {
            //长度小于等等于1没办法剪
            if(n <= 1)
                return 0;
            //对于f(2),长度为2的绳子,只有一种剪法,剪成两段长度为1的绳子,剪后的乘积为1
            if(n == 2)
                return 1;
            //对于f(3),长度为3的绳子,只有一种剪法,剪成两段长度为1和2的绳子,但剪后的乘积为2
            if(n == 3)
                return 2;
            int max = 0;
            //数组用于存储绳子乘积最大值
            int value[] = new int[n + 1];
            value[0] = 0;
            value[1] = 1;
            //剪后的乘积为1,比自身更小;如果不是求自身的值,要求乘积最大值的话就没必要剪
            value[2] = 2;
            //剪后的乘积为2,比自身更小;如果不是求自身的值,要求乘积最大值的话也没必要剪
            value[3] = 3;
            //从f(4)开始迭代
            for(int i = 4;i <= n; i++) {
                max = 0;
                for(int j = 1;j <= i/2; j++) {
                    int val = value[j] * value[i - j];
                    max = val > max ? val : max;
                }
                value[i] = max;
            }
            max = value[n];
            return max;
    }

    对于刚学习动态规划的同学,看完上面那题会有点吃力吧?那我们再来一题简单的,给大家增加信心!

    例子2:
    跳台阶问题
    一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个n级台阶总共有多少种跳法。

    1、判题题意是否为找出一个问题的最优解
    这个我还真的看不出,直觉判断这题可以通过动态规划迭代出来….有经验的网友可以分享下看法,指导一下本小白白。

    2、从上往下分析问题,大问题可以分解为子问题,子问题中还有更小的子问题
    题目中没有给粟子,我们可以自己举点粟子。例如,跳上一个6级台阶台阶,有多少种跳法;由于青蛙一次可以跳两阶,也可以跳一阶,所以我们可以分成两个情况
    1、青蛙最后一次跳了两阶,问题变成了“跳上一个4级台阶台阶,有多少种跳法”
    2、青蛙最后一次跳了一阶,问题变成了“跳上一个5级台阶台阶,有多少种跳法”
    由上可得f(6) = f(5) + f(4);
    由此类推,f(4)=f(3) +f(2)

    3.、从下往上分析问题 ,找出这些问题之间的关联(状态转移方程)
    跟上面的例题一相同,可以由f(1)逐渐迭代上去
    由2可得,状态转移方程为:f(n)=f(n-1)+f(n-2)

    4、边界情况分析
    跳一阶时,只有一种跳法,所以f(1)=1
    跳两阶时,有两种跳法,直接跳2阶,两次每次跳1阶,所以f(2)=2
    跳两阶以上可以分解成上面的情况

    5、解决问题

    public static int jump(int n) {
            //无意义的情况
            if(n <= 0)
                return 0;
            if(n == 1)
                return 1;
            if(n == 2)
                return 2;
            //数组用于存储跳n阶的跳法数
            int[] value = new int[n + 1];
            value[0] = 0;
            value[1] = 1;
            value[2] = 2;
            for(int i = 3; i <= n; i++) {
                value[i] = value[i - 1] + value[i - 2];
            }
            return value[n];
    }

    参考资料

    百度百科——动态规划
    《面试–动态规划》 —五种经典的算法问题——https://blog.csdn.net/tongxinzhazha/article/details/77407648
    《剑指Offer(第二版)》(何海涛著)

    经典中的经典算法:动态规划(详细解释,从入门到实践,逐步讲解)

    动态规划的重要性就不多说,直接进入正题

    首先,我们看一下官方定义:
    定义:
    动态规划算法是通过拆分问题,定义问题状态和状态之间的关系,使得问题能够以递推(或者说分治)的方式去解决。
    动态规划算法的基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。
    基本思想与策略编辑:
    由于动态规划解决的问题多数有重叠子问题这个特点,为减少重复计算,对每一个子问题只解一次,将其不同阶段的不同状态保存在一个二维数组中。
    (来自百度百科)
    说实话,没有动态规划的基础很难看懂,但是也能从中看出一些信息,下面我翻译成人话:
    首先是拆分问题,我的理解就是根据问题的可能性把问题划分成一步一步这样就可以通过递推或者递归来实现.
    关键就是这个步骤,动态规划有一类问题就是从后往前推到,有时候我们很容易知道:如果只有一种情况时,最佳的选择应该怎么做.然后根据这个最佳选择往前一步推导,得到前一步的最佳选择
    然后就是定义问题状态和状态之间的关系,我的理解是前面拆分的步骤之间的关系,用一种量化的形式表现出来,类似于高中学的推导公式,因为这种式子很容易用程序写出来,也可以说对程序比较亲和(也就是最后所说的状态转移方程式)
    我们再来看定义的下面的两段,我的理解是比如我们找到最优解,我们应该讲最优解保存下来,为了往前推导时能够使用前一步的最优解,在这个过程中难免有一些相比于最优解差的解,此时我们应该放弃,只保存最优解,这样我们每一次都把最优解保存了下来,大大降低了时间复杂度

    总结一下动态规划的解题一般思路:
    首先递归应该是我们解决动态规划问题最常用的方法,帅,速度不算太慢
    那么递归到动规的一般转化方法为:
    如果该递归函数有n个参数,那么就定义一个n维数组,数组下标是递归函数参数的取值范围(也就是数组每一维的大小).数组元素的值就是递归函数的返回值(初始化为一个标志值,表明还未被填充),这样就可以从边界值开始逐步的填充数组,相当于计算递归函数的逆过程(这和前面所说的推导过程应该是相同的).

    动规解题的一般思路(标准官方,不过经过前边讲解应该就能理解了):

    1. 将原问题分解为子问题(开头已经介绍了怎么分解) (注意:1,子问题与原问题形式相同或类似,只是问题规模变小了,从而变简单了; 2,子问题一旦求出就要保存下来,保证每个子问题只求解一遍)
    2. 确定状态(状态:在动规解题中,我们将和子问题相关的各个变量的一组取值,称之为一个"状态",一个状态对应一个或多个子问题所谓的在某个状态的值,这个就是状态所对应的子问题的解,所有状态的集合称为"状态空间".我的理解就是状态就是某个问题某组变量,状态空间就是该问题的所有组变量) 另外:整个问题的时间复杂度就是状态数目乘以每个状态所需要的时间
    3. 确定一些初始状态(边界条件)的值 (这个视情况而定,千万别以为就是最简单的那个子问题解,上面只是例子,真正实践动规千变万化)
    4. 确定状态转移方程 (这一步和第三步是最关键的 记住"人人为我"递推,由已知推未知)

    适合使用动规求解的问题:

    1) 问题具有最优子结构性质。如果问题的最优解所包含的 子问题的解也是最优的,我们就称该问题具有最优子结 构性质。

     2) 无后效性。当前的若干个状态值一旦确定,则此后过程的演变就只和这若干个状态的值有关,和之前是采取哪种手段或经过哪条路径演变到当前的这若干个状态,没有关系;遇到求最优解问题一般适合使用动态规划

    告别动态规划,连刷40道动规算法题,我总结了动规的套路

  • 相关阅读:
    Springboot + Mybatis 多数据源配置
    构建微服务:Spring boot 入门篇
    IDEA SpringBoot代码修改热部署/加载
    vue-cli知识点
    vuex知识点
    正则基本知识
    多行SQL语句拼成一条数据
    软件的版本控制
    ASP.NET 表单验证实现浅析
    建造者模式
  • 原文地址:https://www.cnblogs.com/xuwc/p/13906386.html
Copyright © 2011-2022 走看看