基础dp——动态规划

基础dp——动态规划

目录

一、什么是动态规划?

二、动态规划的使用步骤

1.状态表示

2.状态转移方程

3.初始化

4.填表顺序

5.返回值

三、试题讲解

1.最小花费爬楼梯

2.下降路径最小和

3.解码方法


一、什么是动态规划?

        动态规划(Dynamic Programming,简称dp)是一种通过将复杂问题分解为更小的子问题来解决的算法思想,尤其适用于具有重叠子问题最优子结构的优化问题。其核心目标是避免重复计算,通过存储中间结果(记忆化)来提升效率。

动态规划 vs 分治法

  • 共同点:都将问题分解为子问题。
  • 区别
    • 分治法(如归并排序)的子问题独立,无重叠,无需存储中间结果。
    • 动态规划的子问题有重叠,需存储中间结果避免重复计算。

二、动态规划的使用步骤

        为了方便理解下面我将从感性的角度来给大家讲解动态规划的使用步骤

1.状态表示

        首先在解决问题的时候我们通常会使用一块空间来记录已处理的数据,我们把这块空间叫作dp表。

        当我们在解决一个小问题时需要借助之前已解决的问题的数据,就可以到dp表里面查,当前这个小问题解决后继续把它相关的信息存到dp表,然后继续解决下一个小问题。

        这个dp表中的一小块数据表示什么?这些问题指的就是状态表示。在用动态规划解决问题时,要面对最重要的问题就是dp表的状态应该表示是什么?

在解决这个问题时我们通常都是从这几个角度去思考:

  • 1.经验+题目要求
  • 2.分析问题的过程中,发现重复子问题。
  • 3.当我们发现子问题后,假设前(或后)几个小问题已经解决,思考我们在解决当前问题时有没有需要前面已解决的问题的什么信息?

2.状态转移方程

        dp[i]等于什么?这就是状态转移方程。它通常要依赖于已经计算过的状态。

3.初始化

在创建好dp表后主要任务就是对dp表的填写,初始化dp表的作用有以下两点:

  1. 保证填表的时候不越界。
  2. 保证填表的正确性。

4.填表顺序

        为使填写当前状态的时候,所需要的状态已经计算过了。

5.返回值

        最后结合题目要求和状态表示计算结果并返回。

三、试题讲解

1.最小花费爬楼梯

        首先我们分析题目,我们的起点是0或1的位置,而终点是n位置(注意不是n-1位置)。我们在爬楼梯时支付一次费用可以爬一个或两个台阶。

        当我们尝试从动态规划方向去解决,那么我们试着去构造一下相同的子问题,并且假设它已经解决。

注:为了方便在下文我都会把动态规划简称为“动规” 

        用动规解题过程中,在找子问题时我总结了一个很厉害的小技巧,就是保留主问题的逻辑,而换一个问题的对象。比如这里,主问题是从起点爬到楼顶的最小花费,那么构造子问题时我们只需要保留这个问题的逻辑,而把楼顶这个对象改成任意位置i。那么我们就得到了一个子问题(即从起点爬到i位置的最小花费)。然后我们再假设前面的相同的子问题已经解决。

比如:

        动规题复杂多变,以上技巧可以解决大部分的基础动规题,但更多的只是作为一个小经验,并不是所有动规题都可以通过构建子问题来解决的。

        在解一个题时,状态表示可以是很多,不同的状态表示就决定了不同的状态转移方程,而要判断一个状态表示是否正确只需看是否能根据该状态表示推出正确的状态转移方程。

能够理解上图那么动态规划的五步骤就水到渠成了。

状态表示: dp[i]:表示爬到i时的最小花费状态转移方程: dp[i]=min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2])初始化: dp表初始化为全0填表顺序: 从下标为2的位置开始,并且从左往右填写返回值: return dp[n]

代码示例:

class Solution { public: int minCostClimbingStairs(vector<int>& cost){ int n=cost.size(); vector<int> dp(n+1);//默认值就是0,不用初始化 for(int i=2;i<=n;i++) dp[i]=min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]); return dp[n]; } };

        除了刚才的状态表示我们还可以换一个,只需要改变一下子问题,同样的技巧,主问题逻辑不变,把起点这个对象改成任意位置i, 那么我们就得到了一个子问题(即从i位置爬到楼顶的最小花费)。

状态表示: dp[i]:表示从i爬到楼顶的最小花费状态转移方程: dp[i]=cost[i]+min(dp[i+1],dp[i+2])初始化: dp[n-1]=cost[n-1],dp[n-2]=cost[n-2]填表顺序: 从右往左返回值: return min(dp[0], dp[1])

        我们可以发现状态表示的改变使得其他四个步骤的实现逻辑改变,所以可见状态表示的重要性。

代码示例:

class Solution { public: int minCostClimbingStairs(vector<int>& cost){ int n=cost.size(); vector<int> dp(n); dp[n-1]=cost[n-1],dp[n-2]=cost[n-2]; for(int i=n-3;i>=0;i--) dp[i]=cost[i]+min(dp[i+1],dp[i+2]); return min(dp[0],dp[1]); } };

        提示:空间优化:在我们填写某一个状态的时候只需要使用它的前两个状态,所以可以把dp数组简化为两个变量就可以解决问题(需要注意变量的更新)。通常把该优化方法叫做“滚动数组”。很多基础动规都可以进行这样的优化,大家可以试着去实现,这一点在后面也不再提。 

        提示:为了减少逻辑上的赘述,在以下题解中我都只会讲解一种状态表示作为示例。 

2.下降路径最小和

通过上一题找子问题的经验,我们可以这样做:

抓住主问题:从第一行任意位置下降到最后一行的任意一个位置的最小和。 

        保留问题的主逻辑,把对象“第一行任意位置”或“最后一行任意位置”更改,比如更改“最后一行任意位置”为“第i行的任意位置j”。得到子问题,即从“第一行任意位置下降到第i行的任意位置j的最小和”

        接下来假设与它相同的子问题都解决,并利用它们的结果。如下:

动规五步骤:

状态表示: dp[i][j]:从第一行任意位置下降到第i行j列的最小和状态转移方程: dp[i][j]=matrix[i][j]+min(dp[i-1][j-1],min(dp[i-1][j],dp[i-1][j+1]) )初始化:越界区域如下:

        

        创建(n+1)*(n+2)的dp表初始化为全INT_MAX(这样创建dp表减少了为防止越界而做特殊判断带来的成本),再将第一行初始化为全0。把dp表初始化为全INT_MAX,是为了防止越界区域参与最小值的筛选再把第一行初始化为全0是逻辑需要,总的目的都是为了保证填表的正确性。填表顺序: 从上到下,从左往右(或从右往左,只要能保证在填写当前状态时需要的状态已计算过即可)返回值: 返回最后一行的最小值

代码示例: 

class Solution { public: int minFallingPathSum(vector<vector<int>>& matrix) { int n=matrix.size();//题目明确指出是方形,所以行数和列数都一样。 vector<vector<int>> dp(n+1,vector<int>(n+2,INT_MAX)); for(int j=0;j<n+2;j++) dp[0][j]=0; for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) dp[i][j]=matrix[i-1][j-1]+min(min(dp[i-1][j-1],dp[i-1][j]),dp[i-1][j+1]); int ret=INT_MAX; for(int j=1;j<=n;j++) ret=min(ret,dp[n][j]); return ret; } };

3.解码方法

        主问题: 一个非空字符s的解码方法的总数。但我们好像无法从中获取到可以更改的对象,再回去观察上面我们解决的两道问题。它们主问题的逻辑中都是带有区间式的,比如第一题:从起点到终点,第二题:从第一行到最后一行。那么如果我们还想要那个找子问题的技巧去确定状态表示,那么在总结主问题时就要朝着带有区间式的方向去想。

