【力扣】GO解决子序列相关问题

文章目录

    • 一、引言
    • 二、动态规划方法论深度提炼
      • 子序列问题的通用解法模式
    • 三、通用方法论应用示例:最长递增子序列(LeetCode题目300)
      • Go 语言代码实现
    • 四、最长连续递增序列(LeetCode题目674)
      • Go 语言代码实现
    • 五、最长重复子数组(LeetCode题目718)
      • Go 语言代码实现
    • 六、最长公共子序列(LeetCode题目1143)
      • Go 语言代码实现
    • 七、最大子序和(LeetCode题目53)
      • Go 语言代码实现
    • 八、判断子序列(LeetCode题目392)
      • Go 语言代码实现
    • 九、不同的子序列(LeetCode题目115)
      • Go 语言代码实现
    • 十、两个字符串的删除操作(LeetCode题目583)
      • Go 语言代码实现
    • 十一、编辑距离(LeetCode题目72)
      • Go 语言代码实现
    • 十二、回文子串(LeetCode题目647)
      • Go 语言代码实现
    • 十三、最长回文子序列(LeetCode题目516)
      • Go 语言代码实现
    • 十四、结论


tata


一、引言

在LeetCode的算法题中,子序列相关的问题广泛存在,例如“最长递增子序列”、“最长公共子序列”等。这些问题虽然具体题意不同,但本质上都可通过动态规划(Dynamic Programming, DP)来解决。动态规划是一种通过分解子问题并重用子问题解的算法技术,非常适用于这类具有“最优子结构”的问题。

本篇文章将以多个典型的LeetCode题目为例,逐步提炼出解决子序列问题的通用方法论,帮助您系统掌握动态规划解决子序列问题的核心技巧。

力扣(LeetCode)题目的链接列表:

  1. 300. 最长递增子序列
  2. 674. 最长连续递增序列
  3. 718. 最长重复子数组
  4. 1143. 最长公共子序列
  5. 53. 最大子序和
  6. 392. 判断子序列
  7. 115. 不同的子序列
  8. 583. 两个字符串的删除操作
  9. 72. 编辑距离
  10. 647. 回文子串
  11. 516. 最长回文子序列

二、动态规划方法论深度提炼

动态规划解题方法通常包含以下四个核心步骤:

  1. 确定子问题:识别问题的子问题如何划分,并使得较小问题的解可复用于较大问题的解。
  2. 构建最优子结构:确保通过每个子问题的最优解,组合得到整个问题的最优解。
  3. 定义状态转移方程:状态转移方程是动态规划的核心,通过它定义出从一个子问题解递推到另一个子问题解的规则。
  4. 初始化和递归顺序:设置初始值,明确递归或迭代顺序。

子序列问题的通用解法模式

在LeetCode的子序列问题中,动态规划的具体实现有以下共通模式:

  • dp数组的定义dp数组一般用来表示问题最优解中的特定属性,例如“最长子序列长度”、“出现次数”、“和”等。dp数组的定义决定了每个子问题的状态表示。
  • 状态转移方程设计
    • 对于“最长递增子序列”类问题,通常通过比较当前元素与前面元素的关系,递推得到最长子序列的长度。
    • 对于“最长公共子序列”类问题,通常根据两个字符串的字符是否相等来递归或迭代。
    • 对于“编辑距离”类问题,通常需要综合增删改三种操作的最小代价。
  • 初始化与递归顺序:大部分问题中,dp数组会有边界初始值或特定的初始化设定,并采用自底向上或自顶向下的递归顺序。

三、通用方法论应用示例:最长递增子序列(LeetCode题目300)

题目描述
题意:给定一个整数数组nums,找到其中最长的严格递增子序列的长度。

示例

