【力扣】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 // 最长递增子序列的长度,初始化为1
	dp := 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 int
	for 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, 0
	ls, 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数组定义、状态转移方程设计、初始化、递归顺序等方面,我们展示了如何构建并解决常见的子序列问题。动态规划不仅要求细致的代码实现,更强调对问题的整体分解与子问题的递推组合,这些方法论将有助于提升算法的解决能力。希望本文对您深入理解和掌握动态规划有所帮助!

关注我

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

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

相关文章

EXCELL中如何两条线画入一张图中,标记坐标轴标题?

1&#xff0c;打开excel&#xff0c;左击选中两列&#xff0c; 2&#xff0c;菜单栏>“插入”>”二维折线图”选中一个 3&#xff0c;选中出现的两条线中的一条右击>最下一行&#xff0c;“设置数据系列格式” 4&#xff0c;右测“系列选项中”>点击“次坐标轴” 5…

邮件系统SSL加密传输,保护你的电子邮件免受网络威胁

在互联网的浪潮中&#xff0c;企业数字化转型的步伐不断加快。企业邮箱作为数字化应用的重要组成部分&#xff0c;已成为员工沟通、协同工作和企业管理的关键工具。但是在公共网络安全性普遍较弱的背景下&#xff0c;黑客容易侵入企业网络&#xff0c;监控流量&#xff0c;截获…

私域朋友圈运营

今天必须给大家分享一份超棒的朋友圈运营思维导图 有了它&#xff0c;你可以逐步打造属于自己的精彩朋友圈&#x1f389;。无论是想分享生活点滴&#x1f4a7;&#xff0c;还是展示个人魅力✨&#xff0c;又或者推广自己的业务&#x1f4c8;&#xff0c;这份思维导图都能给你指…

vuetify学习笔记(v-app和v-responsive)

我最近在学习vuetify3&#xff0c;我以前是用element plus和taiwind css。vuetify的一个好处是&#xff0c;它不仅是一个向element plus一样提供好用的组件库&#xff0c;而且还提供了向taiwind css一样的原子类&#xff0c;可以通过类名方便的定义组建的样式。以前element plu…

为什么使用 toFixed 方法的结果不一致呢?

偶遇一个不准确的方法 toFixed() &#xff0c;其是 JS 中用于将数字格式化为指定小数位数的方法&#xff0c;但有时返回的结果不够准确&#xff0c;展示如下&#xff1a; 这通常是由于 JavaScript 对浮点数的处理方式导致的。 1. 浮点数精度问题 JavaScript 中的数字是以 IEE…

乡村小道图像分割系统:智能化检测

乡村小道图像分割系统源码&#xff06;数据集分享 [yolov8-seg-C2f-Faster-EMA&#xff06;yolov8-seg-HGNetV2等50全套改进创新点发刊_一键训练教程_Web前端展示] 1.研究背景与意义 项目参考ILSVRC ImageNet Large Scale Visual Recognition Challenge 项目来源AAAI Globa…

JavaWeb合集22-Apache POI

二十二、Apache POI Apache POI是一个处理Miscrosoft Office各种文件格式的开源项目。简单来说就是&#xff0c;我们可以使用POI在Java 序中对Miscrosoft Office各种文件进行读写操作。一般情况下&#xff0c;POI都是用于操作Excel文件。 使用场景&#xff1a;银行网银系统导出…

Unity Vision Pro 保姆级开发教程-PolySpatial VisionOS Samples 示例场景

视频教程地址 PolySpatial VisionOS Samples 示例场景 Unity Vision Pro 中文课堂教程地址&#xff1a; Unity3D Vision Pro 开发教程【保姆级】 | Unity 中文课堂 有界体积样本 Balloon Gallery 气球画廊 气球画廊是一个迷你游戏&#xff0c;演示了使用Indirect Pinch and …

vue3使用i18n做国际化多语言,实现常量跟随语言切换翻译

因为我有一个常量的配置文件在项目中&#xff0c;而且有中文内容&#xff0c;我想在切换语言的时候&#xff0c;跟着这个翻译也实时切换&#xff0c;就可以使用computed计算属性实现。 把name改成下面的样子&#xff1a; name: computed(() > t(pad.regularMode)), 就可以…