        这里我们可以把主问题总结为:从s字符串的下标0开始到下标n-1结束的解码方法总数。那么这样我们就构造出了一个区间“开始”——“结束”。子问题就好构建得多了。

子问题:从下标0开始到下标i结束的解码方法总数。

状态表示: dp[i]:表示从下标0开始到下标i结束的解码方法总数状态转移方程:

        解决了状态表示但这个题的难点主要在于状态转移方程。

     

         我们可以把单独解码和前一个联合解码分开讨论。如果单独解码成功相当于在i-1的所有解码方法中统一加上一个s[i],所以为dp[i-1],解码失败的话,说明不能单独解码,所以结果为0

        同理联合解码成功相当于在i-2的所有解码方法中统一加上一个s[i]与s[i-1]的复合,所以为dp[i-2],解码失败的话,说明不能联合解码所以结果为0。最后只需要把单独解码与联合解码的结果相加。初始化: 因为我们在用dp[i]时需要用到dp[i-1]和dp[i-2]的状态,所以最好把dp[0]和dp[1]初始化。dp[0]可以这样写dp[0]=s[i]!='0',但dp[1]的初始化会比较麻烦,它的初始化逻辑和上面的状态转移方程是一样的。而在下面填表时同样要用到这样的逻辑,就会显得很累赘。

        我们可以使用这样一个初始化技巧:

        那么可以做一个n+1大小的dp表,然后错开一位表示,即 dp[i]:表示从下标0开始到下标i-1结束的解码方法总数。为保证后面的填表顺序正确我们需要dp[1]=s[i]!='0',这是必然的。而dp[0]该初始化为什么?我们可以思考什么时候用到dp[0],即s[0]与s[1]解码成功时,那么它必然是dp[0]=1填表顺序:从dp的2下标开始,并从左往右。返回值: return dp[n]

代码示例:

class Solution { public: int numDecodings(string s) { vector<int> dp(s.size()+1,0); dp[1]=s[0]!='0',dp[0]=1; for(int i=2;i<dp.size();i++) { int m=(s[i-2]-'0')*10+s[i-1]-'0'; if(s[i-1]!='0') dp[i]+=dp[i-1]; if(m>=10&&m<=26) dp[i]+=dp[i-2]; } return dp.back(); } };
非常感谢您能耐心读完这篇文章。倘若您从中有所收获,还望多多支持呀!

Read more

苹果最贵手机要来了!折叠屏iPhone将于9月亮相;部分高校严禁校内使用OpenClaw;黄仁勋预言:传统软件和APP或将消失 | 极客头条

苹果最贵手机要来了!折叠屏iPhone将于9月亮相;部分高校严禁校内使用OpenClaw;黄仁勋预言:传统软件和APP或将消失 | 极客头条

「极客头条」—— 技术人员的新闻圈! ZEEKLOG 的读者朋友们好,「极客头条」来啦,快来看今天都有哪些值得我们技术人关注的重要新闻吧。(投稿或寻求报道:[email protected]) 整理 | 郑丽媛 出品 | ZEEKLOG(ID:ZEEKLOGnews) 一分钟速览新闻点! * 多所高校要求警惕 OpenClaw 安全风险,部分严禁校内使用 * 荣耀 CEO 李健:荣耀机器人全栈自研,将聚焦消费市场 * 马化腾凌晨 2 点发声:还有一批龙虾系产品陆续赶来 * 前快手语言大模型中心负责人张富峥,已加入智源人工智能研究院,负责 LLM 方向 * 最新全球 AI 应用百强榜发布,豆包/DeepSeek/千问上榜 * 苹果折叠 iPhone 将于九月亮相,融合 iPhone 与 iPad 体验

By Ne0inhk
不止“996”!曝硅谷AI创业圈「极限工作制」:每天16小时、凌晨3点下班、周末也在写代码

不止“996”!曝硅谷AI创业圈「极限工作制」:每天16小时、凌晨3点下班、周末也在写代码

编译 | 郑丽媛 出品 | ZEEKLOG(ID:ZEEKLOGnews) “如果你周日去旧金山的咖啡馆,会发现几乎每个人都在工作。” 这是 AI 创业公司 Mythril 联合创始人 Sanju Lokuhitige 最近最直观的感受。去年 11 月,他特地搬到旧金山,只为了更接近 AI 创业浪潮的中心。但很快,他也被卷入了这股浪潮带来的另一面——一种越来越极端的工作文化。 Lokuhitige 坦言,他现在几乎每天工作 12 小时,每周 7 天。除了每周少数几场刻意安排的社交活动(主要是为了和创业者们建立联系),其余时间几乎都在写代码、做产品。 “有时候我整整一天都在编程,”他说,“我基本没有什么工作与生活的平衡。”而这样的生活,在如今的 AI 创业圈里并不算罕见。 旧金山 AI 创业圈的真实日常 一位在旧金山一家 AI

By Ne0inhk
黄仁勋公开发文:传统软件开发模式终结,参与AI不必非得拥有计算机博士学位

黄仁勋公开发文:传统软件开发模式终结,参与AI不必非得拥有计算机博士学位

AI 究竟是什么?在 NVIDIA CEO 黄仁勋看来,它早已不只是聊天机器人或某个大模型,而是一种正在迅速成形的“新型基础设施”。 近日,黄仁勋在英伟达官网发布了一篇长文,提出一个颇具形象的比喻——AI 就像一块“五层蛋糕”。从最底层的能源,到芯片、基础设施、模型,再到最上层的应用,人工智能正在形成一整套完整的产业技术栈,并像电力和互联网一样,逐渐成为现代社会的底层能力。 这也是黄仁勋自 2016 年以来公开发表的第七篇长文。在这篇文章中,他从计算机发展史与第一性原理出发,试图解释 AI 技术栈为何会演化成如今的形态,以及为什么全球正在掀起一场规模空前的 AI 基础设施建设。 在他看来,过去几十年的软件大多是预先编写好的程序:人类设计好算法,计算机按指令执行,数据被结构化存储在数据库中,通过精确查询调用。而 AI 的出现打破了这一模式——计算机开始能够理解图像、文本和声音,并根据上下文实时生成答案、推理结果甚至新的内容。 正因为智能不再是预先写好的代码,而是实时生成的能力,支撑它运行的整个计算体系也必须被重新设计。

By Ne0inhk
猛裁1.6万人后,网站再崩6小时、一周4次重大事故!官方“紧急复盘”:跟裁员无关,也不是AI写代码的锅

猛裁1.6万人后,网站再崩6小时、一周4次重大事故!官方“紧急复盘”:跟裁员无关,也不是AI写代码的锅

整理 | 郑丽媛 出品 | ZEEKLOG(ID:ZEEKLOGnews) 过去几年里,科技公司几乎都在同一件事上加速:让 AI 参与写代码。 从自动补全、自动生成函数,到直接修改系统配置,生成式 AI 已经逐渐走进真实生产环境。但最近发生在亚马逊的一连串事故,却给整个行业泼了一盆冷水——当 AI 开始真正参与生产环境开发时,事情可能远比想象复杂。 最近,多家媒体披露,本周二亚马逊内部紧急召开了一场工程“深度复盘(deep dive)”会议,专门讨论最近频繁出现的系统故障——其中,一个被反复提及的关键词是:AI 辅助代码。 一周 4 次严重事故,亚马逊内部紧急复盘 事情的起点,是最近一段时间亚马逊系统稳定性明显下降。 负责亚马逊网站技术架构的高级副总裁 Dave Treadwell 在一封内部邮件中坦言:“各位,正如大家可能已经知道的,最近网站及相关基础设施的可用性确实不太理想。” 为此,公司决定把原本每周例行举行的技术会议

By Ne0inhk