输入: nums = [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长递增子序列之一是 [2,3,7,101],因此长度为 4。

解题思路

  1. dp数组定义:我们定义dp[i]为以nums[i]结尾的最长递增子序列的长度。
  2. 状态转移方程:对于每一个i,遍历0i-1的所有元素j,如果nums[j] < nums[i],则可以将nums[i]接在以nums[j]结尾的递增子序列后面,因此有dp[i] = max(dp[i], dp[j] + 1)
  3. 初始化:由于单个元素本身也是一个递增子序列,因此dp数组的每个元素初始值都设为1
  4. 递归顺序:从前往后遍历每个元素,依次计算以每个元素结尾的最长子序列长度。

Go 语言代码实现

func lengthOfLIS(nums []int) int {l := len(nums)result := 1 // 最长递增子序列的长度,初始化为1dp := make([]int, l) // dp[i]表示以nums[i]结尾的最长递增子序列长度for i := range dp {dp[i] = 1 // 每个位置的递增子序列长度至少为1}for i := 1; i < l; i++ {for j := 0; j < i; j++ {if nums[j] < nums[i] { // 只有当nums[i]大于nums[j]时,才能构成递增子序列dp[i] = max(dp[i], dp[j]+1) // 更新dp[i]为最大长度}}if dp[i] > result {result = dp[i] // 更新全局最长子序列长度}}return result
}// 工具函数:返回两个整数中的最大值
func max(a, b int) int {if a > b {return a}return b
}

关键要点分析

  • 状态转移方程dp[i] = max(dp[i], dp[j] + 1)的设计,体现了子序列问题的核心思想,即将子问题的最优解转移到大问题上。
  • 由于每个元素的状态仅依赖于之前的元素,因此可以通过嵌套循环递推得到最终解。

四、最长连续递增序列(LeetCode题目674)

题目描述
给定一个未经排序的整数数组nums,找到最长的连续递增子序列(子序列是由连续的数字组成的子数组)的长度。

示例

输入: nums = [1,3,5,4,7]
输出: 3
解释: 最长连续递增子序列是 [1,3,5],长度为3。

解题思路

在该问题中,关键在于找到连续递增的子序列。我们可以使用动态规划(DP)来实现:

  1. dp数组定义:定义dp[i]为以nums[i]结尾的最长连续递增子序列的长度。
  2. 状态转移方程:如果nums[i] > nums[i-1],说明当前元素可以与前一个元素构成递增子序列,因此dp[i] = dp[i-1] + 1。否则,dp[i]重置为1,表示以nums[i]结尾的新起始子序列。
  3. 初始化:每个位置的递增序列至少包含自身一个元素,因此将所有dp[i]初始化为1
  4. 递归顺序:从左到右遍历数组,每次更新dp[i]的值。

Go 语言代码实现

func findLengthOfLCIS(nums []int) int {l := len(nums)if l == 0 {return 0}dp := make([]int, l) // dp[i]表示以nums[i]结尾的最长连续递增子序列长度for i := range dp {dp[i] = 1 // 初始化为1}result := 1 // 用于存储最长连续递增子序列的长度for i := 1; i < l; i++ {if nums[i] > nums[i-1] { // 如果当前元素大于前一个元素dp[i] = dp[i-1] + 1 // 累加前一个子序列的长度} else {dp[i] = 1 // 否则,重置为1,重新开始}if dp[i] > result {result = dp[i] // 更新全局最长长度}}return result
}

关键要点分析

  • 在该题中,连续递增的特点使得状态转移方程更为简单。我们只需判断当前元素和前一元素的关系,以确定是否延续或重新开始子序列。
  • result变量用于存储最长的连续递增子序列长度,确保在遍历中随时得到最终解。

五、最长重复子数组(LeetCode题目718)

接下来,我们将介绍最长重复子数组问题,这一题与前面的最长递增序列问题有所不同,因为它涉及两个数组的匹配问题。


题目描述
给定两个整数数组nums1nums2,返回两个数组中公共的、长度最长的重复子数组的长度。

示例

输入: nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7]
输出: 3
解释: 长度最长的重复子数组是 [3,2,1],长度为3。

解题思路

  1. dp数组定义:我们定义dp[i][j]为在nums1中以第i-1位结尾和在nums2中以第j-1位结尾的最长重复子数组的长度。
  2. 状态转移方程:如果nums1[i-1] == nums2[j-1],则dp[i][j] = dp[i-1][j-1] + 1;否则,dp[i][j] = 0
  3. 初始化dp[i][0]dp[0][j]初始化为0,因为任一数组为空时没有公共子数组。
  4. 递归顺序:从左到右、从上到下填充二维dp数组,并在过程中记录最长的子数组长度。

Go 语言代码实现

