动态规划学习笔记

背景

一般形式是求最值,核心是穷举。

首先,虽然动态规划的核心思想就是穷举求最值,但是问题可以千变万化,穷举所有可行解其实并不是一件容易的事,需要你熟练掌握递归思维,只有列出正确的「状态转移方程」,才能正确地穷举。而且,你需要判断算法问题是否具备「最优子结构」,是否能够通过子问题的最值得到原问题的最值。另外,动态规划问题存在「重叠子问题」,如果暴力穷举的话效率会很低,所以需要你使用「备忘录」或者「DP table」来优化穷举过程,避免不必要的计算。

以上提到的重叠子问题、最优子结构、状态转移方程就是动态规划三要素。具体什么意思等会会举例详解,但是在实际的算法问题中,写出状态转移方程是最困难的,这也就是为什么很多朋友觉得动态规划问题困难的原因。以下是总结的一个思维框架,辅助思考状态转移方程:

明确 base case -> 明确「状态」-> 明确「选择」 -> 定义 `dp` 数组/函数的含义

下面会举两道题目的例子进行解释。

509. 斐波那契数

https://leetcode.cn/problems/fibonacci-number/description

暴力递归

//最简单递归
int fib(int N) {
    if (N == 1 || N == 2) return 1;
    return fib(N - 1) + fib(N - 2);
}

低效的原因:

存在大量重复计算,比如 `f(18)` 被计算了两次,而且你可以看到,以 `f(18)` 为根的这个递归树体量巨大,多算一遍,会耗费巨大的时间。更何况,还不止 `f(18)` 这一个节点被重复计算,所以这个算法及其低效。

这就是动态规划问题的第一个性质:重叠子问题。下面,我们想办法解决这个问题。

带备忘录的递归解法

int fib(int N) {
	//初始化备忘录全为0
	int[] memo = new int[N + 1];
	return dp(memo[], N);
}

//带着备忘录进行递归
int dp(int[] memo, int n) {
	//base case
	if (n == 0 || n == 1) {
		return n;
	}
	
	if (memo[n] != 0) {
		return memo[n];
	}

	memo[n] = dp(memo, n - 1) + dp(memo, n - 2);

	return memo[n];
}

实际上,带[备忘录]的递归算法,把一棵存在巨量几余的递归树通过[剪枝],改造成了一幅不存在几余的递归图,极大减少了子问题 (即递归图中节点)的个数。

子问题个数,即图中节点的总数,由于本算法不存在冗余计算,子问题就是 `f(1)`, `f(2)`, `f(3)` ... `f(20)`,数量和输入规模 n = 20 成正比,所以子问题个数为 O(n)。解决一个子问题的时间,同上,没有什么循环,时间为 O(1)。

所以,本算法的时间复杂度是 O(n),比起暴力算法,是降维打击。

啥叫「自顶向下」?注意我们刚才画的递归树(或者说图),是从上向下延伸,都是从一个规模较大的原问题比如说 `f(20)`,向下逐渐分解规模,直到 `f(1)` 和 `f(2)` 这两个 base case,然后逐层返回答案,这就叫「自顶向下」。

啥叫「自底向上」?反过来,我们直接从最底下、最简单、问题规模最小、已知结果的 `f(1)` 和 `f(2)`(base case)开始往上推,直到推到我们想要的答案 `f(20)`。这就是「递推」的思路,这也是动态规划一般都脱离了递归,而是由循环迭代完成计算的原因。

//这种是从底向上算的版本
int fib(int N) {
    if (N == 0) return 0;
    int[] dp = new int[N + 1];
    // base case
    dp[0] = 0; dp[1] = 1;
    // 状态转移
    for (int i = 2; i <= N; i++) {
        dp[i] = dp[i - 1] + dp[i - 2];
    }

    return dp[N];
}

所以说自顶向下、自底向上两种解法本质其实是差不多的,大部分情况下,效率也基本相同。

