硅基计划4.0 算法 记忆化搜索


文章目录


我们以斐波那契数做一个简单的示例

可以很清楚的看到我们dfs(2)存在重复递归情况,占用额外开销

因此我们记忆化搜索就是搞一个数组,去记录这次递归的值,当遇完全一样的递归情况的时候直接从这个结果中取值就好,最大程度地避免了重复递归的情况,理论上可以从2^n时间复杂度降到n线性级别的时间复杂度

因此我们备忘录的策略就是,递归返回值返回之前,先把这个值存入备忘录,当我们再次递归到相同的情况的时候,直接从这个数组中取值就好

因此我们斐波那契数可以使用记忆化搜索实现

java 复制代码
class Solution {
    //搞一个记忆化数组
    int [] memory = new int[31];
    public int fib(int n) {
        Arrays.fill(memory, -1);
        return dfs(n);
    }

    private int dfs(int n){
        if(memory[n] != -1){
            //说明已经遍历过了
            return memory[n];
        }
        if(n == 0 || n == 1){
            memory[n] = n;
            return n;
        }
        //向上返回的时候,先把值存入记忆化数组
        memory[n] = dfs(n-1)+dfs(n-2);
        return memory[n];
    }
}

好,我们再来看动态规划是怎么实现的,先说一点,动态规划本质上还是一种记忆化搜索

  1. 给一个dp数组,确定i下标表示的状态,其中dp[i]表示求的是第i个斐波那契数
  2. 推导状态转移方程,即想求i下标值需要依赖前面或后面的的其他dp下标的值来求,即dp[i] = dp[i-1] + dp[i-2];
  3. 初始化,即由于dp[0]要依赖于dp[-1]位置,会越界,因此我们把dp[0]会设置为一个初始值0,即dp[0] = 0,dp[1] = 1
  4. 填表顺序,即我们dp数组的依赖关系,是从前往后,还是从后往前呢
  5. 确定返回值,确定我们最终是要返回dp表的哪个结果,最终返回的是dp[题目求的数]
java 复制代码
class Solution {
    public int fib(int n) {
        int [] dp = new int[31];
        dp[0] = 0;
        dp[1] = 1;
        for(int i = 2;i <= n;i++){
            dp[i] = dp[i-1]+dp[i-2];
        }
        return dp[n];
    }
}

这里有几个问题

  1. 所有递归都可以转换成记忆化搜索吗? 答:看题,不一定能,只有和记忆化搜索匹配才可以
  2. 带备忘录的递归&带备忘录的动态规划&记忆化搜索,这三个是一个东西吗? 答:是
  3. 什么是自顶向下&自底向上? 答:自顶向下是求dfs(5),先求dfs(4) dfs(3) ...,而自底向上是求dfs(5) 先求dfs(0) 和 dfs(1) ....
  4. 是否可以所有题目从暴力搜索-->记忆化搜索-->动态规划? 答:不一定都可以,有的时候你写暴力搜索还不如直接去写动态规划来的省时省力

一、不同路径

题目链接

这题我们解法先想想暴力搜索的代码,我们选取任何一个位置

复制代码
□ ▲ □
▲ ✓ □

想知道从到达有几种走法,无非就是从其他地方到达有多少种走法,到达后再走到位置就好

那我们想要知道到达有几种走法,是不是还是要清楚从周围走到有几种走法

以上就是暴力搜索的过程,放心我帮你们测试过了,已经超时了

那我们想想,是不是存在重复的情况,因为我们的递归函数是dfs(i,j) = dfs(i-1,j) + dfs(i,j-1)

那么就肯定存在重复值,我们就搞一个记忆化数组,每次递归到相同位置的时候,先去记忆化数组看一眼,看是不是存在,存在就直接取值,不存在就直接返回

当我们i == 0 || j == 0越界(我们棋盘下标从1开始),这个时候直接return 0

当我们在起点的时候,因为走到起点也是有一种走法,我们就返回1

java 复制代码
class Solution {
    int [][] memory;
    public int uniquePaths(int m, int n) {
        //使用记忆化搜索
        memory = new int[m+1][n+1];
        return dfs(m,n);
    }