func findLength(nums1 []int, nums2 []int) int {l1, l2 := len(nums1), len(nums2)dp := make([][]int, l1+1) // dp[i][j]表示以nums1[i-1]和nums2[j-1]结尾的最长重复子数组长度for i := range dp {dp[i] = make([]int, l2+1)}result := 0 // 用于记录最长重复子数组的长度for i := 1; i <= l1; i++ {for j := 1; j <= l2; j++ {if nums1[i-1] == nums2[j-1] { // 如果两数组对应位置的元素相等dp[i][j] = dp[i-1][j-1] + 1 // 延长前一个子数组的长度if dp[i][j] > result {result = dp[i][j] // 更新全局最长长度}} else {dp[i][j] = 0 // 否则没有公共子数组,重置为0}}}return result
}

关键要点分析

  • 该题的dp数组使用二维结构,以便对比nums1nums2的每个位置组合。
  • 每次找到一个相同元素时,延长重复子数组的长度;否则,将当前子问题解置零。
  • 通过对dp数组的累积更新,记录并获取最长的公共子数组长度。

好的,接下来是最长公共子序列问题(LeetCode题目1143)的详细解析。


六、最长公共子序列(LeetCode题目1143)

题目描述
给定两个字符串text1text2,返回这两个字符串的最长公共子序列的长度。如果不存在公共子序列,则返回0。子序列不要求在原字符串中是连续的,但顺序必须一致。

示例

输入: text1 = "abcde", text2 = "ace" 
输出: 3  
解释: 最长公共子序列是 "ace",因此返回3。

解题思路

最长公共子序列问题与前面的最长重复子数组不同,它关注的是子序列(不连续)的匹配,而不是子数组(连续)的匹配。该问题可使用动态规划解决,步骤如下:

  1. dp数组定义:定义dp[i][j]为字符串text1i个字符和字符串text2j个字符的最长公共子序列的长度。
  2. 状态转移方程:如果text1[i-1] == text2[j-1],则dp[i][j] = dp[i-1][j-1] + 1;否则,dp[i][j] = max(dp[i-1][j], dp[i][j-1])。这个方程表示如果当前字符匹配,则可以在前一个状态基础上延长子序列;如果不匹配,则取两个可能情况的最大值。
  3. 初始化:如果任一字符串为空(即dp[i][0]dp[0][j]),则公共子序列长度为0
  4. 递归顺序:按行或按列填充二维dp数组。

Go 语言代码实现

func longestCommonSubsequence(text1, text2 string) int {l1, l2 := len(text1), len(text2)dp := make([][]int, l1+1) // dp[i][j]表示text1前i个字符和text2前j个字符的最长公共子序列长度for i := range dp {dp[i] = make([]int, l2+1)}for i := 1; i <= l1; i++ {for j := 1; j <= l2; j++ {if text1[i-1] == text2[j-1] { // 当两个字符相等时dp[i][j] = dp[i-1][j-1] + 1 // 延长公共子序列长度} else {dp[i][j] = max(dp[i-1][j], dp[i][j-1]) // 否则取相邻位置的最大长度}}}return dp[l1][l2] // 返回最长公共子序列的长度
}// 工具函数:返回两个整数中的最大值
func max(a, b int) int {if a > b {return a}return b
}

关键要点分析

  • 状态转移方程的设计清晰表达了最长公共子序列的递归关系:当两个字符匹配时,可通过延长已有公共子序列长度获得新解。
  • dp[i][j]的计算依赖于其左、上和左上方位置的值,因此填充顺序需从左到右、从上到下。
  • 通过遍历dp数组的终点值,即dp[l1][l2],得到最长公共子序列的长度。

七、最大子序和(LeetCode题目53)

接下来,我们介绍最大子序和问题,这是一个经典的动态规划问题,要求在一个数组中找到连续子数组的最大和。


题目描述
给定一个整数数组nums,找到具有最大和的连续子数组,并返回其和。

示例

输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。

解题思路

该题目要求寻找最大子序和,属于“区间最大和”问题。可以通过动态规划来解决:

  1. dp数组定义:定义dp[i]为以nums[i]结尾的最大子序和。
  2. 状态转移方程:若前一个位置的dp[i-1]值大于0,则将其加入当前元素形成新子数组;否则,当前元素单独作为新子序列的起点。因此,dp[i] = max(dp[i-1] + nums[i], nums[i])
  3. 初始化dp[0] = nums[0],因为以第一个元素结尾的最大子序和即为nums[0]
  4. 递归顺序:从左到右遍历数组,逐步累积计算最大和。

Go 语言代码实现

