【十二】【动态规划】446. 等差数列划分 II - 子序列、647. 回文子串、5. 最长回文子串,三道题目深度解析

动态规划

动态规划就像是解决问题的一种策略,它可以帮助我们更高效地找到问题的解决方案。这个策略的核心思想就是将问题分解为一系列的小问题,并将每个小问题的解保存起来。这样,当我们需要解决原始问题的时候,我们就可以直接利用已经计算好的小问题的解,而不需要重复计算。

动态规划与数学归纳法思想上十分相似。

数学归纳法:

  1. 基础步骤(base case):首先证明命题在最小的基础情况下成立。通常这是一个较简单的情况,可以直接验证命题是否成立。

  2. 归纳步骤(inductive step):假设命题在某个情况下成立,然后证明在下一个情况下也成立。这个证明可以通过推理推断出结论或使用一些已知的规律来得到。

通过反复迭代归纳步骤,我们可以推导出命题在所有情况下成立的结论。

动态规划:

  1. 状态表示:

  2. 状态转移方程:

  3. 初始化:

  4. 填表顺序:

  5. 返回值:

数学归纳法的基础步骤相当于动态规划中初始化步骤。

数学归纳法的归纳步骤相当于动态规划中推导状态转移方程。

动态规划的思想和数学归纳法思想类似。

在动态规划中,首先得到状态在最小的基础情况下的值,然后通过状态转移方程,得到下一个状态的值,反复迭代,最终得到我们期望的状态下的值。

接下来我们通过三道例题,深入理解动态规划思想,以及实现动态规划的具体步骤。

(注意,本文的做题思路可能适用性没有这么强,但核心思想就是寻找递推关系,推导出我们想要的状态,不用太纠结思路是不是可以运用到别的动态规划题目上,适用性不是很强。动态表示的推导部分,可能不是很适用。)

446. 等差数列划分 II - 子序列 - 力扣(LeetCode)

题目解析

状态表示

状态表示一般是由经验+题目要求得到的,

经验一般是指,以某个位置为结尾,或者以某个位置为开始。

我们希望把等差子序列所有情况进行划分和表示,使得表示出来的情况不会重复和遗漏,并且划分情况的方式是最细致的划分。

如果我们想要确定子序列,很简单,只需要以最后一个元素为划分即可。

例如,我们有一个数组,我们要求把子序列所有情况进行划分和表示,那么我们就可以(以最后一个元素为结尾)对所有情况进行划分。对于每一种子序列情况,最后一个元素的可能值是nums[0]、nums[1].....nums[n-1]。这样我们就可以把每一种情况进行划分,且保证不重复和不遗漏。

我们是不是也可以用这种方法划分等差子序列,实际上是可以的,对于每一种情况,等差子序列,它的最后一个元素值的可能性分别是nums[0]、nums[1].....nums[n-1]。

所以我们对于这种划分情况,可以定义dp[i]表示以i位置为结尾的等差子序列的个数。

接下来我们尝试分析(以nums[i]为结尾的等差子序列)这种情况下等差子序列的个数是多少。

如果我们要推导这种情况下的等差子序列个数,那我们必须先确定倒数第二个元素的下标,倒数第二个元素的下标的可能性是0、1、2、.....、i-1。假设j是(0~i-1)的某个数,令c=nums[i],b=nums[j],则a=2*b-c,表示倒数第二个元素下标为j时,倒数第三个元素值为a,如果a存在与nums数组中,说明倒数第二个元素下标为j时,这种情况等差子序列存在,计数加一,然后考虑下一种情况。

我们分析得到,我们划分情况的方式不是最细致的,因为每种情况里面竟然还需要继续划分情况,所以我们可以修正状态表示。

定义dp[i][j]表示以(i,j)位置为结尾的等差子序列的个数。

接下来我们尝试分析(以(i,j)为结尾的等差子序列)这种情况下等差子序列的个数是多少。

