让三岁小孩都能理解动态规划_来自B站罐装-蜜糖

系列文章目录



一、认识算法

不同于二分查找和堆排序,这种有明确步骤的算法,用一个不太恰当的例子,就像我们在做菜的时候,一般算法就像是先放两克盐,再放两克鸡精,动态规划更像是说先加盐少许,再加鸡精少许,最后达到好吃即可。也是将大问题拆分成小问题。

动态规划难在哪?

  1. 没有一个统一解的算法思想,不容易学透。
  2. 不同方法间的难度差距很大

学习目标

一月入门,二月上手
结合目标,以退为进
如果不是为了进大厂,不需要掌握很好,掌握基本题目即可。

二、记忆化搜索 非常直觉的处理方式

在这里插入图片描述

  1. 首先初始化一个保存记忆搜索内容的缓存,将它初始化成一些数字,这里为“-1”。
  2. 然后在我们执行递归的方法中,往往我们一上来先判断终止条件,这里模板上的终止条件是小于等于1,这时候我们就返回默认值。
  3. 然后判断是否命中记忆缓存,如果命中,直接返回缓存。 然后执行我们的状态转移方程,这里状态转移方程是dp[n]=dp[n-1]+dp[n-2]
  4. 之后更新缓存,并返回结果。

注意:

题目如果足够简单就不用了。
因为使用了系统栈,速度较慢,可能会超时。
当我们完成了记忆化搜索的动态规划后,我们可以根据现在实现的逻辑,将其改为使用矩阵的状态转移动态规划。下面用具体例子练习。

三、70.爬楼梯 入门 模板

在这里插入图片描述
解题思路:
我们可以想到对于爬到第n个台阶,它有两种情况被爬到,第一种从第n-1阶台阶爬1个台阶,第二种从第n-2阶台阶爬2个台阶。所以它被爬到的不同方法,是这两种情况的总和。并且我们使用记忆化搜索(比较符合直觉)。代码如下所示:

class Solution {
   
HashMap<Integer,Integer> map = new HashMap<>();

public int climbStairs(int n){
    if(n == 1){
        return 1;
    }
    if(n == 2){
        return 2;
    }
    if(map.containsKey(n)){return map.get(n);}//记忆力搜索。
    int res =climbStairs(n-1)+ climbStairs(n-2);
    map.put(n,res);
    return res;
    }
}

如果我们按照从下向上的方式上台阶的话,从0和1的位置都只有一种答案,那我们就从0和1开始填写,填写过程:2=1+0,3=1+2,4=3+2(此处数字表示位置)我们可以看出可以通过for循环完成对数组的填写,代码如下:
在这里插入图片描述

通过记忆化搜索 发现动态规划四要素

  1. 状态类型(前缀的、坐标的、区间的)
  2. 转移方程 比如res =climbStairs(n-1)+ climbStairs(n-2);
  3. 数据初始化
  4. 答案位置 比如本体最终答案在n上。

四、118.杨辉三角 使用答案空间处理(题目给了返回值的样式)

在这里插入图片描述

class Solution {
    public List<List<Integer>> generate(int numRows) {
        List<List<Integer>> res = new ArrayList<>();
        List<Integer> one = new ArrayList<>();
        one.add(1);
        res.add(one);
        if(numRows ==1){
            return res;
        }
        List<Integer> two = new ArrayList<>();
        two.add(1);
        two.add(1);
        res.add(two);
        if(numRows ==2){return res;}
        for(int i=2;i<numRows ;i++){
            List<Integer> line = new ArrayList<>();
            line.add(1);
            List<Integer> last =res.get(i-1);
            for(int j=0;j<i-1;j++){
                line.add(last.get(j)+ last.get(j+1));
            }
            line.add(1);
            res.add(line);
        }
    return res;
    }
}

五、198.打家劫舍 记忆化搜索转化

在这里插入图片描述
我们对一个位置的处理是否是正确的,我们只需要对比两种情况,选这个位置或者不选这个位置。
选了这个位置,相当于位置n的值与与上一个位置n-2的最优值相加,再不选这个位置的情况下,则我们相当于直接选了上一个位置n-1的最优值。

class Solution {
    public int rob(int[] nums) {
        if(nums.length ==0){return 0 ;}
        if(nums.length ==1){return nums[0];}
        int n = nums.length;
        int[] dp = new int[n];
        dp[0]= nums[0];
        dp[1] = Math.max(nums[1], nums[0]);
        for(int i=2;i<n;i++){
            dp[i]= Math.max(dp[i-2]+ nums[i],dp[i -1]);
        }
        return dp[n-1];
    }
}

六、279.完全平方数 背包问题

在这里插入图片描述
我们取的完全平方数得范围在 [1, 根号n ]之间,
我们先对更好理解的但是超时的答案代码进行分析,其超时答案代码如下:

class Solution {
    public int numSquares(int n) {
        ArrayList<Integer> list = new ArrayList<>();
        for( int i =1;i*i<=n;i++){
            list.add(i * i);
        }
        int len = list.size();
        int[][] dp = new int[len][n+1];
        for(int i =0;i<len;i++){
            Arrays.fill(dp[i], -1);
        }
        return process(list, 0, n, dp); 
    }
    private int process(List<Integer> list, int index, int rest, int[][] dp){
        if(rest == 0){
            return 0;
        }
        if(index == list.size()){
            return Integer.MAX_VALUE;
        }
        if(dp[index][rest] != -1){
            return dp[index][rest];
        }
        int curr = list.get(index);
        int res = Integer.MAX_VALUE;
        for(int i =0;curr *i<=rest;i++){
            final int process = process(list, index + 1, rest - curr * i,dp);
            if(process != Integer.MAX_VALUE){
                res = Math.min(res, i+process);
            }
        }
        dp[index][rest]= res;
        return res;
    }
}

为了存放完全平方数,我们需要执行如下代码:

ArrayList<Integer> list = new ArrayList<>();
for( int i =1;i*i<=n;i++){
     list.add(i * i);
}

为了实现记忆化搜索,我们需要执行如下代码:我们为每个完全平方数,设置了一个n+1长度的数组,也就是可以存放等于n和比n小的所有整数a所对应的一个数字,这个数字就是这个a可以被最少个数的完全平方数表示的个数。这里我们用“-1”来初始化,等于“-1”表明此时这个位置没有存储记忆。

int len = list.size();
int[][] dp = new int[len][n+1];
for(int i =0;i<len;i++){
     Arrays.fill(dp[i], -1);
}

我们再看process这个函数,代码如下,首先rest==0,说明了输入的n已经完全被完全平方数所替代,不需要用新的完全平方数来替代了,所以返回0。index == list.size()时候,这个index如果作为下标,已经越界,需要被终止。dp[index][rest] != -1说明这里存在记忆,可以进行调用。 int curr = list.get(index);是用来找到index所对应的完全平方数,然后for循环是用来将rest依次减去一个完全平方数,两个完全平方数,更多完全平方数,因为一个大的完全平方数可以由几个小的完全平方数所合成,所以由大的完全平方数构成的n所使用的完全平方数个数更少,也就更符合解题目的。代码整体思路是,先把n用小的完全平方数表示出来,他的完全平方数个数肯定比较大,然后再用更大的表示,使用的完全平方数的个数会减小。

private int process(List<Integer> list, int index, int rest, int[][] dp){
        if(rest == 0){
            return 0;
        }
        if(index == list.size()){
            return Integer.MAX_VALUE;
        }
        if(dp[index][rest] != -1){
            return dp[index][rest];
        }
        int curr = list.get(index);
        int res = Integer.MAX_VALUE;
        for(int i =0;curr *i<=rest;i++){
            final int process = process(list, index + 1, rest - curr * i,dp);
            if(process != Integer.MAX_VALUE){
                res = Math.min(res, i+process);
            }
        }
        dp[index][rest]= res;
        return res;
    }

但是超时了,所以记忆化搜索在某些情况下会超时,所以我们需要用状态矩阵的方式再实现一边。因为我们是要找到最小选取数量, 我们可以借用第70.题爬楼梯的思想,对于组成输入n的最少完全平方数个数,可以由它前面的结果得来,只不过爬楼梯是求种类数,是相加,而现在是取最小值Math.min(dp[i+nn[j]], dp[i] + 1)。代码如下:

class Solution {
public int numSquares(int n){
    int nlen = (int)Math.sqrt(n)+ 1;
    int[] nn = new int[nlen];
    for(int i=0;i<nlen; i++){
        nn[i]=(i +1)*(i + 1);
    }
    int[] dp = new int[n+1];
    Arrays.fill(dp, Integer.MAX_VALUE);
    dp[0]=0;
    for(int i=0;i<n; i++){
        for(int j=0;j<nlen; j++){
            if(i + nn[j] <= n){
                dp[i + nn[j]]= Math.min(dp[i+nn[j]], dp[i] + 1);
            }
        }
    }
    return dp[n];
}
}

七、322.零钱兑换 背包问题

在这里插入图片描述
思路与完全平方数相同,代码如下:

class Solution {
    public int coinChange(int[] coins, int amount) {
        if(coins.length == 0){
            return -1;
        }
        int [] dp = new int[amount +1];
        return process(coins,amount,dp);

    }
    private int process(int[] coins,int amount,int[] dp){
        if(amount < 0){
            return -1;
        }
        if(dp[amount] !=0){
            return dp[amount];
        }
        if(amount == 0){
            return 0;
        }
        int res = Integer.MAX_VALUE;
        for(int i =0;i<coins.length;i++){
            if(coins[i]<=amount){
                final int p1 = process(coins, amount - coins[i], dp);
                if(p1!= -1){
                    res = Math.min(res, p1+1);
                }
            }
        }
        res = res == Integer.MAX_VALUE ? -1 : res;
        dp[amount] = res;
        return res;
    }
}

代码改进:此时状态空间为剩余的金额,我们可以在这个状态空间中进行状态的维护,如下图所示,如果硬币面值为“1”,我们就可以根据1个长度之前的最好结果来更新状态,或者“5”同理。
在这里插入图片描述
所以我们在创建了状态空间之后,就可以针对所有硬币的面额尽心循环,然后从有效的位置开始直接便利所有的结果空间,并记录最优情况。注意没有有效结果的话,需要返回“-1”。
在这里插入图片描述

八、139.单词拆分

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值