动态规划上

动态规划

首先,动态规划问题一般形式就是求最值。比如说求最大递增子序列,最小编辑距离
核心问题是穷举。因为要求最值,肯定要把所有可行的答案穷举出来,然后在其中找最值。
但是,动态规划的穷举问题有点特别,这类问题存在重叠子问题,如果暴力穷举的话效率非常低,需要备忘录来优化穷举过程,避免不必要的重复。
穷举所有的可行解需要列出正确的状态转移方程,才能正确的穷举,这个是最困难的一步。
思维框架
明确 基本情况 -> 明确状态 -> 明确选择 -> 定义dp数组/函数的含义
在这里插入图片描述

注意:用递归的时候最好画出递归树,便于分析算法的复杂度。
这个算法的时间复杂度为2的n次方,时间复杂度爆炸。

观察递归树,发现算法低效的原因,存在大量的重复计算。比如说f(18)就是被计算了两次
这就是动态规划问题的第一个性质:重叠子问题
2.带备忘录的递归解法
既然耗时是重复计算的问题,我们就可以造一个备忘录,每次算出一个子问题的答案以后,先不要着急返回。先记到备忘录里面再返回。每次遇到一个子问题先去备忘录中进行查一查。如果发现之前已经解决过这个问题了。直接把答案拿出来用。不要在耗时去进行计算。
一般使用一个数组来充当这个备忘录。当然也可以使用map来进行,思想都是一样的。
在这里插入图片描述

这个时候,可以发现就会简单很多,此时递归图就会成为在这里插入图片描述
其实就是计算f(20) f(19) f(18)…时间复杂度变成了n 实际上带备忘录的递归解法和迭代的动态规划解法一样。只不过递归是自顶而下而动态规划是自底向上。递归是从f(20)到f(1) f(2) 而动态规划是从f(1) f(2) 到f(20),这也就是动态规划的思路。所以,动态规划一般都脱离了递归,而是由循环迭代完成计算。

dp数组的迭代解法
有了备忘录的启发,我们可以把这个备忘录独立出来成为一张表。这就叫做DP table 在这个表上完成自底向上的推算。
答案如下:
在这里插入图片描述
这里,引出状态转移方程这个名词,实际上就是描述问题结构的数学形式:
在这里插入图片描述
为什么叫做状态转移方程呢?其实就是听起来高端而已,f(n)是一个状态n,这个状态n是由状态n-1和状态n-2相加转移而来。这就叫做状态转移,仅此而已。
例如:return f(n-1) + f(n-2),dp[i] = dp[i-1] + dp[i-2] 这种都是围绕这个方程式的不同表现形式。可见列出状态转移方程的重要性,它是解决问题的核心,而且很容易发现,其实状态转移方程直接代表着暴力解法。
千万不要看不起暴力解,动态规划问题最困难就是写出这个暴力解,即状态转移方程只要写出这个暴力解,优化方法无非就是使用备忘录或者DP table 再无奥妙可言。
这个例子的最后,还可以进一步优化,斐波那契数列的状态转移方程,当前状态只和之前的两个状态有关,其实并不需要那么长的DP table来存储所有的状态。只要想办法存储之前的两个状态就可以了。所以,可以进一步优化,把空间复杂度降为o(1)。
就是数组只存三个数就可以了。

动态规划的另一个重要特性 最优子结构

上面的那个例子主要是说明了重叠子问题的消除方法。因为没有涉及到求最值,其实严格来说不算是动态规划问题。下面这个问题将会体现最优子结构这个过程。
最优子结构的意思就是子结构已经是最优解了,然后用子结构这个最优解来求父结构。
以这个分硬币为例
在这里插入图片描述
很显然,当我需要求amount=11的时候,我要是知道amount=10的最优解结构时,只需要+1即可得到其一个解,当然不一定最优。或者我知道amount = 6的最优解时,只需要+5(加一个硬币)就可以得到最优解的一个解。
这种就是最优解结构,但是,注意子问题不能相互限制,也就是说,硬币的数量没有限制,要相互独立。不能说用了几个硬币a就得使用几个硬币吧b。
在这里插入图片描述

写在最后