令c=nums[j],b=nums[i],则a=2*b-c,表示倒数第三个元素值为a,如果a存在与nums数组中,说明这种情况等差子序列存在,计数加一,然后考虑下一种情况。

根据我们的分析,这种划分方式是最细致的,所以我们可以继续分析,尝试推导状态转移方程。

故状态表示为,

定义dp[j][i]表示以(j,i)位置为结尾的等差子序列的个数。

状态转移方程

dp[j][i]表示以(j,i)位置为结尾的等差子序列的个数。

我们针对于(以i位置元素为结尾的等差子序列,以及i位置元素)进行分析,想一想dp[i]能不能由其他状态推导得出。

  1. 如果只考虑(i,j)位置上两个元素, 这种情况下等差子序列个数为0,因为等差子序列最少要有三个元素。 故dp[i][j]=0。

  2. 如果不止考虑(i,j)位置上两个元素, 令c=nums[j],b=nums[i],则a=2*b-c,假设a的下标为k。

    1. 如果k<i, 很容易可以得到dp[i][j]=dp[k][i]+1, 在以(k,i)位置为结尾的等差子序列后面都加上j位置的元素,种类数是dp[k][i],但是这样还有一种情况没有考虑到,那就是(k,i,j)三个元素组成的等差子序列,所以我们需要多加1。

    2. 如果k>i, 这种情况不符合题意,所以此时dp[i][j]=0

将上述情况进行合并和简化,

  1. 如果第二种情况中,有一个k<i,则dp[i][j]会被赋值。

  2. 如果第二种情况中,没有一个k<i,则dp[i][j]不会被赋值,此时dp[i][j]=0。

所以我们可以把dp[i][j]=0,这种情况放到初始化中处理,也就是把所有位置状态初始化为0,这样就只需要考虑第二种情况中k<i的情况。

我们发现,在状态转移方程中,我们需要确定 a 元素的下标。因此我们可以在dp之前,将

所有元素+下标数组绑定在一起,放到哈希表中。这里为何要保存下标数组,是因为我们要统计个

数,所有的下标都需要统计。即,

 
        unordered_map<long long, vector<int>> hash;for (int i = 0; i < n; i++)hash[nums[i]].push_back(i);

故状态转移方程为

 
 for (int j = 2; j < n; j++) // 固定倒数第一个数{for (int i = 1; i < j; i++) // 枚举倒数第二个数{long long a = (long long)nums[i] * 2 - nums[j]; // 处理数据溢出if (hash.count(a))for (auto k : hash[a])if (k < i)dp[i][j] += dp[k][i] + 1;elsebreak;}}

初始化

根据状态转移方程,我们知道想要推导出(i,j)位置的状态需要用到(k,i)位置的状态,k是(0~i-1)之间的某个值。所以我们初始化最基础的最小的解,推导第二个状态((1,2)位置的状态)时,需要初始化(0~1-1)(i)即dp[0][1]=0。

再结合在状态转移方程中的分析,我们需要将所有位置的状态初始化为0。

故初始化为,

 
        vector<vector<int>> dp(n, vector<int>(n)); // 创建 dp 表

默认初始化为0。

填表顺序

根据状态转移方程,我们知道想要推导出(i,j)位置的状态需要用到(k,i)位置的状态,k是(0~i-1)之间的某个值。

  1. 如果固定i填写j, i的变化一定是从小到大,因为要用到(k,i)位置的值,横坐标k一定小于i,所以i变化需要从小到大,那么j的变化可以从小到大,也可以从大到小。因为固定了i的变化,填写(i,j)时,(k,)的状态值都已经填写好了。

  2. 如果固定j填写i, j的变化一定是从小到大,因为要用到(k,i)位置的值,i一定小于j,纵坐标i一定小于j,所以j变化需要从小到大,那么i的变化可以从小到大,也可以从大到小。因为固定了j的变化,填写(i,j)时,(,i)的状态值都已经填写好了。