    private int dfs(int posx,int posy){
        if(memory[posx][posy] != 0){
            //说明这个位置已经被递归过了
            return memory[posx][posy];
        }
        if(posx == 1 && posy == 1){
            //此时位于起点,算一种方法
            memory[1][1] = 1;
            return memory[1][1];
        }
        if(posx == 0 || posy == 0){
            //此时已经越界
            return 0;
        }
        //此时递归之前先存好值,因为从周边来到这里的方式只有一种
        memory[posx][posy] = dfs(posx, posy-1)+dfs(posx-1, posy);
        return memory[posx][posy];
    }
}

因此转为动态规划也是这个思路

java 复制代码
class Solution {
    public int uniquePaths(int m, int n) {
        int [][] dp = new int[m+1][n+1];
        dp[1][1] = 1;//注意起点本身也是一种走法
        for(int i = 1;i <= m;i++){
            for(int j = 1;j <= n;j++){
                if(i == 1 && j == 1){
                    continue;
                }
                dp[i][j] = dp[i-1][j] + dp[i][j-1];
            }
        }
        return dp[m][n];
    }
}

二、最长递增的子序列

请注意是子序列不是连续的子区间,子序列的数字可以不连续

比如[1,3,4,6],子序列可以是[1,4,6],而连续的子区间是[1,3,4]

好,回到这一题,我们这一题是要保证后面的数比前面大,那我们可不可以这样子

每次从一个下标开始,依次枚举后面的数(不包括下标数本身),如果比当前下标值大,就继续往后找,直到出现一个小于等于当前下标值的数,枚举就结束,记录这个子序列长度,添加到结果中

java 复制代码
class Solution {
    public int lengthOfLIS(int[] nums) {
        int ret = 0;
        int length = nums.length;
        int [] memory = new int[length];
        for(int i = 0;i < length;i++){
            //每一次枚举从i下标往后的最大值
            ret = Math.max(ret,dfs(nums,i,memory));
        }
        return ret;
    }

    private int dfs(int [] nums,int pos,int [] memory){
        //避免最后一个下标传入,因为本质上最后一个下标情况长度是1
        if(memory[pos] != 0){
            //说明这个位置已经递归过了
            return memory[pos];
        }
        int ret = 1;
        for(int i = pos+1;i < nums.length;i++){
            if(nums[i] > nums[pos]){
                //说明是符合要求的,还是要往后枚举
                //注意i下标本身也是符合要求的,我们递归后枚举的是从i+1下标开始
                //因此要记得加上你1表示i下标也是符合要求的情况
                ret = Math.max(ret,dfs(nums,i,memory)+1);
            }
        }
        //递归前添加值
        memory[pos] = ret;
        return memory[pos];
    }
}

通过这个记忆化搜索我们观察到,每一次我们枚举一个数,这个数都要依赖后面更大的数,因此我们动态规划可以倒着枚举

java 复制代码
class Solution {
    public int lengthOfLIS(int[] nums) {
        int length = nums.length;
        int [] dp = new int[length];
        //直接全部初始化成1,便于边界情况返回
        Arrays.fill(dp, 1);
        //统计dp数组中符合要求的最大子长度
        int max = 0;
        //因为我们dp取值依赖于后面,因此要倒着枚举
        for(int i = length-1;i >= 0;i--){
            for(int j = i+1;j < length;j++){
                if(nums[j] > nums[i]){
                    dp[i] = Math.max(dp[i],dp[j]+1);
                }
            }
            max = Math.max(max,dp[i]);
        }
        return max;
    }
}

每一个dp下标的值都表示从当前下标i开始,后面有多少个比我大,我记录它们的个数(长度),再加上我自己本身的长度1

三、猜数字大小II

题目链接

这题叽里咕噜,啰里八嗦讲了那么多,本质上就是一个给钱游戏
你从1到n中随机选择一个数,作为一个个策略(第一次选择的每种情况都代表一个策略)
接下来从你选的这个数开始,进行猜数字游戏,猜小了猜大了都会有提示
题目要你做的就是,在你当前策略下(第一次选的这个数的策略下),寻找你能遇到的最坏的情况(即你怎么选要给的钱最多)
然后,在所有策略中,你需要去找出那种策略需要给的钱最少
说白了就是要在所有策略中寻找在这种策略的最坏情况下给的钱最少