func maxSubArray(nums []int) int {l := len(nums)dp := make([]int, l) // dp[i]表示以nums[i]结尾的最大子序和dp[0] = nums[0]result := nums[0] // 存储全局最大和for i := 1; i < l; i++ {dp[i] = max(dp[i-1]+nums[i], nums[i]) // 更新dp[i]为当前最大和if dp[i] > result {result = dp[i] // 更新全局最大和}}return result
}

关键要点分析

  • 该题的dp数组设计特别之处在于,它在每一步选择是否将前面的子序列结果带入当前状态。
  • 状态转移方程中的max(dp[i-1] + nums[i], nums[i])确保了我们每一步都可以选择“重开”子序列或“延续”已有的子序列,从而实现最优子结构。

八、判断子序列(LeetCode题目392)

最后,我们来看一个简单但巧妙的子序列问题,即如何判断一个字符串是否是另一个字符串的子序列。


题目描述
给定字符串st,判断s是否是t的子序列。可以假设两个字符串均仅包含英文小写字母。

示例

输入: s = "abc", t = "ahbgdc"
输出: true
解释: s 是 t 的子序列。

解题思路

这个问题不需要完整的动态规划来解决,因为子序列要求顺序一致但不连续。我们可以通过双指针或DP判断每个字符的匹配情况。

  1. dp数组定义:使用双指针分别遍历st,若s[i] == t[j],则移动s的指针,否则仅移动t的指针。
  2. 状态转移方程:无经典的DP状态转移方程,本题可用指针遍历简化实现。
  3. 初始化:双指针从字符串起始位置开始。
  4. 递归顺序:顺序遍历st

Go 语言代码实现

//动态规划

func isSubsequence1(s, t string) bool {ls, lt := len(s), len(t)dp := make([][]int, ls+1)var maxLength intfor i := range dp {dp[i] = make([]int, lt+1)}for i := 1; i <= ls; i++ {for j := 1; j <= lt; j++ {if s[i-1] == t[j-1] {dp[i][j] = dp[i-1][j-1] + 1} else {dp[i][j] = dp[i][j-1]}if maxLength < dp[i][j] {maxLength = dp[i][j]}}}return maxLength == ls
}

//双指针