返回值

dp[j][i]表示以(j,i)位置为结尾的等差子序列的个数。

结合题目意思,我们需要返回所有等差子序列的个数,所以我们需要遍历dp表,统计所有等差子序列的个数,然后返回。

代码实现

 
class Solution {
public:int numberOfArithmeticSlices(vector<int>& nums) {int n = nums.size();// 优化unordered_map<long long, vector<int>> hash;for (int i = 0; i < n; i++)hash[nums[i]].push_back(i);vector<vector<int>> dp(n, vector<int>(n)); // 创建 dp 表int sum = 0;for (int j = 2; j < n; j++) // 固定倒数第一个数{for (int i = 1; i < j; i++) // 枚举倒数第二个数{long long a = (long long)nums[i] * 2 - nums[j]; // 处理数据溢出if (hash.count(a))for (auto k : hash[a])if (k < i)dp[i][j] += dp[k][i] + 1;elsebreak;sum += dp[i][j];}}return sum;}
};

647. 回文子串 - 力扣(LeetCode)

题目解析

状态表示

状态表示一般是由经验+题目要求得到的,

经验一般是指,以某个位置为结尾,或者以某个位置为开始。

我们希望把回文子数组所有情况进行划分和表示,使得表示出来的情况不会重复和遗漏,并且划分情况的方式是最细致的划分。

我们很容易可以得到这样的划分情况,以nums[i]位置结尾的回文子数组。

所有的回文子数组,都有最后一个元素,这些元素的可能性是nums[0],nums[1].....nums[n-1]。

所以我们可以这样划分,使得表示出来的情况不会重复和遗漏。

所以我们对于这种划分情况,可以定义dp[i]表示以i位置为结尾的回文子数组的个数。

接下来我们尝试分析(以nums[i]为结尾的回文子数组)这种情况下回文子数组的个数是多少。

如果我们要推导这种情况下,回文子数组的个数,首先我们需要确定以nums[i]结尾的回文子数组开头在哪里,假设j是(0~i-1)之间的某个数,那么这种情况下,回文子数组开头元素的下标可能是0、1、2....i-1。针对开头为j的情况,判断j和i位置的元素是不是相同的,如果是相同的,然后判断j+1和i-1位置元素是不是相同的,以此类推,如果都是相同的这种情况就符合回文子数组,计数加一,然后分析下一种情况。

我们分析得到,我们划分情况的方式不是最细致的,因为每种情况里面竟然还需要继续划分情况,所以我们可以修正状态表示。

定义dp[i][j]表示以i位置为开头,j位置为结尾的回文子数组的个数。

接下来我们分析(以i位置为开头,j位置为结尾的回文子数组)这种情况下回文子数组的个数是多少。

判断j和i位置的元素是不是相同的,如果是相同的,然后判断j+1和i-1位置元素是不是相同的,以此类推,如果都是相同的这种情况就符合回文子数组,计数加一,然后分析下一种情况。

根据我们的分析,这种划分方式是最细致的,所以我们可以继续分析,尝试推导状态转移方程。

故状态表示为,

定义dp[i][j]表示以i位置为开头,j位置为结尾的回文子数组的个数。

状态转移方程

dp[i][j]表示以i位置为开头,j位置为结尾的回文子数组的个数。

我们想一想能不能由其他位置的状态推导出dp[i][j]。

  1. 如果nums[i]==nums[j],

    1. 如果(i~j)只有一个元素,即i==j, 此时回文子数组的个数为1,dp[i][j]=1。

    2. 如果(i~j)只有两个元素,即i+1==j, 此时回文子数组的个数为1,dp[i][j]=1。

    3. 如果(i~j)有多个元素,(大于2个元素),即i+1<j, 此时dp[i][j]=dp[i+1][j-1]。

