【算法】从记忆化搜索到递推——动态规划入门_记忆化搜索和递推-程序员宅基地

技术标签: 算法  记忆化搜索  动态规划  dp  

笔者说:我们为什么要学记忆化搜索?

因为——有些动态规划直接去想递推公式太难了,所以可以先写成记忆化搜索。

由于记忆化搜索是从将大问题分解成子问题的角度去考虑的,所以会简单一些。

本文的题目其实都比较简单,但是为了学习记忆化搜索,还是要用记忆化搜索再做一遍,不要眼高手低。

如果读者觉得本文的题目太简单了,可以去尝试一下 【算法】区间DP (从记忆化搜索到递推DP) 这篇文章中的题目。(笔者也是在做到比较难直接写出递推方法的题目时,才认识到记忆化搜索的重要性!


下面主要就是题单,本文没什么好看好学的。

预备知识

在这里插入图片描述
就像图中写的一样,先思考回溯要怎么写,然后改成记忆化搜索,然后将这个版本的代码翻译成递推公式形式的 dp。

例题:198. 打家劫舍

198. 打家劫舍

在这里插入图片描述
提示:

1 <= nums.length <= 100
0 <= nums[i] <= 400

记忆化搜索

在这里插入图片描述

将大问题分解成子问题,即 dfs (i) 可以分解成 dfs (i - 1),即抢从 0~i 的最好结果可以分解到抢 0 ~ i - 1的结果。

class Solution {
    
    int[] nums, memo;
    int ans = 0;

    public int rob(int[] nums) {
    
        this.nums = nums;
        memo = new int[nums.length];
        Arrays.fill(memo, -1);
        return dfs(0);
    }

    public int dfs(int i) {
    
        if (i >= nums.length) return 0;
        if (memo[i] != -1) return memo[i];
        memo[i] = Math.max(nums[i] + dfs(i + 2), dfs(i + 1));
        return memo[i];
    }
}

从代码可以看出,所有记忆化搜索其实是从暴力 dfs 来的,只不过发现在暴力 dfs 的过程中有些子问题会被重复计算,因此加了一个记忆数组 memo 用来存储已经搜索过的子问题,这也就是 记忆化搜索 名字的由来。

看着记忆化搜索的代码就会比较容易写出递推形式的dp,翻译成 dp 如下:

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

由于 dp 数组的无后效性,因此还可以将 dp 数组优化成两个变量。(这里就不写了

相关题目练习

再次声明!
下列题目都比较简单,有一定基础的人会觉得直接写 dp 反倒比写记忆化搜索还简单一些。(记忆化搜索反倒麻烦了

70. 爬楼梯

70. 爬楼梯

在这里插入图片描述
当前 i 阶的方案数可以从 i - 1 和 i - 2 转移而来。

记忆化搜索

class Solution {
    
    int[] memo;
    int n;

    public int climbStairs(int n) {
    
        this.n = n;
        memo = new int[n + 1];
        Arrays.fill(memo, -1);
        return dfs(n);
    }

    public int dfs(int i) {
    
        if (i <= 2) return i;
        if (memo[i] != -1) return memo[i];
        return memo[i] = dfs(i - 1) + dfs(i - 2);
    }
}

dp

class Solution {
    
    public int climbStairs(int n) {
    
        if (n == 1 || n == 2) return n;
        int[] dp = new int[n];
        dp[0] = 1;
        dp[1] = 2;
        for (int i = 2; i < n; ++i) dp[i] += dp[i - 1] + dp[i - 2];
        return dp[n - 1];
    }
}

746. 使用最小花费爬楼梯

746. 使用最小花费爬楼梯
在这里插入图片描述

提示:

2 <= cost.length <= 1000
0 <= cost[i] <= 999

记忆化搜索

class Solution {
    
    int[] cost, memo;
    int n;

    public int minCostClimbingStairs(int[] cost) {
    
        this.cost = cost;
        n = cost.length;
        memo = new int[n];
        Arrays.fill(memo, -1);
        return Math.min(dfs(n - 1), dfs(n - 2));    // dfs(i)表示从i再走一步需要的花费
    }

    public int dfs(int i) {
    
        if (i < 0) return 0;
        if (memo[i] != -1) return memo[i];
        return memo[i] = cost[i] + Math.min(dfs(i - 1), dfs(i - 2));
    }
}

dp

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

2466. 统计构造好字符串的方案数

2466. 统计构造好字符串的方案数

在这里插入图片描述

记忆化搜索

class Solution {
    
    final long MOD = (long)1e9 + 7;
    long[] memo;
    int zero, one;

    public int countGoodStrings(int low, int high, int zero, int one) {
    
        this.zero = zero;
        this.one = one;
        memo = new long[high + 1];
        Arrays.fill(memo, -1);
        long ans = 0;
        for (int i = low; i <= high; ++i) ans = (ans + dfs(i)) % MOD;
        return (int)ans;
    }

    public long dfs(int i) {
    
        if (i == 0) return 1;	// 边界条件
        if (i < 0) return 0;
        if (memo[i] != -1) return memo[i];
        return memo[i] = (dfs(i - zero) + dfs(i - one)) % MOD;
    }
}

dp

class Solution {
    
    public int countGoodStrings(int low, int high, int zero, int one) {
    
        long[] dp = new long[high + 1];
        dp[0] = 1;
        final long MOD = (long)1e9 + 7;
        long ans = 0;

        for (int i = 1; i <= high; ++i) {
    
            dp[i] = (dp[i] + (i - zero >= 0? dp[i -zero]: 0)) % MOD;
            dp[i] = (dp[i] + (i - one >= 0? dp[i -one]: 0)) % MOD;
            if (i >= low) ans = (ans + dp[i]) % MOD;
        }
        return (int)ans;
    }
}

213. 打家劫舍 II

213. 打家劫舍 II

在这里插入图片描述

记忆化搜索

class Solution:
    def rob(self, nums: List[int]) -> int:
        # 防止一些题目爆栈
        sys.setrecursionlimit(10000000)
        # 在3.9以前的版本没有@cache可使用@lru_cache(maxsize=None)达成一样的效果
        # 当然这里也可以用哈希表手动存
        @cache
        def dfs(i,end,s):
            if i>=end:
                return 0
            if not s:
                return max(dfs(i+1,end,True)+nums[i],dfs(i+1,end,False))
            return dfs(i+1,end,False)
        return max(dfs(1,len(nums)-1,True)+nums[0],dfs(1,len(nums),False))

上面的代码是直接抄过来的,我实在是觉得这题写记忆化搜索太麻烦了,不如直接递推dp。

dp

dp 数组分别考虑两种情况:不偷 0,或者不偷 n - 1。

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

做完这些题,给我的感觉就是——
对于简单的 dp 题,直接写 dp 还更简单一些,硬写记忆化搜索还有点难。

901. 滑雪

https://www.acwing.com/activity/content/problem/content/1013/

在这里插入图片描述

dp[i][j] 表示从 (i, j) 出发可以完成的最长滑雪长度。

import java.util.*;

public class Main {
    
    static int ans = 0, r, c;
    static int[][] m, dp;
    static int[] dx = new int[]{
    -1, 0, 1, 0}, dy = new int[]{
    0, -1, 0, 1};

    public static void main(String[] args){
    
        Scanner sc = new Scanner(System.in);
        r = sc.nextInt();
        c = sc.nextInt();
        m = new int[r][c];
        dp = new int[r][c];
        for (int i = 0; i < r; ++i) {
    
            for (int j = 0; j < c; ++j) {
    
                m[i][j] = sc.nextInt();
            }
        }

        for (int i = 0; i < r; ++i) {
    
            for (int j = 0; j < c; ++j) {
    
                if (dp[i][j] == 0) dfs(i, j);
            }
        }

        System.out.println(ans);
    }

    static int dfs(int i, int j) {
    
        if (dp[i][j] != 0) return dp[i][j];
        int res = 1;
        for (int k = 0; k < 4; ++k) {
    
            int nx = i + dx[k], ny = j + dy[k];
            if (nx >= 0 && nx < r && ny >= 0 && ny < c && m[i][j] > m[nx][ny]) res = Math.max(res, 1 + dfs(nx, ny));
        }
        ans = Math.max(ans, res);
        return dp[i][j] = res;
    }
}
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_43406895/article/details/131541684

智能推荐

黑马程序员_Android开发者必备的42个链接_黑马程序员android 依赖注入-程序员宅基地

文章浏览阅读813次。---------------------- ASP.Net+Android+IOS开发、.Net培训、 期待与您交流!----------------------下面收集了42个帮助大家学习Android的内容链接,部分内容是面向初学者的,帮助大家从头开始学习Android开发,其他则面向较高级的开发者。希望推荐的这些内容对你有帮助。官方网站1、谷歌Android开发_黑马程序员android 依赖注入

tp5项目上传到服务器,tp5上传到云服务器-程序员宅基地

文章浏览阅读253次。tp5上传到云服务器 内容精选换一换更新后端云服务器,可修改字段为后端云服务器的名称和权重,可以为性能好的服务器设置更大的权重,用来接收更多的流量。如果member绑定的负载均衡器的provisioning status不是ACTIVE,则不能更新该member。PUT /v2/{project_id}/elb/pools/{pool_id}/members/{member华为云帮助中心,为用户提..._tp5加密上传服务器

3、Ktor学习-ApplicationCall简介;-程序员宅基地

文章浏览阅读313次。  处理路由时,将获得ApplicationCall的上下文。  ApplicationCall提供对两个主要属性ApplicationRequest和ApplicationResponse的访问。 如其名称所示,它们对应于传入请求和传出响应。 除此之外,它还提供了一个ApplicationEnvironment,以及一些有用的函数来帮助响应客户端请求。 鉴于管道可以异步执行,Applicati..._ktor call获取响应体

【VRRP主备切换】VRRP&STP&OSPF&NAT&3A_基于vrrp+ospf的主备切换-程序员宅基地

文章浏览阅读3.3k次。实验拓扑实验环境两台PC分属VLAN10与VLAN20,PC1为.10网段,PC2为.20网段SW6为傻瓜交换机SW7,SW8上针对两个VLAN分别做VRRP,同时运行STPR1为路由器,在R1上做NAT,R2模拟外网VRRPVRRP虚拟出一个网关设备,主机填写虚拟网关地址(vrid),主机的所有流量发送到虚拟网关设备,再由虚拟网关设备,发送给真实网关设备进行转发数据。正常情况下由mater网关设备管理这个虚拟MAC地址,转发用户数据。当Master失效后,其他设备其中一个设备成为Maste_基于vrrp+ospf的主备切换

tensorflow GPU版本的正确配置过程_tensorflow如何配对gpu-程序员宅基地

文章浏览阅读1.7k次,点赞3次,收藏19次。首先声明:以下均为 Linux(Ubuntu 14.04)环境下,且采用 miniconda(anaconda也可以)进行安装之前配置过一次,虽然最终配好了,但是稀里糊涂的,尤其是CUDA、cudnn、tensorflow 三者版本的问题,之前的配置过程可以看这里。前几天到了新公司,又要配置了,折腾了半天终于算是搞明白应该怎么配置了。顺便提一下,感觉 tensorflow 官网的这个版本图不..._tensorflow如何配对gpu

Logback使用_logback的maxhistory使用方法-程序员宅基地

文章浏览阅读179次。一、什么是Logback Logback是由log4j创始人设计的又一个开源日志组件。logback当前分成三个模块:logback-core,logback- classic和logback-access。logback-core是其它两个模块的基础模块。logback-classic是log4j的一个 改良版本。此外logback-classic完整实现SLF4J API使你可以很方便..._logback的maxhistory使用方法

随便推点

二叉排序树BST+求树深度算法_设计求二叉排序树深度的算法-程序员宅基地

文章浏览阅读2k次,点赞2次,收藏2次。#include "stdio.h"#include "malloc.h"typedef struct node { int key; struct node *lchild, *rchild; }BSTNode, *BSTree;void InsertBST(BSTree *bst, int key){ BSTree s; if (*bst == NULL) { s_设计求二叉排序树深度的算法

c++01背包模版_c++ 背包模板-程序员宅基地

文章浏览阅读668次。01背包dp先找个题目装箱问题需要考虑的就是这件物品放不放题目要求所有物品体积价值最大最后再减一下就好了#include<bits/stdc++.h>using namespace std;int maxv, n;int f[35][20005];//答案存在这!int v[35];//这件物品所需空间int main() { cin >> maxv >> n; for (int i = 1; i <= n; i++) cin >&_c++ 背包模板

微信小程序大学校园二手教材与书籍拍卖系统设计与实现_基于微信公众号的大学校园二手教材与书籍拍卖系统-程序员宅基地

文章浏览阅读569次。目前人们所熟知的二手交易平台如转转、闲鱼等,作为综合型二手交易平台涵盖了大量的商品,但是无法满足很多大学生对于专业书籍的选择以及学习资料的获取,很多大学生还在通过他人介绍来实现二手书籍的交易,结合而当前大学生二手交易的现状,利用微信小程序应用的便捷性开发设计一款针对大学校园环境内的二手教材及书籍拍卖系统,利用该系统实现在线的书籍教材拍卖,通过竞拍和加价的方式来实现书籍的交易,满足学生书籍需求的同时也实现了资源的再利用。关键词:微信小程序;1.4.2 Mysql数据库 2。1.4.3 JAVA语言 3。_基于微信公众号的大学校园二手教材与书籍拍卖系统

解决Linux下同时使用有线和无线网络时,网络连接的优先级问题_linux以太网和无线网冲突问题-程序员宅基地

文章浏览阅读3.2w次,点赞18次,收藏76次。问题是这样的:本人自己用一台Linux服务器,平时当FTP和爬虫用。还有一台mac开发用,经常需要用网线和linux通过网线直连来传输数据和控制服务器。蛋疼的事发生了:Linux服务器一旦插上网线,网络流量就只能经过有线网络了,导致Linux服务器不能上网。 查了很多资料,在我的服务器上都行不通。最后终于用route路由表配置默认网关解决了。具体方法很简单,如下:1.查看当前网关信息 ip rou_linux以太网和无线网冲突问题

AFLW2000-3D数据库介绍及自带代码使用-程序员宅基地

文章浏览阅读5.4k次,点赞3次,收藏17次。简介:AFLW2000-3D由AFLW数据库的前2000张图片及其三维信息组成。三维信息由3DMM重建(Blanz et.al A morphable model for the synthesis of 3d faces, SIGGRAPH'99)得到,并且包含68个特征点的三维信息。该数据库的三维数据精准度存在争议。数据库自带了matlab处理的代码,可以将人脸三维数据用3DMM表示。..._aflw2000-3d

从前端程序员的视角看小程序的稳定性保障_小程序稳定性怎么提高-程序员宅基地

文章浏览阅读1.2k次。当我们谈业务稳定性的时候,通常是指后端工程师从架构的角度来看的,例如限流和降级、流量调度、业务开关、容量压测等,但监控也是整个业务稳定性建设中不可或缺的一环,例如对业务和前端的监控,以保证出现问题的时候,可以第一时间找到根因所在。今天,我们就结合小程序的场景,来看看如何做好小程序的监控。 本文转载至InfoQ大前端技术号「前端之巅」,作者慕扉,阿里巴巴高级前端工程师。 小程..._小程序稳定性怎么提高