还是很抽象???我来跟你画个图来讲讲

因此我们每次最开始先选取一个数,然后从这个数开始,枚举比它小的,比它大的,统计结果

但是你想想,是不是存在重复情况啊,比如我上一个策略枚举[1,4]的所有情况,那我下一个策略如果还是要同样枚举[1,4]的所有情况,不就存在重复计算了吗

因此我们搞一个记忆化数组,用来存储不同枚举情况的值,在后续递归的时候直接取就好,避免重复递归

java 复制代码
class Solution {
    int [][] memory;
    public int getMoneyAmount(int n) {
        memory = new int[n+1][n+1];
        return dfs(1,n);
    }

    private int dfs(int start,int end){
        if(start >= end){
            //越界或者是找到了数字
            return 0;
        }
        //判断是否是重复枚举
        if(memory[start][end] != 0){
            return memory[start][end];
        }
        int ret = Integer.MAX_VALUE;
        for(int head = start;head <= end;head++){
            //针对当前选择的head值(是一种策略),进行展开
            //展开左右子树,寻找最坏情况下的值
            int left = dfs(start, head-1);
            int right = dfs(head+1, end);
            //对于所有策略的结果,我们要选择在最坏情况下最省钱的策略
            ret = Math.min(ret,Math.max(left,right)+head);
        }
        memory[start][end] = ret;
        return ret;
    }
}

四、矩阵中最长递增路径

题目链接

这题思路就是暴力从每个位置进行一次尝试,统计在这一次尝试中走的最长的路径长度,再去选取所有尝试中最大的结果

java 复制代码
class Solution {
    int height;
    int wide;
    int [][] memory;
    public int longestIncreasingPath(int[][] matrix) {
        height = matrix.length;
        wide = matrix[0].length;
        memory = new int[height][wide];
        int ret = 0;
        for(int i = 0;i < height;i++){
            for(int j = 0;j < wide;j++){
                ret = Math.max(ret, dfs(matrix, i, j));
            }
        }
        return ret;
    }

    //同理使用向量数组
    int [] x = {1,-1,0,0};
    int [] y = {0,0,1,-1};

    //同理posx表示行,posy表示列
    private int dfs(int [][] matrix,int posx,int posy){
        //当前情况算一个长度,就是以自己就可以组成一条路径
        int ret = 1;
        if(memory[posx][posy] != 0){
            return memory[posx][posy];
        }
        for(int i = 0;i < 4;i++){
            int curX = posx+x[i];
            int curY = posy+y[i];
            if(curX >= 0 && curX < height && curY >= 0 && curY < wide && matrix[curX][curY] > matrix[posx][posy]){
                //返回的时候要+1,因为这里求的是以(x,y)为起点的路径,以(x,y)为起点的自己可以组成一条路径
                ret = Math.max(ret,dfs(matrix, curX, curY)+1);
            }
        }
        memory[posx][posy] = ret;
        return ret;
    }
}

希望本篇文章对您有帮助,有错误您可以指出,我们友好交流


END

相关推荐
CoderYanger27 分钟前
动态规划算法-简单多状态dp问题:18.买卖股票的最佳时机Ⅳ
开发语言·算法·leetcode·动态规划·1024程序员节
大飞哥~BigFei27 分钟前
deploy发布项目到国外中央仓库报如下错误Project name is missing
java
白羊无名小猪27 分钟前
正则表达式(捕获组)
java·mysql·正则表达式
Less is moree29 分钟前
2.C语言文件操作(一):fgetc(),fgets(),fread的区别
c语言·开发语言·算法
狂奔小菜鸡29 分钟前
Day23 | Java泛型详解
java·后端·java ee
onejson29 分钟前
idea中一键执行maven和应用重启
java·maven·intellij-idea
CoderYanger30 分钟前
动态规划算法-简单多状态dp问题:13.删除并获得点数
java·开发语言·数据结构·算法·leetcode·动态规划·1024程序员节
听风吟丶31 分钟前
Java 微服务 APM 实战:Prometheus+Grafana 构建全维度性能监控与资源预警体系
java·微服务·prometheus
浅川.2531 分钟前
xtuoj Balls
数据结构·算法