  2. 如果nums[i]!=nums[j], 此时不可能构成回文子数组,所以回文子数组个数为0,即dp[i][j]=0。

我们可以把(nums[i]!=nums[j])这种情况放到初始化中进行处理,也就是把所有状态初始化为0。

故状态转移方程为,

 
        for (int i = n - 1; i >= 0; i--) {for (int j = i; j < n; j++) {if (s[i] == s[j])dp[i][j] = i + 1 < j ? dp[i + 1][j - 1] : 1;}}

初始化

根据状态转移方程,我们在推导dp[i][j]时,需要用到dp[i+1][j-1]。

因为只有i+1<j时才会用到dp[i+1][j-1],i和j都是介于(0~n-1)之间,所以i+1不可越界,同时j-1>i,也不会越界,所以我们只需要初始化所有元素为0,即在状态转移方程中分析的初始化即可。

填表顺序

根据状态转移方程,我们在推导dp[i][j]时,需要用到dp[i+1][j-1]。

  1. 如果固定i填写j,i应该从大到小变化,当i从大到小变化,填写dp[i][j]时,dp[i+1][]一定已经填写完毕,所以j的变化可以从大到小也可以从小到大。

  2. 如果固定j填写i,j应该从小到大变化,当j从大到小变化,填写dp[i][j]时,dp[][j-1]一定已经填写完毕,所以i的变化可以从大到小也可以从小到大。

返回值

dp[i][j]表示以i位置为开头,j位置为结尾的回文子数组的个数。

结合题目要求,我们需要统计所有回文子数组的个数,所以我们需要遍历dp表,统计个数然后返回。

代码实现

 
class Solution {
public:int countSubstrings(string s) {int n = s.size();vector<vector<int>> dp(n, vector<int>(n));int ret = 0;for (int i = n - 1; i >= 0; i--) {for (int j = i; j < n; j++) {if (s[i] == s[j])dp[i][j] = i + 1 < j ? dp[i + 1][j - 1] : 1;ret += dp[i][j];}}return ret;}
};

5. 最长回文子串 - 力扣(LeetCode)

题目解析

状态表示

我们要返回最长回文子数组,那么我们就必须知道最长回文子数组的开头下标和结尾的下标。

所以我们可以定义状态表示为dp[i][j]表示以i位置为开头,j位置为结尾的回文子数组最长的长度。

接着我们分析这种划分方式下,dp[i][j]数据的推导。

判断nums[i]是不是等于nums[j],如果相等,就判断nums[i+1],nums[j-1] 是不是相等,以此类推,如果全部都相等,说明是回文子数组,故dp[i][j]=j-i+1。

根据我们的分析,这种划分方式是最细致的,所以我们可以继续分析,尝试推导状态转移方程。

故,状态表示为,

dp[i][j]表示以i位置为开头,j位置为结尾的回文子数组最长的长度

状态转移方程

dp[i][j]表示以i位置为开头,j位置为结尾的回文子数组的长度。

我们想一想能不能由其他位置的状态推导出dp[i][j]。

  1. 如果nums[i]==nums[j]

    1. 如果(i~j)之间只有一个元素,即i==j, 此时回文子数组长度为1,即dp[i][j]=1。

    2. 如果(i~j)之间只有两个元素,即i+1==j, 此时回文子数组长度为2,即dp[i][j]=2。

    3. 如果(i~j)之间元素大于2,即i+1<j, 此时回文子数组长度dp[i][j]=dp[i+1][j-1]==0?0:dp[i+1][j-1]+2;