第一个斐波那契数列的问题,解释了如何通过「备忘录」或者「dp table」的方法来优化递归树,并且明确了这两种方法本质上是一样的,只是自顶向下和自底向上的不同而已。
第二个问题是标准的动态规划问题。求的是最值问题,分析了最优子结构。
计算机问题本身就是穷举问题穷举所有可能性,算法设计无非就是先思考如何穷举,然后再追求如何聪明的穷举。
列出动态转移方程是在解决如何穷举的问题
备忘录。DP table就是在追求如何聪明地穷举,用空间换时间的思路。是降低时间复杂度的不二法门。

动态规划 ----中
在这里插入图片描述
为什么斐波那契数列数据比较大时,用递归效率很慢:
在这里插入图片描述
在这个递归树中,进行了多次计算:避免这些重复计算,就可以用备忘录来进行记录;
使用一个数据来记忆。
递归:以其为例:是一个自上向下的解决问题,所有,能自上向下解决的问题,都可以使用自下向上来进行解决,就是自上向下解决问题会简单一点。
在这里插入图片描述
注意:这个时间复杂度是On的,只访问了一次。 不过不用这个备忘录的话,时间复杂度是n的次方的。
动态规划的定义:
在这里插入图片描述
在这里插入图片描述
刚刚备忘录是使用了自顶向下的解决问题:
力扣 70题:
在这里插入图片描述
思路如下:
在这里插入图片描述
代码如下:这里要注意:爬i步电梯,可以爬了i-2步,然后爬二步,这是一种情况,也可以是爬了i-1步,爬一步,这也是一种情况;所以,ints[i] 分为两种情况 分别是ints[i-2]和ints[i-1]。所以,把这两个相加就可以了。注意:不要再加1了,这两种情况相加就好了。
比如说 走3步台阶,可以分为已经走了两步台阶了,再走一步;也可分为走了一步台阶了,走两步;
情况就是 (1) 1 1 1;(2)2 1 ;(3) 1 2;这三种情况。
在这里插入图片描述
下面进行的是力扣120
在这里插入图片描述
思路如下:第一个list不管
第二个list 的情况是[5,6]
第三个list的情况是[11,10,11] 中间这个数可能情况为 5 + 5 =10 5 + 6 =11 由于,我们是需要弄最小值,所以,保存10
第四个listde 情况是[15,11,18,14];
所以,最小值为11;
代码如下:

import java.util.ArrayList;
import java.util.List;

public class Solution120 {
    public static int minimumTotal(List<List<Integer>> triangle) {
        for (int i = 1; i < triangle.size(); i++) {

            List<Integer> list = triangle.get(i);
            int size = list.size();
            List<Integer> beforeList = triangle.get(i - 1);

            Integer integer = list.get(0);
            list.set(0,beforeList.get(0) + integer);
            for (int j = 1; j < size - 1; j++) {
                    if (beforeList.size() == 1){
                        list.set(1,beforeList.get(0) + list.get(1));
                        continue;
                    }
                    int min = Math.min(beforeList.get(j), beforeList.get(j - 1));
                    Integer value = list.get(j);
                    list.set(j,value + min);
            }
            int j = size - 1;
            Integer value = beforeList.get(j - 1);
            list.set(j, value + list.get(j));
        }
        List<Integer> list = triangle.get(triangle.size() - 1);
        int res = list.get(0);
        for (int i = 1; i < list.size(); i++) {
            res = Math.min(res, list.get(i));
        }
        return res;
    }

    public static void main(String[] args) {
        ArrayList<List<Integer>> lists = new ArrayList<>();
        ArrayList<Integer> list1 = new ArrayList<>();
        ArrayList<Integer> list2 = new ArrayList<>();
        ArrayList<Integer> list3 = new ArrayList<>();
        list1.add(2);
        list2.add(3);
        list2.add(4);
        list3.add(6);
        list3.add(5);
        list3.add(7);

        lists.add(list1);
        lists.add(list2);
        lists.add(list3);
        System.out.println(minimumTotal(lists));
    }
}

动态规划 题343
在这里插入图片描述
对于,这道题目:刚刚拿到的时候,会没有思路,那么就选择用暴力解法,先试试;
但是:由于,我们不知道它的循环次数,所以,用for是解决不了的。对于,用for解决不了的事情,思路要转向使用递归来进行解决。
在这里插入图片描述
这样就转化成为了递归结构,这样就可以使用回溯算法来进行解决了。这里有重叠的子问题。
解决最优解的问题
可以转化为通过求子问题的最优解,可以获得原问题的最优解,这样的结构就叫做最优子结构。
由此可得,上面的就是一个最优子结构。
在这里插入图片描述
代码如下:

import org.omg.PortableInterceptor.INACTIVE;

import java.util.HashMap;
import java.util.Map;

public class Solution343 {

    public int integerBreak(int n) {
        HashMap<Integer, Integer> map = new HashMap<>();
        return breakInteger(n, map);
    }

    private int breakInteger(int n, Map<Integer,Integer> map) {
        if (n == 1){
            return 1;
        }
        Integer target = map.get(n);
        if (target != null){
            return target;
        }
        int result = 0 ;
        for (int i = 1; i < n; i++) {
            // 直接返回,递归后返回,之前存的最大值,三者取最大,作为最终值来进行返回
            result = Math.max(i * (n-i), Math.max(i * breakInteger(n - i, map), result));
        }
        // 搞一个备忘录来处理问题
        map.put(n, result);
        return result;
    }

    public static void main(String[] args) {
        Solution343 solution343 = new Solution343();
        System.out.println(solution343.integerBreak(10));
    }


}

递归,还是比较简单的,分割i-1的最大值,就用breakInteger(i-1)来进行表示即可。很舒服。
这种方法,是使用递归来解决的:这种不是动态规划,为此,下面我们使用动态规划,自底向上的来解决问题:
首先创建一个int[n+1]的数组;int[1] = 1;
把数组1-n的所有数据都从小到大的求解出来:
int[i]的值要进行分割,分割范围j = [1,i-1]
当分割到j时,它的最大值取值,可能是j*(i-j) 也可能是j*(i-j)的最大值,而这个最大值,在之前我们已经给它存好了。所以,可以直接出来的。

动态规划状态定义和状态转移
在这里插入图片描述
如果用暴力解法:n个房子,随机选取有2^n次方的选择 还有检查是否触发报警 所以时间复杂度为:
在这里插入图片描述
由于是组合问题 所以可以用回溯问题来解决一下;
但是,这里不是让你找组合问题,而是最优化问题。而又是一个组合的解空间,很容易就想到了使用递归的方式来解决这个问题;而会不会找到的递归结构中,拥有重叠子问题,以及最优子结构。进而就可以使用记忆化搜索或动态规划。
递归结构如下:
在这里插入图片描述
所以,可以看到会有很多重叠子问题。
状态的定义和状态转移:
在这里插入图片描述
状态:就是定义这个函数的定义:
状态转移:就是定义这个函数应该怎么做
发现最优解问题用备忘录大部分都可以解决:代码如下:

 public int rob(int[] nums) {
        int length = nums.length;
        if (length == 0){
            return 0;
        }
        if (length == 1){
            return nums[0];
        }
        if (length == 2){
            return Math.max(nums[0], nums[1]);
        }else {
            nums[1] = Math.max(nums[0], nums[1]);
        }

        // 统计[0,n-1]中各个位置时能抢劫到的最多的钱
        for (int i = 2; i < length; i++) {
            nums[i] = Math.max(nums[i] +nums[i-2], nums[i-1]);
        }
        return nums[length - 1];
    }

打家劫舍2
下面用一下动态规划
这个题目要是一起做,感觉很难;但是,如果有了第一题的方法;这时候,再对其进行解析;这个题目的意思是,第一个房子和最后一个房子不能一起偷。
可以分成 找出第一个数不考虑的时候的最大值, 再找出最后一个值不考虑的最大值。这两个最大值中最大的一个就是最终答案。

import com.sun.org.apache.xerces.internal.impl.xpath.regex.Match;

import java.util.Map;

public class Solution213 {
    // 还是用备忘录来解决
    // 但是由于他是环形的,所以,第一个位置和最后一个位置不能同时存在
    // 所以,最大值肯定是在不考虑第一个和不考虑最后一个的情况下产生。
    public int rob(int[] nums) {
        int length = nums.length - 1;
        int[] intsIgnoreFirst = new int[length];
        int[] intsIgnoreEnd = new int[length];
        // 执行index次
        for (int i = 0; i < length; i++) {
            intsIgnoreFirst[i] = nums[i + 1];
            intsIgnoreEnd[i] = nums[i];
        }
        int i = rob2(intsIgnoreFirst);
        return Math.max(rob2(intsIgnoreFirst), rob2(intsIgnoreEnd));
    }