基于SpringBoot的人才公寓管理系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码、微信小程序源码 精品专栏&#xff1a;…

一个包含了超过 200 个实用脚本的 Python 脚本库,如文件管理、网络操作、图像处理、文本处理等

前言 在日常的工作和生活中&#xff0c;我们经常会遇到一些重复性的任务&#xff0c;如文件管理、网络cao作、图像处理、文本处理等。这些任务虽然简单&#xff0c;但如果频繁手动cao作&#xff0c;不仅耗时耗力&#xff0c;还容易出错。 现有的软件虽然能处理一部分问题&…

Vue2+Univer 环境搭建

node js 版本 16.32 参考文档&#xff1a; Vue2Univer实现可编辑Excel_vue univer-CSDN博客 https://univer.ai/guides/sheet/getting-started/quickstart 实现步骤&#xff1a; 1、包里面直接写这些 "riophae/vue-treeselect": "0.4.0","univ…

基于深度学习的图像修复系统设计与实现(PyQt5、CodeFormer ffhq-dataset数据集)

&#x1f497;博主介绍&#x1f497;&#xff1a;✌在职Java研发工程师、专注于程序设计、源码分享、技术交流、专注于Java技术领域和毕业设计✌ 温馨提示&#xff1a;文末有 CSDN 平台官方提供的老师 Wechat / QQ 名片 :) Java精品实战案例《700套》 2025最新毕业设计选题推荐…

Matlab 车牌识别技术

1.1设计内容及要求&#xff1a; 课题研究的主要内容是对数码相机拍摄的车牌&#xff0c;进行基于数字图像处理技术的车牌定位技术和车牌字符分割技术的研究与开发&#xff0c;涉及到图像预处理、车牌定位、倾斜校正、字符分割等方面的知识,总流程图如图1-1所示。 图1-1系统总…

为什么自动化测试落地这么难?

最近一直在想一个问题&#xff0c;就是自动化测试落地为什么这么难&#xff1f; 想要找到原因首先我们要明确实施自动化测试的目的&#xff0c;价值&#xff0c;以及要解决的问题是什么&#xff1f;然后我们可以再进一步分析为什么自动化测试很难落地&#xff1f; 实施自动化…

数据采集与数据分析:数据时代的双轮驱动

“在当今这个数据驱动的时代&#xff0c;信息已成为企业决策、市场洞察、科学研究等领域不可或缺的核心资源。而爬虫数据采集与数据分析&#xff0c;作为数据处理链条上的两大关键环节&#xff0c;它们之间相辅相成&#xff0c;共同构成了数据价值挖掘的强大引擎。” 爬虫数据采…

【js逆向专题】12.RPC技术

目录 一. websocket1. 什么是websocket2. websocket的原理3. websocket实现方式1. 客户端2.服务端3. 实际案例1. 案例目标2. 解析思路 二. RPC1. RPC 简介2.Sekiro-RPC1. 使用方法1. 执行方式2.客户端环境3.使用参数说明 2. 测试使用1. 前端代码2. SK API3.python调用代码 三.项…

AR模型时序预测——预测未来(含完整代码)

一、前言 随着数据科学的快速发展&#xff0c;利用自回归&#xff08;AR&#xff09;模型进行时序预测已成为一个热门话题。AR模型因其简洁有效&#xff0c;广泛应用于各类预测任务。本文将介绍一套基于Matlab的AR模型时序预测代码&#xff0c;重点在于如何通过历史数据预测未…

工业相机详解及选型

工业相机相对于传统的民用相机而言&#xff0c;具有搞图像稳定性,传输能力和高抗干扰能力等&#xff0c;目前市面上的工业相机大多数是基于CCD&#xff08;Charge Coupled Device)或CMOS(Complementary Metal Oxide Semiconductor)芯片的相机。 一&#xff0c;工业相机的分类 …

爬虫+数据保存

爬虫以及数据保存 这篇文章, 分享如何将爬虫爬到的数据, 保存到excel表格当中。 文章目录 1.安装保存数据的第三方库openpyxl并使用 2.爬虫加单表数据保存 3.爬虫加多表数据保存 4.实战 一、安装保存数据的第三方库openpyxl并使用 我们需要安装openpyxl的第三方库 安装…