  2. 如果nums[i]!=nums[j] 此时dp[i][j]=0。

我们可以把(nums[i]!=nums[j])这种情况放到初始化中进行处理,也就是把所有状态初始化为0。

故状态转移方程为,

 
for (int i = n - 1; i >= 0; i--) {for (int j = i; j < n; j++) {if (s[i] == s[j]) {if (i + 1 < j)dp[i][j] =dp[i + 1][j - 1] == 0 ? 0 : dp[i + 1][j - 1] + 2;else if (i + 1 == j)dp[i][j] = 2;else if (i == j)dp[i][j] = 1;}}}

初始化

根据状态转移方程,我们知道想要推导出(i,j)位置的状态可能需要用到(i+1,j-1)位置的状态。

当i+1<j时,需要用到(i+1,j-1)位置的状态。而i和j介于(0~n-1)之间,又i+1<j,所以i+1一定不会越界,j>i+1>1,j-1>0,故j-1也不会越界,所以我们只需要初始化所有元素为0,即在状态转移方程中分析的初始化即可。

填表顺序

根据状态转移方程,我们知道想要推导出(i,j)位置的状态可能需要用到(i+1,j-1)位置的状态。

  1. 如果固定i填写j,i应该从大到小变化,当i从大到小变化,填写dp[i][j]时,dp[i+1][]一定已经填写完毕,所以j的变化可以从大到小也可以从小到大。