    public int rob2(int[] nums){
        int length = nums.length;
        if (length == 0){
            return 0;
        }
        if (length == 1){
            return nums[0];
        }
        if (length == 2){
            return Math.max(nums[0], nums[1]);
        }else {
            nums[1] = Math.max(nums[0], nums[1]);
        }
        for (int i = 2; i < length; i++) {
            nums[i] = Math.max(nums[i-1], nums[i] + nums[i-2]);
        }
        return nums[length - 1];
    }

    public static void main(String[] args) {
        int[] a = {1,3,1,3,100};
        Solution213 solution213 = new Solution213();
        int i = solution213.rob(a);
        System.out.println(i);
    }
}

动态规划升级版

动态规划升级版—双变量情况:
在这里插入图片描述
很显然,这里有两个变量 一个是容量,另一个是重量,我们之前处理的备忘录都是一维数组,现在要处理的是二维数组。所以,会有一个难度上的提升。但是,使用自底向上的方法的话,其实也是可以的。就是要先写出开始的几个备忘录,然后,进行找规律。
在这里插入图片描述
首先先创建一个二维数组,横代表的是背包的容量,竖代表的是选择物品的id
所以,第一行代表的就是,只选择物品0时,在背包容量为0-5时,所能获得的最大总价值。
第二行代表的就是,可选择物品为0-1时,在背包容量为0-5时,所能获得的最大总价值。
第三行代表的是,可选择物品0-2时,在背包容量为0-5时,所能获得的最大总价值。

所以,第一行的结果如下:
在这里插入图片描述
现在,进行查看第二行:很显然,可选择的物品是0,1时,如果背包容量为0,那么能获取的最大值为0,如果背包容量为1,防止最少是能获得6了,因为可选择物品为0时,它就是为6了。那么如果它不选择0,但是由于选择1需要的空间为2,所以,也不行,它最大只能是6了。
在这里插入图片描述
如果背包为2 选择的为0-1 那么最大值,就是6 和( 00 位置上的数加上10)来进行比 因为2-0 = 2;所以为10。
同理:在考虑1 3这个位置时,首先是考虑不把1这个物品放进背包 那就等于6 然后考虑将1这个物品放进背包,那就是1这个物品的价值加上 01(不把1这个物品放进背包 背包空余为1的情况,这个情况最大是6)这个不把1这个物品放进背包的情况相加 就等于 10 + 6 具体图示如下:
在这里插入图片描述
由此推到第三行,2 0其结果肯定为0;
在这里插入图片描述
2-1 的位子 ,由于空间等于1 无法放入1 或 2 的商品,所以,只能考虑0-1 的情况 所以,值为6;也可以直接把1 1 情况的值抄出来。
2-2 由于2号物品需要3的空间,所以,只能变成不考虑2物品的情况 最大值就是10

等到了2-3的位置时,它可以分成 1.不考虑2物品的情况,此时,最大就是1-3 的值,就是16
2.考虑2 物品的情况,此时值为12 + 1-0 的情况 = 12;
所以,2-3的位置上,值为16;

等到了2-4的位置时,它可以分为1.不考虑2物体的情况,此时,最大值就是1-4的值,就是16
2.考虑2物体的情况,此时,值就是 2物体的值 + 1-1的值 = 12 + 6 = 18
所以,2-4的值为22;
等到了 2-5的位置时,它可以分为 1.不考虑2物体的情况,此时,最大值为1-5的值,就是16
2.考虑2物体的情况,此时,最大值为 12 + 1-2的值 就是22;
所以,2-5的值为22
在这里插入图片描述
背包问题升级
由于,第i行元素只依赖于第i-1行元素。理论上,只需要保持两行元素就可以了,所以,空间复杂度,可以将n*c 变成c
首先:先算出i = 0 i = 1的情况
在这里插入图片描述
当算到i=2的时候时,直接用i=2将i=0这一行给占掉。 在这里插入图片描述
来以此类推,上面第一行在处理i为偶数的情况,第二行 处理i为奇数的情况。

在这里插入图片描述
由于背包的容量是有限的,所以可以转化为有限个背包问题
在这里插入图片描述
这时候,就可以使用三维数组了,就是多了一个物品体积的因素而已。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值