这里,引出「状态转移方程」这个名词,实际上就是描述问题结构的数学形式:

为啥叫「状态转移方程」?其实就是为了听起来高端。

`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,再无奥妙可言。

322. 零钱兑换

https://leetcode.cn/problems/coin-change/description/

暴力递归

首先,这个问题是动态规划问题,因为它具有「最优子结构」的。要符合「最优子结构」,子问题间必须互相独立。啥叫相互独立?你肯定不想看数学证明,我用一个直观的例子来讲解。

        回到凑零钱问题,为什么说它符合最优子结构呢?假设你有面值为 `1, 2, 5` 的硬币,你想求 `amount = 11` 时的最少硬币数(原问题),如果你知道凑出 `amount = 10, 9, 6` 的最少硬币数(子问题),你只需要把子问题的答案加一(再选一枚面值为 `1, 2, 5` 的硬币),求个最小值,就是原问题的答案。因为硬币的数量是没有限制的,所以子问题之间没有相互制,是互相独立的。

所以我们可以这样定义 `dp` 函数:`dp(n)` 表示,输入一个目标金额 `n`,返回凑出目标金额 `n` 所需的最少硬币数量。

👆想明白这个点之后就可以写伪代码了!!

// 伪码框架
int coinChange(int[] coins, int amount) {
    // 题目要求的最终结果是 dp(amount)
    return dp(coins, amount)
}

// 定义:要凑出金额 n,至少要 dp(coins, n) 个硬币
int dp(int[] coins, int n) {
    // 做选择,选择需要硬币最少的那个结果
    for (int coin : coins) {
        res = min(res, 1 + dp(coins, n - coin))
    }
    return res
}

再往上加入base case 搞定,这题环境下的base case是,当n<0时返回-1,=0时返回0(也就是结束递归的情况。

int coinChange(int[] coins, int amount) {
    // 题目要求的最终结果是 dp(amount)
    return dp(coins, amount)
}

// 定义:要凑出金额 n,至少要 dp(coins, n) 个硬币
int dp(int[] coins, int amount) {
    // base case
    if (amount == 0) return 0;
    if (amount < 0) return -1;

    int res = Integer.MAX_VALUE;
    for (int coin : coins) {
        // 计算子问题的结果
        int subProblem = dp(coins, amount - coin);
        // 子问题无解则跳过
        if (subProblem == -1) continue;
        // 在子问题中选择最优解,然后加一
        res = Math.min(res, subProblem + 1);
    }

    return res == Integer.MAX_VALUE ? -1 : res;
}

递归算法的时间复杂度分析:子问题总数 x 解决每个子问题所需的时间。

带备忘录的递归

类似之前斐波那契数列的例子,只需要稍加修改,就可以通过备忘录消除子问题:

class Solution {
    int[] memo;

    int coinChange(int[] coins, int amount) {
        memo = new int[amount + 1];
        // 备忘录初始化为一个不会被取到的特殊值,代表还未被计算
        Arrays.fill(memo, -666);

        return dp(coins, amount);
    }

    int dp(int[] coins, int amount) {
        if (amount == 0) return 0;
        if (amount < 0) return -1;
        // 查备忘录,防止重复计算
        if (memo[amount] != -666)
            return memo[amount];

        int res = Integer.MAX_VALUE;
        for (int coin : coins) {
            // 计算子问题的结果
            int subProblem = dp(coins, amount - coin);
            // 子问题无解则跳过
            if (subProblem == -1) continue;
            // 在子问题中选择最优解,然后加一
            res = Math.min(res, subProblem + 1);
        }
        // 把计算结果存入备忘录
        memo[amount] = (res == Integer.MAX_VALUE) ? -1 : res;
        return memo[amount];
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/315456.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Python】新鲜出炉的海洋捕食者算法Python版本

2020年发表的海洋捕食者算法《Marine Predators Algorithm: A nature-inspired metaheuristic》。 作者只在原论文中给出了MATLAB代码&#xff0c;网上也没有Python版本&#xff0c;我自己用Python重写了MATLAB代码。 """2020海洋捕食者算法 """…

【自控实验】4. 数字仿真实验

本科课程实验报告&#xff0c;有太多公式和图片了&#xff0c;干脆直接转成图片了 仅分享和记录&#xff0c;不保证全对 使用matlab中的simulink进行仿真 实验内容 线性连续控制系统的数字仿真 根据开环传递函数G(S)的不同&#xff0c;完成两个线性连续控制系统的仿真。 …

C#上位机与欧姆龙PLC的通信12----【再爆肝】上位机应用开发(WPF版)

1、先上图 继上节完成winform版的应用后&#xff0c;今天再爆肝wpf版的&#xff0c;看看看。 可以看到&#xff0c;wpf的确实还是漂亮很多&#xff0c;现在人都喜欢漂亮的&#xff0c;颜值高的&#xff0c;现在是看脸时代&#xff0c;作为软件来说&#xff0c;是交给用户使用的…

【目标检测】YOLOv5算法实现(七):模型训练

本系列文章记录本人硕士阶段YOLO系列目标检测算法自学及其代码实现的过程。其中算法具体实现借鉴于ultralytics YOLO源码Github&#xff0c;删减了源码中部分内容&#xff0c;满足个人科研需求。   本系列文章主要以YOLOv5为例完成算法的实现&#xff0c;后续修改、增加相关模…

vue3hooks的使用

在 Vue 3 中&#xff0c;hooks 是用于封装组件逻辑的方法&#xff0c;类似于 Vue 2 中的 mixins。 使用 Hooks 可以提高代码的可维护性、可读性、可复用性和可测试性&#xff0c;降低代码之间的耦合度&#xff0c;使得组件的状态更加可控和可预测。 要使用 hooks&#xff0c;…

【VRTK】【Unity】【游戏开发】更多技巧

课程配套学习项目源码资源下载 https://download.csdn.net/download/weixin_41697242/88485426?spm=1001.2014.3001.5503 【概述】 本篇将较为零散但常用的VRTK开发技巧集合在一起,主要内容: 创建物理手震动反馈高亮互动对象【创建物理手】 非物理手状态下,你的手会直接…

MATLAB - 四旋翼飞行器动力学方程

系列文章目录 前言 本例演示了如何使用 Symbolic Math Toolbox™&#xff08;符号数学工具箱&#xff09;推导四旋翼飞行器的连续时间非线性模型。具体来说&#xff0c;本例讨论了 getQuadrotorDynamicsAndJacobian 脚本&#xff0c;该脚本可生成四旋翼状态函数及其雅各布函数…

C++|44.智能指针

文章目录 智能指针unique_ptr特点一——无法进行复制 shared_ptr特点一——可复制特点二——计数器&#xff08;用于确定删除的时机&#xff09; 其他 智能指针 通常的指针是需要特殊地去申请对应的空间&#xff0c;并在不使用的时候还需要人工去销毁。 而智能指针相对普通的指…

ubuntu20.04网络问题以及解决方案

1.网络图标消失&#xff0c;wired消失&#xff0c;ens33消失 参考&#xff1a;https://blog.51cto.com/u_204222/2465609 https://blog.csdn.net/qq_42265170/article/details/123640669 原始是在虚拟机中切换网络连接方式&#xff08;桥接和NAT&#xff09;&#xff0c; 解决…

Java-网络爬虫(三)

文章目录 前言一、爬虫的分类二、跳转页面的爬取三、网页去重四、综合案例1. 案例三 上篇&#xff1a;Java-网络爬虫(二) 前言 上篇文章介绍了 webMagic&#xff0c;通过一个简单的入门案例&#xff0c;对 webMagic 的核心对象和四大组件都做了简要的说明&#xff0c;以下内容…

LeetCode---121双周赛---数位dp

题目列表 2996. 大于等于顺序前缀和的最小缺失整数 2997. 使数组异或和等于 K 的最少操作次数 2998. 使 X 和 Y 相等的最少操作次数 2999. 统计强大整数的数目 一、大于等于顺序前缀和的最小缺失整数 简单的模拟题&#xff0c;只要按照题目的要求去写代码即可&#xff0c;…

高级分布式系统-第6讲 分布式系统的容错性--进程的容错

分布式系统的容错原则既适用于硬件&#xff0c; 也适用于软件。 两者的主要区别在于硬件部件的同类复制相对容易&#xff0c; 而软件组件在运行中的同类复制&#xff08; 进程复制&#xff09; 涉及到更为复杂的分布式操作系统的容错机制。 以下是建立进程失效容错机制的一些基…

腾讯云添加SSL证书

一、进入腾讯云SSL证书&#xff1a; ssl证书控制台地址 选择“我的证书”&#xff0c;点击"申请免费证书" 2、填写域名和邮箱&#xff0c;点击“提交申请” 在此页面中会出现主机记录和记录值。 2、进入云解析 DNS&#xff1a;云解析DNS地址 进入我的解析-记录…

css3基础语法与盒模型

css3基础语法与盒模型 前言CSS3基础入门css3的书写位置内嵌式外链式导入式&#xff08;工作中几乎不用&#xff09;行内式 css3基本语法css3选择器标签选择器id选择器class类名原子类复合选择器伪类元素关系选择器序号选择器属性选择器css3新增伪类![在这里插入图片描述](https…

AI教我学编程之C#类型

前言 在上一课 中我们通过C#入门程序了解到关于C#的基础知识&#xff0c;这节课我们来感受作为C家族最大的黑马&#xff0c;在TIOBE榜单 上受欢迎程度未来两个月可能超越java的存在&#xff1a;C#的魅力 重点先知 1、C#程序或DLL的源代码是一组类型声明。 2、对于可执行程序&…

高压消防泵:科技与安全性的完美结合

在现代社会&#xff0c;随着科技的不断发展&#xff0c;各种高科技设备层出不穷&#xff0c;为我们的生活带来了极大的便利。在森林火灾扑救领域&#xff0c;恒峰智慧科技研发的高压消防泵作为一种高效、节能、绿色、环保的优质设备&#xff0c;将科技与安全性完美地结合在一起…

【面试突击】注册中心面试实战

&#x1f308;&#x1f308;&#x1f308;&#x1f308;&#x1f308;&#x1f308;&#x1f308;&#x1f308; 欢迎关注公众号&#xff08;通过文章导读关注&#xff1a;【11来了】&#xff09;&#xff0c;及时收到 AI 前沿项目工具及新技术 的推送 发送 资料 可领取 深入理…

uniapp 如何使用echarts 以及解决tooltip自定义不生效问题

使用的是echarts-for-wx插件&#xff1b; 正常写法案例&#xff1a;给tooltip数值加个% <template><view><uni-ec-canvas class"uni-ec-canvas"id"uni-ec-canvas"ref"canvas"canvas-id"uni-ec-canvas":ec"ec&quo…

蚁群算法(ACO)解决旅行商(TSP)问题的python实现

TSP问题 旅行商问题&#xff08;Travelling Salesman Problem, 简记TSP&#xff0c;亦称货郎担问题)&#xff1a;设有n个城市和距离矩阵D [dij]&#xff0c;其中dij表示城市i到城市j的距离&#xff0c;i, j 1, 2 … n&#xff0c;则问题是要找出遍访每个城市恰好一次的一条回…

Salesforce财务状况分析

纵观Salesforce发展史和十几年财报中的信息&#xff0c;Salesforce从中小企业CRM服务的蓝海市场切入&#xff0c;但受限于中小企业的生命周期价值和每用户平均收入小且获客成本和流失率不对等&#xff0c;蓝海同时也是死海。 Salesforce通过收购逐渐补足品牌和产品两块短板&am…