  2. 如果固定j填写i,j应该从小到大变化,当j从大到小变化,填写dp[i][j]时,dp[][j-1]一定已经填写完毕,所以i的变化可以从大到小也可以从小到大。

返回值

dp[i][j]表示以i位置为开头,j位置为结尾的回文子数组最长的长度

结合题目意思,我们需要得到回文子数组最长的长度,所以我们需要遍历dp表,返回最长的长度。

代码实现

 
class Solution {
public:string longestPalindrome(string s) {int n = s.size();vector<vector<int>> dp(n, vector<int>(n));int len = 1, begin = 0;for (int i = n - 1; i >= 0; i--) {for (int j = i; j < n; j++) {if (s[i] == s[j]) {if (i + 1 < j)dp[i][j] =dp[i + 1][j - 1] == 0 ? 0 : dp[i + 1][j - 1] + 2;else if (i + 1 == j)dp[i][j] = 2;else if (i == j)dp[i][j] = 1;}if (dp[i][j] > len) {len = dp[i][j];begin = i;}}}return s.substr(begin, len);}
};

结尾

今天我们学习了动态规划的思想,动态规划思想和数学归纳法思想有一些类似,动态规划在模拟数学归纳法的过程,已知一个最简单的基础解,通过得到前项与后项的推导关系,由这个最简单的基础解,我们可以一步一步推导出我们希望得到的那个解,把我们得到的解依次存放在dp数组中,dp数组中对应的状态,就像是数列里面的每一项。最后感谢您阅读我的文章,对于动态规划系列,我会一直更新,如果您觉得内容有帮助,可以点赞加关注,以快速阅读最新文章。

最后,感谢您阅读我的文章,希望这些内容能够对您有所启发和帮助。如果您有任何问题或想要分享您的观点,请随时在评论区留言。

同时,不要忘记订阅我的博客以获取更多有趣的内容。在未来的文章中,我将继续探讨这个话题的不同方面,为您呈现更多深度和见解。

谢谢您的支持,期待与您在下一篇文章中再次相遇!

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基础算法(7):离散化和区间合并

1.离散化 离散化是一个很好用的技巧&#xff0c;可以很大程度上降低时间和空间复杂度 离散化是把无限空间中有限的个体映射到有限的空间中去&#xff0c;减少空间的使用。 比如&#xff1a;我们有一组很大的数据 &#xff1a;1 3 277438 2884821 428 239823128 如果我们…

AI时代系列丛书(由北京大学出版社出版)

前言 在AI时代&#xff0c;程序员面临着新的机遇和挑战。为了适应这个快速发展的时代&#xff0c;掌握新技能并采取相应的应对策略是至关重要的。 对于办公人员或程序员来说&#xff0c;利用AI可以提高工作效率。例如&#xff0c;使用AI助手可以帮助自动化日常的重复性工作&a…

Feign远程调用丢失请求头问题处理

在浏览器发送一个Q请求,请求中原包含请求头headers信息,controller某个A方法接收到Q请求并调用某个B方法。B方法中有Feign远程调用,在执行Feign远程调用其他服务时,会丢失掉原来请求中的请求头信息。 Feign远程调用底层是发送Http请求,而发送请求时会经过Feign的拦截器。…

使用Pycharm给html文件添加浏览器

1、选择菜单栏的File---->选择setting设置 2、选择Tools(工具)---> Web Browser(web 浏览器) 勾选 自己想要添加的浏览器前面 的勾选框即可 注意点击ok进行保存

Python数据科学应用从入门到精通--Python读取、合并SPSS数据文件

在很多情况下&#xff0c;我们需要调用SPSS软件产生的数据&#xff0c;下面通过示例来进行讲解。首先需要将本书提供的数据文件存储在安装spyder-py3的默认路径位置&#xff08;C:/Users/Administrator/.spyder-py3/&#xff0c;注意具体的安装路径可能与此不同&#xff09;&am…

【MATLAB】【数字信号处理】线性卷积和抽样定理

已知有限长序列&#xff1a;xk1,2,1,1,0,-3, hk[1,-1,1] , 计算离散卷积和ykxk*h(k) 。 程序如下&#xff1a; function [t,x] My_conv(x1,x2,t1,t2,dt) %文件名与函数名对应 %自写的卷积函数 x conv(x1,x2)*dt; t0 t1(1) t2(1); L length(x1) length(x2)-2; t t0:dt…

吉良吉影狂喜!HandRefiner:一种可以有效修正畸形手部图像的技术

这种方法首先使用深度学习模型从图片中识别出手部区域。然后它将手部 crops 出来&#xff0c;并利用一个生成对抗网络试图生成一个更加符合人体工学标准的手部形状。 GitHub&#xff1a;https://github.com/wenquanlu/HandRefiner/ 论文&#xff1a;https://arxiv.org/abs/231…

交通 | 司乘匹配:基于增量成本计算的优化算法

编者按&#xff1a; 司乘匹配是打车服务中一项至关重要的任务&#xff0c;如果这一步做得不够优化&#xff0c;可能导致乘客需要更长的时间才能到达目的地&#xff0c;同时司机的收入也会因此减少。由于司乘匹配是一个持续进行的过程&#xff0c;每一时刻都在不断涌入新的打车…

Android开发中使用Coil

Coil - Android开源图像加载库 Coil是一个开源的图像加载库&#xff0c;用于在Android中显示网络或本地图像资源。 为什么我们使用Coil&#xff1f; 快速&#xff1a;Coil进行了许多优化&#xff0c;包括内存和磁盘缓存、内存中的图像降采样、自动暂停/取消请求等。轻量级&a…

C++每日一练(10):线性查找

题目描述 输入n个数和一个需要查找的目标数&#xff0c;进行线性查找。 输入 第一行输入n&#xff08;1<n<1000&#xff09;&#xff0c; 第二行输入n个整数&#xff0c; 第三行输入要查找的目标数t。 输出 输出查找到的目标数的排序号&#xff0c;若查不到则输出no。 输…

Linux 安装Jupyter notebook 并开启远程访问

文章目录 安装Python安装pip安装Jupyter启动Jupyter Notebook1. 生成配置文件2. 创建密码3. 修改jupyter notebook的配置文件4. 启动jupyter notebook5. 远程访问jupyter notebook 安装Python 确保你的系统上已经安装了Python。大多数Linux发行版都预装了Python。你可以在终端…

基于Java SSM框架实现房屋租赁合同系统项目【项目源码+论文说明】计算机毕业设计

基于java的SSM框架实现房屋租赁合同系统演示 摘要 在网络高速发展的时代&#xff0c;众多的软件被开发出来&#xff0c;给用户带来了很大的选择余地&#xff0c;而且人们越来越追求更个性的需求。在这种时代背景下&#xff0c;人们对房屋租赁系统越来越重视&#xff0c;更好的…