动态规划算法题1
动态规划做题步骤
- 确定状态表示:dp表中某一个位置中的值所表示的含义就是状态表示
- 根据状态表示推导状态转移方程:dp[i]等于什么状态转移方程就是什么,用之前或者之后的状态,推导出dp[i]的值
- 初始化(防止越界):根据状态转移方程填表,保证填表时候不越界
- 确定填表顺序:填写当前状态的时候,所需要的状态必须都已计算过
- 返回结果:结合题目要求+状态表示
三步问题
面试题 08.01. 三步问题 - 力扣(LeetCode)
状态表示为:以i位置结尾,得到该题状态表示为dp[i]走到第i阶楼梯的走法总数
状态转移方程:状态转移方程以位置的状态最近一步来划分问题, 当对于第 i 阶楼梯,可以从i-3阶走3步上来,或者从 i-2阶走2步上来, 或者从 i -1阶走1步上来。 因此状态转移方程为
dp[i] = dp[i-3] + dp[i-2] + dp[i-1]。
步骤:
- 判断边界情况:由于状态转移方程需要前三个元素的状态表示,所以当n<=3时直接返回推出来的结果接口
- 创建dp表
- 初始化dp表:初始化前三个元素值(防止越界)
- 根据状态转移方程进行填表:dp[i] = dp[i-3] + dp[i-2] + dp[i-1]。
状态转移方程描述:
dp[i-3] 表示所有走到 i-3 阶的走法,然后一次性跨 3 步到 i 阶。
dp[i-2] 表示所有走到 i-2 阶的走法,然后一次性跨 2 步到 i 阶。
dp[i-1] 表示所有走到 i-1 阶的走法,然后一次性跨 1 步到 i 阶。
不需要考虑 "从 i-3 阶分多步(如 1+1+1 或 1+2 等)走到 i 阶"
因为 dp[i-3] 已经包含了所有可能的走法(包括 1+1+1、1+2、2+1、3 等),而最后一步必须是 +3(即一次性跨 3 步)。如果从 i-3 阶分多步走到 i 阶(比如 1+1+1),那么这些情况已经被 dp[i-1] 和 dp[i-2] 覆盖了:i-3 → i-2 → i-1 → i(即 1+1+1)已经被 dp[i-1] 包含(因为 dp[i-1] 包含了所有走到 i-1 的走法,最后一步是 +1)。
i-3 → i-1 → i(即 2+1)已经被 dp[i-1] 包含(因为 dp[i-1] 包含了所有走到 i-1 的走法,最后一步是 +1)。
i-3 → i(即 3)才是 dp[i-3] 贡献的部分。
dp[i-3] 贡献的是最后一步跨 3 步的走法。
dp[i-2] 贡献的是最后一步跨 2 步的走法。
dp[i-1] 贡献的是最后一步跨 1 步的走法。
这样计算不会重复也不会遗漏,因为 dp[i-1]、dp[i-2]、dp[i-3] 已经包含了所有可能的走法组合,而最后一步的步数决定了它们属于哪个部分。
class Solution {public int waysToStep(int n) {//判断边界情况if(n == 1 || n == 2) return n;if(n == 3) return 4;// 1. 创建动态规划表dp,dp[i]表示走到第i阶楼梯的走法总数int[] dp = new int[n+1];// 2. 初始化dp表的前三个值(基础情况)dp[1] = 1; dp[2] = 2; dp[3] = 4;// 3.根据动态转移方程进行填表// 由于数字可能很大,每次相加后都取模1000000007防止溢出for(int i = 4; i <=n; i++){dp[i] = ((dp[i - 3] + dp[i - 2])%1000000007 + dp[i - 1])%1000000007;}// 4. 返回结果:第n阶楼梯的走法总数return dp[n];}
}
使用最小花费爬楼梯
746. 使用最小花费爬楼梯 - 力扣(LeetCode)
解法一:
1. 状态表示:以i位置为结尾,dp[i]表示到达 i 位置的时候最小花费。
2. 状态转移方程推导:
在计算 dp[i] 时,存在两种可行的到达方式:
先到达第 i - 1 个位置,接着支付 cost[i - 1] 的费用,然后向前走一步到达第 i 个位置。这种情况下,到达第 i 个位置的最小花费为 dp[i - 1] + cost[i - 1]。
先到达第 i - 2 个位置,接着支付 cost[i - 2] 的费用,然后向前走两步到达第 i 个位置。这种情况下,到达第 i 个位置的最小花费为 dp[i - 2] + cost[i - 2]。
由于 dp[i] 代表的是到达第 i 个位置的最小花费,所以我们需要取上述两种情况中的最小值,即 dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2])。
3. 初始化操作:
在进行状态数组 dp 的填充时,当计算 dp[0] 和 dp[1] 时,由于需要参考前两个位置的状态,会出现越界问题。因此,需要对这两个元素进行初始化。
4. 填表顺序:
状态数组 dp 的填充顺序是从左到右,也就是依次计算 dp[0]、dp[1]、dp[2] 直至 dp[n]。
5. 返回值:最终,我们直接返回 dp[n],它就是到达第 n 个位置时的最小花费。
class Solution {public int minCostClimbingStairs(int[] cost) {int len = cost.length; // 获取楼梯的总阶数// 1. 创建动态规划表dp,dp[i]表示到达第i阶楼梯的最小花费// 数组长度为len+1,因为我们要计算到达顶层(即第len阶)的最小花费int[] dp = new int[len+1];// 2. 初始化dp表 起点花费为0,数组默认为0 省略初始化// 3. 根据状态转移方程填表 从第2阶开始计算,直到第len阶(顶层)for(int i = 2; i <= len; i++){// 计算从第i-1阶跨1步到第i阶的总花费int tmp1 = dp[i - 1] + cost[i - 1];// 计算从第i-2阶跨2步到第i阶的总花费int tmp2 = dp[i - 2] + cost[i - 2];// 取两种方式中的较小值作为到达第i阶的最小花费dp[i] = Math.min(tmp1, tmp2);}// 4. 返回结果 dp[len]表示到达顶层(第len阶)的最小花费return dp[len];}
}
解法二:解法一采用以 i 位置为结尾的思想结合题目条件得到状态表示,而解法二则是基于以 i 位置为起点的思想结合题目来构建状态表示。
1.状态表示定义:定义 dp[i] 为从第 i 个位置出发,到达楼顶所需的最小花费。
2.状态转移方程推导:
在计算 dp[i] 时,存在两种可行的行进方式:
支付 cost[i] 的费用,然后向前走一步到达第 i + 1 个位置,之后从该位置出发到达楼顶。这种情况下,从第 i 个位置出发到达楼顶的最小花费为 cost[i] + dp[i + 1]。
支付 cost[i] 的费用,然后向前走两步到达第 i + 2 个位置,之后从该位置出发到达楼顶。这种情况下,从第 i 个位置出发到达楼顶的最小花费为 cost[i] + dp[i + 2]。
由于 dp[i] 代表的是从第 i 个位置出发到达楼顶的最小花费,所以我们需要取上述两种情况中的最小值,即 dp[i] = min(cost[i] + dp[i + 1], cost[i] + dp[i + 2])。
3.初始化操作:
因为在状态转移过程中需要用到后续两个位置的状态值,所以要对最后两个下标对应的元素进行初始化。到达楼顶不需要额外花费,其最小花费为 0,所以 dp 数组的大小与 cost 数组相同即可。最后两个位置的最小花费就是当前位置的费用。
4.填表顺序:
状态数组 dp 的填充顺序是从右到左,也就是依次计算 dp[n - 1]、dp[n - 2] 直至 dp[0]。
5.返回值:
由于 dp[i] 表示从第 i 个位置出发到达楼顶的最小花费,而整个行程可以从第 0 个位置或者第 1 个位置开始,所以最终应返回 dp[0] 和 dp[1] 中的最小值。
class Solution {public int minCostClimbingStairs(int[] cost) {int len = cost.length; // 获取楼梯的总台阶数// 1. 创建动态规划表dp,dp[i]表示从第i阶台阶出发到达楼顶的最小花费// 这里采用从后向前填充的方式,因此数组长度与cost相同int[] dp = new int[len];// 2. 初始化dp表// 最后一级台阶(len-1)的最小花费就是cost[len-1],因为必须支付该台阶费用才能到达楼顶// 倒数第二级台阶(len-2)可以选择直接跨两步到楼顶,或先到len-1再跨一步// 因此取两者中的较小值加上当前台阶的花费dp[len - 1] = cost[len - 1]; dp[len - 2] = cost[len - 2];// 3. 根据状态转移方程逆向填表(从倒数第三级台阶开始向前计算)// 状态转移方程:dp[i] = min(dp[i+1], dp[i+2]) + cost[i]// 表示从第i阶出发的最小花费等于下一步(i+1或i+2)的最小花费加上当前台阶的花费for(int i = len - 3; i >= 0; i--){dp[i] = Math.min(dp[i+1], dp[i+2]) + cost[i];}// 4. 返回结果// 根据题意可以从第0级或第1级台阶开始,因此返回两者中的较小值return dp[0] < dp[1] ? dp[0] : dp[1];}
}
解码方式
91. 解码方法 - 力扣(LeetCode)
1.状态表示:dp[i]表示以字符串s中第i位置结尾时,解码方法的总数。
2.状态转移方程推导:对字符串第i个位置进行解码,存在两种可行的情况:
第一种:
当s[i]能够单独进行解码时,也就意味着s[i]代表一个有效的字符编码。此时,以i位置结尾的解码方法总数,与以i - 1位置结尾的解码方法总数是相同的,即dp[i]可以从dp[i - 1]推导而来。如果s[i]不能单独解码,例如s[i]为字符0且它前面没有可与之组合的字符形成合法编码,那么所有基于此位置的局部解码尝试都是无效的。由于我们关注的是全局的解码数,所以这种无效情况在后续推导中需要避免。
第二种:
当s[i - 1]和s[i]组合起来进行解码时,它们所构成的数字s[i - 1] * 10 + s[i]必须在10到26这个闭区间内。这是因为在合法的解码规则中,不能出现前导0。如果组合数字在0到9之间,就说明存在前导0,不符合解码要求。当s[i - 1]和s[i]成功解码时,以i位置结尾的解码方法总数,就等于以i - 2位置结尾的解码方法总数,即dp[i]可以从dp[i - 2]推导而来。
综合以上两种情况,当两种解码方式都可以时,dp[i]等于dp[i-2]和dp[i-1]之和。
注意:只有在解码成功的前提下,才进行相应的累加操作
3.初始化:为了保证填表时不越界,所以需要初始化dp[i-1]和dp[i-2]所涉及的位置,即下标0和下标1处的元素。
初始化dp[0]
- 根据状态表示,dp[0]表示以0位置结尾的解码方式总数。当s[0]不等于0时,s[0]可以单独构成一种合法的解码方式,因此dp[0]初始化为1。若s[0]等于0,则表示解码失败.
初始化dp[1],存在以下三种情况:
- 当s[1]等于0时,说明解码失败.
- 当s[1]能够单独解码,且s[0]与s[1]不能组合解码时,dp[1]为1。例如,当s[0]和s[1]都在1到9之间,且s[0] * 10 + s[1]不在10到26的范围内时,只有一种单独解码的方式。
- 当s[1]既能单独解码,又能与s[0]组合解码时,dp[1]为2。
4.填表顺序:从左往右
5.返回结果:根据状态表示,dp[n-1]表示以字符串最后一个位置结束的解码方式总数,因此直接返回dp[n-1]即可。
class Solution {public int numDecodings(String s) {char[] arr = s.toCharArray();int n = arr.length;// 1. 创建dp表,dp[i]表示以i位置结尾时,解码方法的总数int[] dp = new int[n];// 2. 初始化dp表 第一个字符不为'0',有1种解码方式if(arr[0] != '0') dp[0] = 1;// 处理字符串长度为1的边界情况 if(n == 1) return dp[0]; // 初始化第二个位置 当前一个字符和当前字符都不为0则表示有一种解码方式if(arr[1] != '0' && arr[0]!='0') dp[1] += 1;//前两个字符组合解码(必须在10-26之间)则又有一种解码方式int t = (arr[0] - '0') * 10 + arr[1] - '0';if( t >= 10 && t <= 26) dp[1] += 1;// 3.根据状态转移方程进行填表for(int i = 2; i < n; i++){// 情况1:当前字符单独解码(必须非'0')if(arr[i] != '0') dp[i] += dp[i-1];// 情况2:当前字符和前一个字符组合解码int tt = (arr[i-1] - '0') * 10 + arr[i] - '0';if( tt >= 10 && tt <= 26) dp[i] += dp[i - 2];}// 4.返回结果return dp[n-1];}
}
优化初始化:
上述解码方式计数问题中,可以通过添加虚拟节点的方式,对初始化过程进行优化。原本在初始化dp[1]时,其判断逻辑体中的条件一致,造成了代码逻辑的冗余。
添加虚拟节点的目的是为了统一初始化逻辑,减少重复判断,而且后续填表计算依赖于前面节点的值,所有虚拟节点的值设置必须确保整个填表过程的正确性。
初始化操作调整:只需要对虚拟节点和原数组中下标为0位置对应的dp节点进行初始化。原本对下标为1位置节点的初始化操作被省略,该位置的值将在后续循环中,依据统一的状态转移方程进行计算。
下标映射调整:因为添加了头部虚拟节点,在访问原字符串来计算dp值时,所有原字符串的下标都需要减1。这确保了dp数组与原字符串在位置上的正确对应关系。
结果返回:由于dp数组增加了一个虚拟节点,最终的结果只需返回dp[n]即可。其中n为原字符串长度对应的dp数组下标,这样便能得到原字符串的解码方法总数。
class Solution {public int numDecodings(String s) {char[] arr = s.toCharArray();int n = arr.length;// 1. 创建dp表,dp[i]表示前i+1个字符的解码方式数int[] dp = new int[n + 1];dp[0] = 1;if(arr[0] != '0') dp[1] = 1;// 3.根据状态转移方程进行填表for(int i = 2; i <= n; i++){// 情况1:当前字符单独解码(必须非'0')if(arr[i - 1] != '0') dp[i] += dp[i-1];// 情况2:当前字符和前一个字符组合解码int tt = (arr[i-2] - '0') * 10 + arr[i - 1] - '0';if( tt >= 10 && tt <= 26) dp[i] += dp[i - 2];}// 4.返回结果return dp[n];}
}
不同路径
62. 不同路径 - 力扣(LeetCode)
在解决路径计算问题时,引入虚拟节点,这一举措极大简化了边界情况的处理。最初, dp[i][j] 用于表示抵达坐标 (m, n) 的路径总数。但随着虚拟节点的引入,下标映射需进行相应调整。
1. 状态表示:将 dp[i - 1][j - 1] 定义为抵达坐标 (m, n) 的总路径数,即以 i 位置为路径终点。
2. 状态转移方程推导:考虑到机器人每次仅能向下或向右移动一格, dp[i][j] 所代表的总路径数,应为其上方节点和左方节点的路径数之和。而根据题目要求机器人路径不能包含任何有障碍物的格子,所以该状态转移方程分成两种情况。
第一种情况:[i,j]位置为障碍物,说明从上或从右都到不了该位置,该位置就为0
第二种情况:没有障碍物,依据状态表示,上方节点的总路径数为 dp[i - 1][j] ,左方节点的总路径数为 dp[i][j - 1] ,因此状态转移方程为 dp[i][j] = dp[i - 1][j] + dp[i][j - 1] 。
3. 初始化操作:在填充状态数组 dp 时,若按照既定的状态转移方程计算 dp[0][1] ,会导致数组越界。所以,在状态数组 dp 的最上方和最左侧各添加一层虚拟节点。由于起始节点需被赋值为1,因此只需将 dp[1][0] 或 dp[0][1] 其中之一初始化为1即可。
4. 填表顺序:在填充状态数组 dp 时,按照从左到右的顺序依次计算每个元素,确保每个节点的路径数能依据已计算的相邻节点得出。
5. 返回值:经过上述步骤的计算, dp[m][n] 即为抵达坐标 (m, n) 的总路径数,直接返回该值。
class Solution {public int uniquePaths(int m, int n) {// 1. 创建 dp 表,由于引入了虚拟节点dp[i][j] 表示到达 (i-1, j-1) 的路径数,// 这里使用 (m+1) × (n+1) 的数组,方便边界处理,m=0或n=0时均为虚拟节点int[][] dp = new int[m+1][n+1];// 2.初始化// 关键初始化:让 dp[1][1] = dp[0][1] + dp[1][0] = 1 + 0 = 1// 这样 dp[1][1] 表示到达 (0,0) 的路径数为 1(起点)dp[0][1] = 1;//根据状态转移方程进行填表for(int i = 1; i <= m; i++){for(int j = 1; j <=n; j++){// 当前格子的路径数 = 从上方来的路径数 + 从左方来的路径数dp[i][j] = dp[i-1][j]; // 上方路径数dp[i][j] += dp[i][j-1];// 左方路径数}}// 4. 返回结果:dp[m][n] 表示到达 (m-1, n-1) 的路径数return dp[m][n];}
}
按摩师
面试题 17.16. 按摩师 - 力扣(LeetCode)
1.状态表示:最初,定义dp[i]为选择到i位置时的最长预约时长。为更细致地解决问题,进一步将dp[i]划分为两个状态数组:
- f[i]:表示选择到i位置时,nums[i]必定被选择的情况下,此时的最长预约时长。
- g[i]:表示选择到i位置时,nums[i]不被选择的情况下,此时的最长预约时长。
2.状态转移方程
计算f[i]:由于f[i]要求必须选择nums[i],那么前一个位置必然不能选择,依据状态表示,前一个位置不选时的最大预约时长由g[i - 1]表示。因此,f[i] = g[i - 1] + nums[i] 。
计算g[i]:g[i]表示不选择nums[i],对于前一个位置i - 1,既可以选择也可以不选择,具体分为以下两种情况:
- 若前一个位置选择,即取f[i - 1]。
- 若前一个位置不选择,即取g[i - 1]。
为获取最长预约时长,g[i] = max(f[i - 1], g[i - 1])。
3.初始化操作:根据状态转移方程,计算过程仅需知道前一个元素的状态,因此只需对起始位置0进行初始化。
- 初始化f[0]:因为f数组表示必选当前元素的情况,所以f[0] = nums[0]。
- 初始化g[0]:由于g数组表示不选当前元素的情况,所以g[0] = 0。
4.填表顺序:在填充状态数组时,按照从左向右的顺序,同步填充f和g两个数组,确保每个位置的状态值能依据已计算的前一个位置得出。
5.返回结果:题目要求计算最长预约时长,只需返回f[n - 1]和g[n - 1]中的较大值,该值即为最终所求的最长预约时长。
class Solution {public int massage(int[] nums) {int n = nums.length;if(n == 0) return 0; // 边界情况:无预约时直接返回0// 1.创建dp表// f[i]:选择第i个预约时的最大总时长// g[i]:不选择第i个预约时的最大总时长int[] f = new int[n];int[] g = new int[n];// 2.初始化f[0] = nums[0]; // 选择第0个预约,时长为nums[0]// 3.根据状态转移方程进行填表for(int i = 1; i < n; i++){// 选择第i个预约:必须不选第i-1个预约,所以加上g[i-1]f[i] = g[i - 1] + nums[i];// 不选择第i个预约:可以选择或不选第i-1个预约,取最大值g[i] = Math.max(g[i - 1], f[i - 1]);}// 4. 返回结果:最终选择或不选最后一个预约的最大值return Math.max(f[n-1], g[n-1]);}
}
最大子数组和
53. 最大子数组和 - 力扣(LeetCode)
1.状态表示:定义dp[i],它表示以位置i结尾的所有子数组中,元素总和的最大值。
2.状态转移方程推导:
情况一:考虑以位置i结尾的子数组中,仅包含nums[i]这一个元素的情形。在这种情况下,dp[i]就等于nums[i]。
情况二:当以位置i结尾的子数组长度大于1时,它可以看作是以位置i - 1结尾的最大子数组和dp[i - 1],再加上当前元素nums[i]。
综合以上两种情况,dp[i]取二者的最大值,即状态转移方程为dp[i] = max(nums[i], dp[i - 1] + nums[i])。该方程确保dp[i]始终是符合定义的最大子数组和。
3.初始化:状态转移方程依赖于dp[i - 1],当i = 0时,dp[-1]并不存在,会导致越界问题。因此,需要对dp[0]进行初始化。由于此时数组中仅有nums[0]一个元素,所以dp[0] = nums[0]。
4.填表顺序:根据状态转移方程,dp[i]的计算仅依赖于dp[i - 1],因此按照从左到右的顺序依次计算dp数组的元素,能够确保在计算每个dp[i]时,所需的dp[i - 1]已经计算完成。
5.返回结果:需要注意,dp[i]仅表示以位置i结尾的最大子数组和,而整个数组的最大子数组和不一定以最后一个位置结尾。因此,返回结果应该是dp数组中的最大值,这个值才是原问题的答案。
class Solution {public int maxSubArray(int[] nums) {int n = nums.length;// 1. 创建一个dp数组,用于保存以每个位置结尾的最大子数组和int[] dp = new int[n];// 2. 初始化dp数组的第一个元素,即以nums[0]结尾的最大子数组和就是nums[0]本身dp[0] = nums[0];int max = dp[0]; ///用于记录全局的最大子数组和 初始化最大值为第一元素// 3. 根据状态转移方程填充dp数组// 状态转移方程:dp[i] = max(nums[i], dp[i - 1] + nums[i])// 表示以nums[i]结尾的最大子数组和,要么是nums[i]本身,要么是之前的最大子数组和加上nums[i]for(int i = 1; i < n; i++){dp[i] = Math.max(nums[i], dp[i - 1] + nums[i]);max = Math.max(max,dp[i]);}// 4. 返回全局的最大子数组和return max;}
}
最长递增子序列
300. 最长递增子序列 - 力扣(LeetCode)
1. 状态表示:dp[i]表示以i位置为结尾的所有子序列中,最长递增子序列的长度。
2. 状态转移方程:基础情况和递推情况。
在基础情况下,每个元素自身构成一个长度为1的子序列,即dp[i]=1
在递推情况下,需要考察前面所有小于当前元素的子序列,从中选取最长的进行扩展。具体来说,对于0到i-1范围内的每个j,当nums[j]<nums[i]时,将nums[i]接在以nums[j]结尾的子序列后面,此时dp[i] = max(dp[i], dp[j]+1)。
3. 初始化:将整个dp数组初始化为1,这样既包含了基础情况,又为后续的递推计算提供了正确的初始值。
4. 填表顺序:从左向右
5. 返回值:最终结果需要返回dp数组中的最大值,因为最长递增子序列可能以数组中任意一个元素结尾。这与许多动态规划问题中直接取最后一个元素作为结果的做法有所不同,需要特别注意。
class Solution {public int lengthOfLIS(int[] nums) {int n = nums.length;// 1.创建dp表dp[i] 表示以nums[i]结尾的最长递增子序列的长度int[] dp = new int[n];// 2.初始化// 因为每个元素自身都可以构成一个长度为1的递增子序列,所以将dp数组初始化为1Arrays.fill(dp,1);// 记录当前找到的最长递增子序列的长度,初始值为1(单个元素的情况)int max = 1;// 3.根据状态转移方程填表// 外层循环遍历数组,计算以每个元素结尾的最长递增子序列的长度for(int i = 1; i < n; i++){// 内层循环用于比较当前元素nums[i]与前面的元素nums[j]for(int j = 0; j < i; j++){if(nums[j] < nums[i]) {//如果nums[j] < nums[i],那么以nums[i]结尾的递增子序列长度可以在以nums[j]结尾的递增子序列长度基础上加1dp[i] = Math.max(dp[j]+1,dp[i]);}}// 更新全局的最长递增子序列的长度max = Math.max(max,dp[i]);}return max;}
}
回文子串
647. 回文子串 - 力扣(LeetCode)
1. 状态表示:dp[i[j]为字符串从下标i到下标j的子串是否为回文子串。
2. 状态转移方程:可以分为两种情况,第一种情况是当s[i]不等于s[j]时,该子串必定不是回文串,直接设置dp[i][j]为false。
第二种情况是当s[i]等于s[j]时,又细分为三种子情况:当i和j重合时,即单个字符必定是回文串;当i和j相邻时,两个相同字符组成的子串也是回文串;当i和j不相邻时,该子串的回文性质取决于其内部子串dp[i+1][j-1]的回文状态。
3. 初始化:初始化方面,由于只需要考虑i小于等于j的情况,且单个字符的情况已经通过状态转移方程处理为true,因此不需要额外的初始化步骤。
4. 填表顺序:填表顺序采用从下往上的方式逐行填写,即i从字符串末尾向前遍历。这种顺序确保了在计算dp[i][j]时,其依赖的子问题dp[i+1][j-1]已经被正确计算出来。
5. 返回值:最终结果是统计dp表中所有为true的个数,这代表了原字符串中所有可能的回文子串数量。需要注意的是,这里的返回值不是某个特定的dp值,而是整个表中满足条件的累计总数。
class Solution {public int countSubstrings(String s) {int n = s.length();// 1. 创建动态规划表dp[i][j],表示s[i...j]是否为回文子串boolean[][] dp = new boolean[n][n];int ret = 0; // 统计回文子串总数// 2. 填表过程:从下往上(i从n-1到0),从左往右(j从i到n-1)for(int i = n - 1; i>=0; i--){for(int j = i; j < n; j++){// 3. 状态转移:// 当首尾字符相等时,若子串长度<=2直接为回文,// 否则取决于内部子串dp[i+1][j-1]是否为回文if(s.charAt(i) == s.charAt(j))dp[i][j] = i + 1 < j ? dp[i+1][j-1] : true;if(dp[i][j]) ret++;}}return ret;}
}
最长公共子序列
1143. 最长公共子序列 - 力扣(LeetCode)
1. 状态表示:设dp[i][j]表示第一个字符串的前i个字符(即[0,i]区间)和第二个字符串的前j个字符(即[0,j]区间)的最长公共子序列的长度。
2. 状态转移方程:状态转移方程需要考虑两种情况。第一种情况是当两个字符串的当前字符相等时(即s1[i] == s2[j]),这意味着当前字符必定是最长公共子序列的一部分。因此,我们可以在这个位置的状态转移方程中直接继承前一个状态的值并加1,即dp[i][j] = dp[i-1][j-1] + 1。
第二种情况:当两个字符串的当前字符不相等时(即s1[i] != s2[j]),这时需要考虑两种子情况:要么不选择第一个字符串的当前字符(即考察dp[i-1][j]),要么不选择第二个字符串的当前字符(即考察dp[i][j-1])。
这两种情况中的最大值将成为当前状态的值,即dp[i][j] = max(dp[i-1][j], dp[i][j-1])。
值得注意的是,第三种可能性(即同时不选择两个字符)实际上已经被包含在上述两种情况中,因此不需要单独考虑。
所以dp[i][j] = max(dp[i - 1][ j ],dp[ i ][ j - 1])
3. 初始化:引入虚拟层(第0行和第0列)来简化边界条件的处理。具体来说,在原始字符串前添加一个虚拟的空字符,使得dp[0][j]和dp[i][0]都初始化为0,这表示空字符串与任何其他字符串的最长公共子序列长度自然为0
4. 填表顺序:填表顺序遵循自然的从左到右、从上到下的顺序,这样可以确保在计算每个状态时,其所依赖的子问题都已经被计算过。
5. 返回值:由状态表示可知dp[i][j]就表示第一个字符串[0,i]之间和第二个字符串[0,j]之间中的最长公共子序列的长度,所以直接返回dp[m][n]即可,其中 m
和 n
分别是两个输入字符串的长度。
class Solution {public int longestCommonSubsequence(String text1, String text2) {// 获取两个字符串的长度int m = text1.length(), n = text2.length();// 在原始字符串前添加一个空格,构造新的字符数组// 这样做的目的是:// 1. 保持索引映射关系更直观(从1开始)// 2. 空串(第0行/第0列)有实际意义,表示与空串的LCS长度为0char[] ch1 = (" " + text1).toCharArray(); char[] ch2 = (" " + text2).toCharArray();// 1. 创建dp表// dp[i][j]表示text1前i个字符和text2前j个字符的最长公共子序列长度// 数组大小为(m+1)*(n+1)是为了包含空串的情况int[][] dp = new int[m+1][n+1];// 2. 初始化 - 有虚拟层(第0行和第0列),所以不用显式初始化// dp[0][j]和dp[i][0]都默认为0,表示空串与任何字符串的LCS长度为0// 3. 根据状态转移方程进行填表for(int i = 1; i <= m; i++) {for(int j = 1; j <= n; j++) {if(ch1[i] == ch2[j]) {// 如果当前字符相等,则LCS长度等于去掉这两个字符后的LCS长度加1dp[i][j] = dp[i - 1][j - 1] + 1;} else {// 如果当前字符不等,则LCS长度等于两个子问题的较大值:// 1. 去掉text1的当前字符// 2. 去掉text2的当前字符dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);}}}// 4. 返回结果// dp[m][n]就是text1和text2的最长公共子序列长度return dp[m][n];}
}
相关文章:
动态规划算法题1
动态规划做题步骤 确定状态表示:dp表中某一个位置中的值所表示的含义就是状态表示根据状态表示推导状态转移方程:dp[i]等于什么状态转移方程就是什么,用之前或者之后的状态,推导出dp[i]的值初始化(防止越界):根据状态…...
π0.5:带开放世界泛化的视觉-语言-动作模型
25年4月来自具身机器人创业公司 PI 公司的论文“π0.5: a Vision-Language-Action Model with Open-World Generalization”。 为了使机器人发挥作用,它们必须在实验室之外的现实世界中执行实际相关的任务。虽然视觉-语言-动作 (VLA) 模型在端到端机器人控制方面已…...
ESP32开发入门(四):ESP32-s3多串口开发实践
摘要 本文详细介绍ESP32-S3芯片的UART外设开发方法,涵盖UART0(默认调试串口)、UART1和UART2的配置与使用技巧,并提供完整示例代码,帮助开发者快速实现多设备串口通信。 一、ESP32-S3串口硬件资源 ESP32-S3芯片提供3个UART控制器࿱…...
树莓派学习专题<10>:使用V4L2驱动获取摄像头数据--申请和管理缓冲区
树莓派学习专题<10>:使用V4L2驱动获取摄像头数据--申请和管理缓冲区 1. 申请和管理缓冲区代码2. 代码解析3. 实测结果 1. 申请和管理缓冲区代码 /* 数据缓冲区 */ typedef struct tag_BufDesc {void *pvBufPtr ;size_t szBuf…...
Android10.0 Android.bp文件详解,以及内置app编写Android.bp文件
1.前言 在10.0的系统rom定制化开发中,在内置app的时候都是常用的用法,用Android.mk的常用,但是某些时候,会 使用Android.bp的方式来内置app,接下来就来使用常用的方式来写内置so aar jar等文件 2.Android.bp文件详解,以及内置app编写Android.bp文件的介绍 根据设计,An…...
git回退commit
在Git中回退提交(commit)主要有两种方法:使用 `git reset` 或 `git revert`,具体取决于是否需要保留提交历史或是否已推送到远程仓库。以下是详细步骤: 一、使用 `git reset`(适合本地未推送的提交) `git reset` 会移动分支的 HEAD 指针到指定提交,可选择是否保留修改。…...
arcpy列表函数的应用(4)
动态获取字段信息 在处理要素类或表时,可能需要动态获取字段信息,以便根据字段类型或名称进行特定操作。可以使用arcpy.ListFields()函数获取字段列表,并根据需要筛选字段。 示例: python # 获取指定要素类的所有字段 fields …...
02 业务流程架构
业务流程架构提供了自上而下的组织鸟瞰图,是业务流程的全景图。根据所采用的方法不同,有时被称为流程全景图或高层级流程图,提供了业务运营中所有业务流程的整体视图。 这样有助于理解企业内部各个业务流程之间的相互关系以及它们如何共同工…...
「Mac畅玩AIGC与多模态01」架构篇01 - 展示层到硬件层的架构总览
一、概述 AIGC(AI Generated Content)系统由多个结构层级组成,自上而下涵盖交互界面、API 通信、模型推理、计算框架、底层驱动与硬件支持。本篇梳理 AIGC 应用的六层体系结构,明确各组件在系统中的职责与上下游关系,…...
如何有效防止 SQL 注入攻击?
🔒 如何有效防止 SQL 注入攻击? SQL 注入(SQL Injection)是黑客通过构造恶意输入,篡改 SQL 查询语句的攻击方式。以下是 7 大防御策略,涵盖开发、测试和运维全流程。 ✅ 1. 使用参数化查询(Pre…...
路由交换网络专题 | 第九章 | NAT地址转换 | NAT回流
拓扑图 (1)配置实现内网用户可以通过 NAT 转换地址访问外网。 // 配置一条静态路由通往PC2 [AR1]ip route-static 0.0.0.0 0 60.1.1.10 // 配置ACL匹配网段 [AR1]acl 2000 [AR1-acl-basic-2000]rule permit source 192.168.1.10 0.0.0.0 // 设置地址池(不…...
DFPatternFunctor遍历计算图
文件:include/tvm/relay/dataflow_pattern_functor.h 功能:定义 DFPatternFunctor 基类,为 DFPattern 提供访问者模式(Visitor Pattern)的实现框架,支持对不同类型的模式节点进行差异化处理。 继承关系: template &…...
Spring Boot中@RequestParam、@RequestBody、@PathVariable的区别与使用
Spring Boot中RequestParam、RequestBody、PathVariable的区别与使用 前言 在当今的Web开发领域,Spring Boot凭借其简洁、高效和强大的功能,成为了Java开发者构建Web应用的首选框架。在开发过程中,处理来自客户端的请求参数是一项常见且关键…...
大模型 SFT 中的关键技术总结学习
文章目录 微调策略LoRA 微调核心思想具体实现过程超参数与技巧实现步骤 QLoRA 相关技术1. 核心原理2. 技术优势3. 实现流程4. 应用场景 P-tuning核心思想关键技术点训练流程优点应用场景 P-tuning v2Prefix Tuning一、关键概念前缀(Prefix)虚拟标…...
AI如何重塑DDoS防护行业?六大变革与未来展望
一、AI驱动的攻击与防御:攻防博弈的全面升级 AI技术的引入使DDoS攻防进入“智能对抗”时代,攻击者与防御方均借助AI提升效率,形成新的技术平衡。 1. 攻击端:AI赋能攻击的智能进化 动态流量生成:攻击者利用生成对抗网…...
电池的寿命
思路: 首先,我们观察发现:由于每枚电池的使用时间不同,而我们又要减少浪费才能使所有电池加起来用得最久,不难发现:当n2时,输出较小值。 第一步:将电池分为两组,使两组…...
Android完整开发环境搭建/Studio安装/NDK/本地Gradle下载配置/创建AVD/运行一个Android项目/常用插件
目录 安装Android Studio 修改sdk位置 配置 HTTP 代理 安装 NDK 设置快捷键 Gradle 说明 setting.gradle init.gradle build.gradle 下载 相关设置 创建项目 阿里云加速 清理缓存并同步 创建AVD 实用插件 ADB Idea Android Drawable Importer GsonFormat …...
【KWDB 创作者计划】_KWDB引领数据库技术革新的璀璨之星
【KWDB 创作者计划】_KWDB引领数据库技术革新的璀璨之星 🌟嗨,我是LucianaiB! 🌍 总有人间一两风,填我十万八千梦。 🚀 路漫漫其修远兮,吾将上下而求索。 在当今数字化浪潮汹涌澎湃的时代&…...
设计模式--桥接模式详解
桥接模式(bridge pattern) 桥接模式时将抽象部分与它的实现部分分离,使他们可以独立的变化。它是一种对象结构型模式,又称为柄体(Handle and Body)模式或者接口(interface)模式&…...
Python+Selenium+Pytest+Allure PO模式UI自动化框架
一、框架结构 allure-report:测试报告base:定位元素封装data:数据log:日志文件page:页面封装文件夹report:缓存报告testcases:测试用例层utils:工具类run.py:执行文件 二…...
【C语言操作符详解(一)】--进制转换,原反补码,移位操作符,位操作符,逗号表达式,下标访问及函数调用操作符
目录 一.操作符的分类 二.二进制和进制转换 2.1--2进制转10进制 编辑 2.1.1--10进制转2进制数字 2.2--2进制转8进制和16进制 2.2.1--2进制转8进制 2.2.2--2进制转16进制 三.原码,反码,补码 四.移位操作符 4.1--左移操作符 4.2--右移操作符…...
回顾|Apache Cloudberry™ (Incubating) Meetup·2025 杭州站
2025 年 4 月 19 日,由酷克数据与中启乘数联合举办的 Apache Cloudberry™ (Incubating) Meetup 杭州站在浙江省杭州市滨江区滨江会展中心成功举办。本次活动邀请了 Cloudberry PPMC 团队成员、活跃内核贡献者以及中兴 EBASE-A、阿里云 ADB-PG、网易、中启乘数等多…...
使用 Autofac 实现依赖注入
前言:接上一篇文章,有了微软官方的依赖注入组件Microsoft.Extensions.DependencyInjection, 那么今天介绍一个新的开源的依赖注入组件Autofac 一、二者的差异Autofac和微软官方的依赖注入组件(Microsoft.Extensions.DependencyIn…...
HTTP:十二.HTTPS
HTTPS 概述 超文本传输安全协议(英语:HyperText Transfer Protocol Secure,缩写:HTTPS;常称为HTTP over TLS、HTTP over SSL或HTTP Secure)是一种通过计算机网络进行安全通信的传输协议。HTTPS经由HTTP进行通信,利用TLS加密数据包。 HTTPS的主要目的是提供对网站服务器…...
《代码整洁之道》第12章 迭进 - 笔记
好的设计是如何形成的? 章节核心: 好的软件设计不是完全靠前期庞大的设计方案来完成的,而更多地是在持续的编码、测试和重构过程中,“涌现”或“演进”出来的。 设计不是一次性的前期活动 大白话: 作者认为&#x…...
数字巴别塔:全栈多模态开发框架如何用自然语言重构软件生产关系?
一、自然语言编程的范式革命 1. 从代码行数到语义密度 开发效率对比(某金融 SaaS 案例): 开发方式代码量(行)开发时间(天)维护成本($/年)传统 React5,2004512,000低代码…...
【C语言极简自学笔记】C 语言数组详解:一维数组与二维数组
在 C 语言中,数组是一种非常重要的数据结构,它可以将多个相同类型的元素组织在一起,以便于我们进行批量处理和操作。本文将详细介绍 C 语言中的一维数组和二维数组,包括它们的定义、初始化、元素访问以及内存存储等方面的内容。 …...
从零构建云原生秒杀系统——后端架构与实战
📝个人主页🌹:一ge科研小菜鸡-CSDN博客 🌹🌹期待您的关注 🌹🌹 一、引言:秒杀系统的挑战与机遇 在电商、票务、抢购等业务场景中,“秒杀”系统扮演着至关重要的角色。 秒杀活动通常会在极短时间内爆发出数十倍至数百倍的平时流量,这对后端系统的承载能力、响应…...
Linux Socket编程:从API到实战
Linux Socket编程完全指南:从API到实战 概述 Socket(套接字)是网络编程的基础,它允许不同主机或同一主机上的不同进程之间进行通信。在Linux系统中,Socket编程主要通过一系列系统调用来实现,这些API提供了…...
德州仪器(TI)—TDA4VM芯片详解(1)—产品特性
写在前面 本系列文章主要讲解德州仪器(TI)TDA4VM芯片的相关知识,希望能帮助更多的同学认识和了解德州仪器(TI)TDA4VM芯片。 若有相关问题,欢迎评论沟通,共同进步。(*^▽^*) 错过其他章节的同学…...
增强版wps-plugin-deepseek开源插件是DeepSeek 支持的 WPS 插件,在您的办公工作流程中提供智能文档自动化和 AI 驱动的生产力增强
一、软件介绍 文末提供程序和源码下载学习 增强版wps-plugin-deepseek开源插件专为WPS Office插件开发打造的Vue模板,搭配Vite构建工具,提供丰富的WPS API实操示例。虽然官方提供了TypeScript扩展包,但支持程度有限,因此本项目选…...
在 Cursor 中 配置 GitHub MCP Server
文章目录 1、简单回顾 sequentialthinking 的安装2、提出问题:如何在 cursor 配置 github mcp 呢3、结果如下How to Configure GitHub MCP in CursorPrerequisitesStep 1: Update Cursor (if needed)Step 2: Generate a GitHub Personal Access TokenStep 3: Open Cursor MCP S…...
uniapp-商城-40-shop 购物车 选好了 进行订单确认4 配送方式3 地址编辑
前面说了配送 和地址页面 当地址页面为空或需要添加地址时,需要添加地址。 我的地址页面有个按钮 就是添加地址 点击 添加地址 按钮 后,就会跳转到地址添加的页面 1、添加地址页面 2、添加地址文件夹以及文件的创建 3、添加地址的代码 <template…...
初步自定义layui的table(laravel 12)
layui的table是非常好的表格,有美观的样式,对接起来也很便捷。使用后端翻页传过来的数据,本地测试是好的,部署到服务器时,翻页不起作用。故而暂时采用一次性读取全部数据,发送给table,界面如下所…...
手写SpringMVC(基本框架)
服务器启动阶段处理 分析服务器启动阶段都都需要初始化什么? 1.初始化Spring容器 组件扫描包下的类纳入IOC容器管理创建视图解析器对象创建所有的拦截器对象扫描这和包下的所有类org.myspringmvc.web.servlet.mvc.method.annotation,全部实例化&#…...
JS-OCR-demo加载本地文件
背景: 在了解 Tesseract 的识别效果的时候,有个demo项目很好用。有个小毛病,就是没事都要从摄像头抓取图片,然后进行识别。如果可以从本地读取图,就更方便了。 实现: 下载项目代码:https://gi…...
MySQL 表的约束(一)
文章目录 表的约束空属性默认值列描述zerofill主键总结 表的约束 1. 为什么要有表的约束? 因为要保证数据的完整性和可约束性,合法性 空属性 两个值:null(默认的)和not null(不为空)数据库默认字段基本都是字段为空…...
论文导读 - 基于大规模测量与多任务深度学习的电子鼻系统实现目标识别、浓度预测与状态判断
基于大规模测量与多任务深度学习的电子鼻系统实现目标识别、浓度预测与状态判断 原论文地址:https://www.sciencedirect.com/science/article/abs/pii/S0925400521014830 引用此论文(GB/T 7714-2015): WANG T, ZHANG H, WU Y, …...
力扣hot100_子串_python版本
一、560. 和为 K 的子数组 思路:这就是一道典型的前缀和的题代码: class Solution:def subarraySum(self, nums: List[int], k: int) -> int:presum [0] * (len(nums) 1)for i, x in enumerate(nums):presum[i 1] presum[i] x # 前缀和序列需要n1个ans 0…...
cached-property - 类属性缓存装饰器
本文翻译整理自:https://github.com/pydanny/cached-property 文章目录 一、关于 cached-property相关链接资源关键功能特性 二、安装三、使用指南1、基础用法2、手动清除缓存3、线程安全版本4、异步支持5、缓存超时(TTL) 四、致谢 一、关于…...
「Mac畅玩AIGC与多模态03」部署篇02 - 在 Mac 上部署 Dify
一、概述 本篇介绍如何在 macOS 环境下本地部署 Dify 平台,作为多模型协同与工作流集成的可视化应用服务。Dify 提供了模型调用、对话管理、知识库问答、插件服务等功能,可与 Ollama、OpenAI、DeepSeek 等推理后端集成,适用于本地智能体应用的快速搭建与扩展。 二、部署流…...
扩散模型和马尔科夫链
1. 扩散模型的基本原理 扩散模型的灵感来源于热力学扩散(如一滴墨水在水中逐渐扩散的过程),其核心分为两个阶段: 前向过程(Forward Process):逐步向数据添加噪声,直到数据完全变为随…...
Dify框架面试内容整理-Dify如何处理知识库的集成?
Dify 在知识库集成方面采用了“检索增强生成(RAG)”的技术架构,核心实现思路如下: 一、知识库集成的整体流程 Dify处理知识库集成通常包括以下关键步骤: 文档上传↓...
第35课 常用快捷操作——用“鼠标左键”拖动图元
概述 拖动某个图元,是设计过程中常需要用到的操作,我们可以在原理图中拖动某个元器件符号,也可以在PCB图中拖动某个焊盘。 和常用的软件类似,用按住鼠标左键的方式来完成拖动操作。 用鼠标左键拖动图元 在想要拖动的图元上&…...
复盘笔记1
以下是一份专业股市投资操盘手的复盘清单,涵盖市场分析、交易策略、风险管理等核心环节,帮助系统化梳理每日交易并优化次日决策: --- ### **一、市场整体复盘** 1. **指数与成交量分析** - 主要指数表现(上证、深证、创业板、科…...
海思dump图原理
在海思中是用指令进行对应的dump。 例如./vi_chn_dump 0 0 1 1 第一个指令代表是dump哪里的数据。 第一个0代表是vi_pipe。 第二个0代表vi_chn。 第一个1代表需要dump帧的数量。 第二个dump代表dump帧的位置,如果是0表示dump的是在所有ISP模块后面的数据࿰…...
C++:STL——list
一简介 底层是一个带头双向循环列表 二、成员函数 (1)构造函数 三、迭代器 四、修饰函数 (1)insert 插入和删除不再使用下标,而是使用迭代器指针作为要插入位置的形参,这是因为:vector是连续的…...
在Azure Databricks中实现缓慢变化维度(SCD)的三种类型
在Azure Databricks中使用PySpark实现缓慢变化维度(SCD)的三种核心类型,需结合Spark SQL和DataFrame API的特性,并利用Delta Lake的事务支持。以下是具体设计与实现步骤,以及测试用例: 通过以下步骤&#…...
Segment Anything in Images and Videos
目录 摘要 Abstract SAM2 模型框架 图像编码器 记忆机制 提示编码器和掩码解码器 实验 代码 总结 摘要 SAM2是基于Meta公司推出的Segment Anything Model升级而来的先进分割模型。它在SAM的基础上,通过引入记忆注意力模块和优化图像编码器等改进…...
C++之异常
目录 一、异常的概念及使用 1.1、异常的概念 1.2、异常的抛出和捕获 1.3、栈展开 1.4、查找匹配的处理代码 1.5、异常重新抛出 1.6、异常安全问题 1.7、异常规范 1.8、C异常的优缺点 二、标准库的异常 一、异常的概念及使用 1.1、异常的概念 异常处理机制允许程序中…...