func isSubsequence(s, t string) bool {i, j := 0, 0ls, lt := len(s), len(t)for i < ls && j < lt {if s[i] == t[j] { // 若字符匹配,移动s的指针i++}j++ // 始终移动t的指针}return i == ls // 若s的指针移动到末尾,则s是t的子序列
}

关键要点分析

  • 双指针法在保证正确性的前提下,以最优复杂度O(n)解决了子序列判定问题。
  • 相较于完整的DP表结构,双指针方法简洁且高效,非常适合子序列匹配类问题。

好的,接下来我们继续介绍 不同的子序列(LeetCode题目115)的问题及其解法。


九、不同的子序列(LeetCode题目115)

题目描述
给定一个字符串s和一个字符串t,计算在s的子序列中t出现的个数。

示例

输入: s = "rabbbit", t = "rabbit"
输出: 3
解释: 如下图所示,有 3 种可以从 s 中得到 "rabbit" 的方式。

解题思路

在该题中,我们需要统计ts中作为子序列出现的次数。可以使用动态规划,通过分析字符是否匹配,递推出每个子问题的解。具体步骤如下:

  1. dp数组定义:定义dp[i][j]s的前i个字符中t的前j个字符出现的次数。
  2. 状态转移方程
    • s[i-1] == t[j-1]时,dp[i][j] = dp[i-1][j-1] + dp[i-1][j],表示当前字符相等时,子序列可以选择匹配当前字符或忽略当前字符;
    • s[i-1] != t[j-1]时,dp[i][j] = dp[i-1][j],表示字符不匹配的情况下,只能通过忽略当前字符来求解。
  3. 初始化dp[i][0] = 1,即t为空时,是任何字符串的子序列,子序列出现次数为1。
  4. 递归顺序:按行遍历dp表,以自顶向下、自左向右的顺序填充。

Go 语言代码实现

func numDistinct(s string, t string) int {ls, lt := len(s), len(t)dp := make([][]int, ls+1) // dp[i][j]表示s的前i个字符中包含t的前j个字符的子序列个数for i := range dp {dp[i] = make([]int, lt+1)dp[i][0] = 1 // 当t为空字符串时,是s的子序列}for i := 1; i <= ls; i++ {for j := 1; j <= lt; j++ {if s[i-1] == t[j-1] {dp[i][j] = dp[i-1][j-1] + dp[i-1][j] // 匹配或不匹配当前字符} else {dp[i][j] = dp[i-1][j] // 不匹配时,忽略s的当前字符}}}return dp[ls][lt]
}

关键要点分析

  • 该题的动态规划表格通过字符匹配与否,确保了子序列匹配的所有可能组合都被考虑。
  • 通过逐层累积每一子问题的最优解,dp[ls][lt]即为s中出现t的所有方式。

十、两个字符串的删除操作(LeetCode题目583)

接下来,我们将探讨两个字符串的删除操作这一问题,该题的本质是在两个字符串之间寻找相似部分,从而最小化删除次数。


题目描述
给定两个单词word1word2,返回使得两个单词相同所需的最小步数。每步可以删除任意一个字符串中的一个字符。

示例

输入: word1 = "sea", word2 = "eat"
输出: 2
解释: 将 "sea" 变为 "ea" 并删除 "t",最少需要 2 步。

解题思路

该题的核心在于最小化两个字符串的删除操作,具体实现可以通过找到最长公共子序列来进行。步骤如下:

  1. dp数组定义:定义dp[i][j]为将word1的前i个字符与word2的前j个字符变为相同所需的最小步数。
  2. 状态转移方程
    • word1[i-1] == word2[j-1]时,字符相等,不需删除操作,故dp[i][j] = dp[i-1][j-1]
    • word1[i-1] != word2[j-1]时,考虑删除一个字符后使得剩余字符匹配的最小代价,故dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + 1
  3. 初始化dp[i][0] = i,表示word2为空时,需要删除word1的所有字符;同理,dp[0][j] = j
  4. 递归顺序:从左到右逐步构建二维dp表。

Go 语言代码实现

func minDistance(word1 string, word2 string) int {l1, l2 := len(word1), len(word2)dp := make([][]int, l1+1) // dp[i][j]表示将word1的前i个字符与word2的前j个字符变为相同所需的最小步数for i := range dp {dp[i] = make([]int, l2+1)dp[i][0] = i // 初始化第一列}for j := range dp[0] {dp[0][j] = j // 初始化第一行}for i := 1; i <= l1; i++ {for j := 1; j <= l2; j++ {if word1[i-1] == word2[j-1] {dp[i][j] = dp[i-1][j-1] // 如果字符相等,不需要额外操作} else {dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + 1 // 选择删除的最小代价}}}return dp[l1][l2]
}// 工具函数:返回两个整数中的较小值
func min(a, b int) int {if a < b {return a}return b
}

关键要点分析

  • 该问题可以看作是“编辑距离”的简化版,主要涉及删除操作,因此只需考虑删除的最小代价。
  • dp[i][j]记录了当前最小步数,并通过累积前面的最小操作实现了从局部最优解推向全局最优解。

十一、编辑距离(LeetCode题目72)

编辑距离是经典的动态规划问题之一,主要关注插入、删除和替换操作的最小代价。


题目描述
给定两个单词word1word2,计算将word1转换成word2所需的最小操作数。可以对一个单词执行插入、删除或替换操作。

示例

输入: word1 = "horse", word2 = "ros"
输出: 3
解释: horse -> rorse (替换 'h' 为 'r') -> rose (删除 'r') -> ros (删除 'e')

解题思路

该问题可通过动态规划解决,逐步最小化每次编辑的代价:

  1. dp数组定义dp[i][j]为将word1的前i个字符变为word2的前j个字符所需的最小编辑次数。
  2. 状态转移方程
    • 如果word1[i-1] == word2[j-1],则无需编辑,dp[i][j] = dp[i-1][j-1]
    • 若不相等,则取插入、删除、替换操作的最小代价,dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1
  3. 初始化:当word2为空时,dp[i][0] = i;同理,dp[0][j] = j
  4. 递归顺序:逐步填充整个dp表,从左到右、从上到下。

Go 语言代码实现

func minDistance(word1, word2 string) int {l1, l2 := len(word1), len(word2)dp := make([][]int, l1+1) // dp[i][j]表示将word1的前i个字符变为word2的前j个字符的最小编辑次数for i := range dp {dp[i] = make([]int, l2+1)dp[i][0] = i // 初始化第一列}for j := range dp[0] {dp[0][j] = j // 初始化第一行}for i := 1; i <= l1; i++ {for j := 1; j <= l2; j++ {if word1[i-1] == word2[j-1] {dp[i][j] = dp[i-1][j-1] // 如果字符相等,不需额外操作} else {dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1 // 插入、删除、替换的最小代价}}}return dp[l1][l2]
}// 工具函数:返回三个整数中的最小值
func min(a, b, c int) int {if a < b {if a < c {return a}return c}if b < c {return b}return c
}

关键要点分析

  • 本题动态规划的核心在于清晰的状态转移方程,将字符是否匹配的三种编辑操作的最小代价逐步递推。
  • dp[i][j]通过三种操作的最小代价累积,得出将word1转换成word2所需的最小编辑距离。

好的,接下来我们继续介绍 回文子串(LeetCode题目647)的问题及其解法。


十二、回文子串(LeetCode题目647)

题目描述
给定一个字符串s,计算并返回s中的回文子串数量。具有相同顺序且相同字符的子串称为回文。

示例

输入: s = "abc"
输出: 3
解释: 回文子串为 ["a","b","c"]。
输入: s = "aaa"
输出: 6
解释: 回文子串为 ["a","a","a","aa","aa","aaa"]。

解题思路

这道题要求我们统计字符串中的回文子串数量,可以利用动态规划来判断每个子串是否为回文,并记录回文的数量。

  1. dp数组定义:定义dp[i][j]表示从字符s[i]s[j]的子串是否是回文子串。若为回文子串,则dp[i][j] = true,否则为false
  2. 状态转移方程
    • s[i] == s[j]j - i <= 1时,dp[i][j] = true
    • 否则,当s[i] == s[j]dp[i+1][j-1] = true时,dp[i][j] = true
  3. 初始化:长度为1的子串均为回文,因此对每个位置idp[i][i] = true
  4. 递归顺序:从右下角向左上角遍历dp数组,逐步填充。

Go 语言代码实现

func countSubstrings(s string) int {l := len(s)dp := make([][]bool, l) // dp[i][j]表示s从i到j的子串是否为回文for i := range dp {dp[i] = make([]bool, l)}result := 0 // 用于记录回文子串的数量for i := l - 1; i >= 0; i-- { // 从右下角到左上角填充dp数组for j := i; j < l; j++ {if s[i] == s[j] && (j-i <= 1 || dp[i+1][j-1]) {dp[i][j] = true // s[i:j+1]为回文result++ // 每找到一个回文子串就计数加一}}}return result
}

关键要点分析

  • 状态转移方程dp[i][j]结合字符相等性和内部子串的回文性有效判断了每个子串的回文性质。
  • 遍历顺序为从右下角到左上角,保证每个较短子串的状态在较长子串状态确定前已完成更新。

十三、最长回文子序列(LeetCode题目516)

最后,我们来看一道回文序列问题,要求在一个字符串中找到最长的回文子序列长度。


题目描述
给定一个字符串s,找到其中最长的回文子序列的长度。子序列不要求是连续的,但顺序必须保持一致。

示例

输入: s = "bbbab"
输出: 4
解释: "bbbb" 是最长的回文子序列。

解题思路

该问题可以使用动态规划通过递归判断字符的对称性来解决。

  1. dp数组定义:定义dp[i][j]为字符串s从位置ij的子串的最长回文子序列长度。
  2. 状态转移方程
    • s[i] == s[j],则dp[i][j] = dp[i+1][j-1] + 2
    • s[i] != s[j],则dp[i][j] = max(dp[i+1][j], dp[i][j-1])
  3. 初始化:对于每个位置idp[i][i] = 1,即单个字符的回文长度为1。
  4. 递归顺序:从下到上、从左到右填充dp数组,保证子问题优先求解。

Go 语言代码实现

func longestPalindromeSubseq(s string) int {l := len(s)dp := make([][]int, l) // dp[i][j]表示s从i到j的最长回文子序列长度for i := range dp {dp[i] = make([]int, l)dp[i][i] = 1 // 初始化单个字符的回文长度为1}for i := l - 1; i >= 0; i-- { // 从下到上遍历for j := i + 1; j < l; j++ { // 从左到右填充if s[i] == s[j] {dp[i][j] = dp[i+1][j-1] + 2 // 若字符相等,则递推最长回文长度} else {dp[i][j] = max(dp[i+1][j], dp[i][j-1]) // 否则取左右子问题的最大值}}}return dp[0][l-1] // 返回整个字符串的最长回文子序列长度
}// 工具函数:返回两个整数中的最大值
func max(a, b int) int {if a > b {return a}return b
}

关键要点分析

  • 本题通过递归状态方程有效地判断了字符对称性,dp[i][j]记录每个子问题的最优解,保证子问题递推到全局。
  • 本题与最长回文子串问题不同,最长回文子序列并不要求子序列连续,状态转移时依赖左、下和左下角元素。

十四、结论

在本篇文章中,我们系统地梳理了LeetCode中一系列子序列类问题的动态规划解法。通过dp数组定义、状态转移方程设计、初始化、递归顺序等方面,我们展示了如何构建并解决常见的子序列问题。动态规划不仅要求细致的代码实现,更强调对问题的整体分解与子问题的递推组合,这些方法论将有助于提升算法的解决能力。希望本文对您深入理解和掌握动态规划有所帮助!

关注我

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

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

相关文章

目前最新 Reflector V11.1.0.2067版本 .NET 反编译软件

目前最新 Reflector V11.1.0.2067版本 .NET 反编译软件 一、简介二、.NET Reflector的主要功能包括&#xff1a;1. **反编译**: 反编译是将已编译的.NET程序集&#xff08;如.dll或.exe文件&#xff09;转换回可读的源代码。这使得开发者可以查看和学习第三方库的实现细节&…

手机淘宝自动下单退货自动化RPA脚本机器人

使用手机集线器连接多个手机并发运行。 脚本分3个部分&#xff08;读取本地连接下单&#xff0c;退货获取退货地址信息&#xff0c;填写快递单号&#xff09; 脚本部分图结构看下面的图片 部分数据统计展示

基于vue框架的的高校设备信息管理系统的设计与实现tx6d7(程序+源码+数据库+调试部署+开发环境)系统界面在最后面。

系统程序文件列表 项目功能&#xff1a;设备管理员,设备维护员,设备类别,设备,设备入库,设备分发,设备调拨,定期维护,维护任务,设备运行记录 开题报告内容 基于Vue框架的高校设备信息管理系统的设计与实现开题报告 一、项目背景及意义 随着高校教育事业的蓬勃发展&#xff…

线性代数学习

1.标量由只有一个元素的张量表示 import torchx torch.tensor([3,0]) y torch.tensor([2,0])x y, x * y, x / y, x**y 2.可以将向量视为标量值组成的列表 x torch.arange(4) x 3.通过张量的索引访问任一元素 x[3] 4.访问张量长度 len(x) 5.只有一个轴的张量&#xff0c…

gin入门教程(10):实现jwt认证

使用 github.com/golang-jwt/jwt 实现 JWT&#xff08;JSON Web Token&#xff09;可以有效地进行用户身份验证,这个功能往往在接口前后端分离的应用中经常用到。以下是一个基本的示例&#xff0c;演示如何在 Gin 框架中实现 JWT 认证。 目录结构 /hello-gin │ ├── cmd/ …

Could not retrieve mirrorlist http://mirrorlist.centos.org错误解决方法

文章目录 背景解决方法 背景 今天在一台新服务器上安装nginx&#xff0c;在这个过程中需要安装相关依赖&#xff0c;在使用yum install命令时&#xff0c;发生了以下报错内容&#xff1a; Could not retrieve mirrorlist http://mirrorlist.centos.org/?release7&archx8…

【永中软件-注册/登录安全分析报告】

前言 由于网站注册入口容易被黑客攻击&#xff0c;存在如下安全问题&#xff1a; 暴力破解密码&#xff0c;造成用户信息泄露短信盗刷的安全问题&#xff0c;影响业务及导致用户投诉带来经济损失&#xff0c;尤其是后付费客户&#xff0c;风险巨大&#xff0c;造成亏损无底洞…

第二十九篇:TCP的报文格式,TCP系列三

TCP是协议&#xff0c;是协议就有协议格式。 1.源端口和目的端口 TCP源端口&#xff08;Source Port&#xff09;&#xff1a;源计算机上的应用程序的端口号&#xff0c;占 16 位。 TCP目的端口&#xff08;Destination Port&#xff09;&#xff1a;目标计算机的应用程序端口…

政安晨【零基础玩转各类开源AI项目】基于本地Ubuntu (Linux ) 系统应用Gradio-Lite:无服务器 Gradio 完全在浏览器中运行

目录 简介 什么是@gradio/lite? 入门 1.导入 JS 和 CSS 2. 创建标签 3. 在标签内编写你的 Gradio 应用程序 更多示例:添加其他文件和要求 多个文件 其他要求 SharedWorker 模式 代码和演示playground 1.无服务器部署 2.低延迟 3. 隐私和安全 限制 尝试一下!…

基于Python+SQL Server2008实现(GUI)快递管理系统

快递业务管理系统的设计与实现 摘要: 着网络新零售的到来&#xff0c;传统物流在网购的洗礼下迅速蜕变&#xff0c;在这场以互联网为基础的时代变革中&#xff0c;哪家企业能率先转变其工作模式就能最先分得一杯羹&#xff0c;物流管理也不例外。传统的物流管理模式效率低下&a…

同城分类信息网站源码系统 PHP+MySQL组合开发 带完整的安装代码包以及搭建部署教程

系统概述 该系统采用PHP作为后端开发语言&#xff0c;MySQL作为数据库管理系统。PHP是一种广泛使用的开源脚本语言&#xff0c;特别适合于Web开发&#xff0c;具有跨平台、易于学习、性能稳定等优点。MySQL则是一款轻量级的关系型数据库管理系统&#xff0c;具有体积小、速度快…

DHorse v1.6.0 发布,基于 k8s 的发布平台

版本说明 新增特性 支持Codeup&#xff08;阿里云云效&#xff09;代码仓库&#xff1b;支持环境的自动部署&#xff1b; 优化特性 管理员角色部署环境部需要审批&#xff1b;优化页面展示&#xff1b; 升级指南 升级指南 DHorse介绍 DHorse是一个轻量级、简单易用的云…

微服务网关Zuul

一、Zuul简介 Zuul是Netflix开源的微服务网关&#xff0c;包含对请求的路由和过滤两个主要功能。 1&#xff09;路由功能&#xff1a;负责将外部请求转发到具体的微服务实例上&#xff0c;是实现外部访问统一入口的基础。 2&#xff09;过滤功能&#xff1a;负责对请求的过程…

crc, md5 和 sha的区别

效率不同: 直接看代码 import zlib import hashlib import timewith open(rD:\data., rb) as f:x f.read()s time.time() for i in range(100000):d zlib.crc32(x) print(time.time() - s)s time.time() for i in range(100000):m hashlib.md5()m.update(x)d m.hexdige…

【CSS】纯CSS Loading动画组件

<template><div class"ai-loader-box"><!-- AI loader --><div class"ai-loader"><div class"text"><p>AI智能分析中....</p></div><div class"horizontal"><div class&quo…

idea项目搭建的四种方式: 一(以idea2017为例)

目录 1. 普通java项目 2. 普通JavaWEB项目 3. maven的JavaWEB项目 4. maven的java项目 1. 普通java项目 ①点击“Create New Project”&#xff1b; ②选择“Java”&#xff0c;选择自己安装的jdk&#xff0c;点击“Next”&#xff1b; ③填好项目名称和路径&#xff1b;…

记录一个docker volume映射目录创建文件报错问题

最近用docker-compse做中间件部署。 发现使用volume映射目录后&#xff0c;初始化时创建文件报错导致容器启动失败问题。 提示是没有访问权限。 如下&#xff1a; mariadb 10:07:26.86 INFO > mariadb 10:07:26.86 INFO > Welcome to the Bitnami mariadb contain…

CORS预检请求配置流程图 srpingboot和uniapp

首先要会判断预检请求 还是简单请求 简单请求 预检请求 #mermaid-svg-1R9nYRa7P9Pll4AK {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-1R9nYRa7P9Pll4AK .error-icon{fill:#552222;}#mermaid-svg-1R9nYRa7P9Pll4…

基于ssm+vue的购物商场的设计与实现

博主介绍&#xff1a;专注于Java&#xff08;springboot ssm 等开发框架&#xff09; vue .net php phython node.js uniapp 微信小程序 等诸多技术领域和毕业项目实战、企业信息化系统建设&#xff0c;从业十五余年开发设计教学工作 ☆☆☆ 精彩专栏推荐订阅☆☆☆☆☆不…

【视频生成大模型】 视频生成大模型 THUDM/CogVideoX-2b

【视频生成大模型】 视频生成大模型 THUDM/CogVideoX-2b CogVideoX-2b 模型介绍发布时间模型测试生成的demo视频生成视频限制 运行环境安装运行模型下载开源协议参考 CogVideoX-2b 模型介绍 CogVideoX是 清影 同源的开源版本视频生成模型。 基础信息&#xff1a; 发布时间 2…