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

news2024/10/26 16:29:51

文章目录

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

关注我

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

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

相关文章

ffmpeg视频滤镜:定向模糊-dblur

滤镜简述 dblur 官网链接 > https://ffmpeg.org/ffmpeg-filters.html#dblur 有一个模糊滤镜&#xff0c;我试了一下&#xff0c;没有感觉到它的特殊之处, 这里简单介绍一下。 滤镜使用 滤镜的参数 angle <float> ..FV.....T. set angle (from 0 t…

找不到包的老版本???scikit-learn,numpy,scipy等等!!

废话不多说 直接上链接了&#xff1a; https://pypi.tuna.tsinghua.edu.cn/simple/https://pypi.tuna.tsinghua.edu.cn/simple/https://pypi.tuna.tsinghua.edu.cn/simple/xxx/ 后面的这个xxx就是包的名字 大家需要什么包的版本&#xff0c;直接输进去就可以啦 举个栗子&#…

零基础Java第十期:类和对象(一)

目录 一、拜访对象村 1.1. 什么是面向对象 1.2. 面向对象与面向过程 二、类定义和使用 2.1. 类的定义格式 2.2. 类的定义练习 三、类的实例化 3.1. 什么是实例化 3.2. 类和对象的说明 四、this引用 4.1. 什么是this引用 4.2. this引用的特性 一、拜访对象村 在…

<项目代码>YOLOv8路面病害识别<目标检测>

YOLOv8是一种单阶段&#xff08;one-stage&#xff09;检测算法&#xff0c;它将目标检测问题转化为一个回归问题&#xff0c;能够在一次前向传播过程中同时完成目标的分类和定位任务。相较于两阶段检测算法&#xff08;如Faster R-CNN&#xff09;&#xff0c;YOLOv8具有更高的…

STMicroelectronics意法半导体车规芯片系列--亿配芯城(ICgoodFind)

在汽车电子领域&#xff0c;意法半导体的车规级芯片系列一直备受瞩目。亿配芯城作为电子元器件领域的可靠供应商&#xff0c;为大家介绍意法半导体车规级芯片系列的卓越之处。 意法半导体在车规级芯片领域拥有深厚的技术积累和丰富的经验。 其车规级芯片涵盖了多个关键领域&am…

8.three.js相机详解

8.three.js相机详解 1、 认识相机 在Threejs中相机的表示是THREE.Camera&#xff0c;它是相机的抽象基类&#xff0c;其子类有两种相机&#xff0c;分别是正投影相机THREE.OrthographicCamera和透视投影相机THREE.PerspectiveCamera&#xff1a; 正投影和透视投影的区别是&am…

【Java】常用方法合集

以 DemoVo 为实体 import lombok.Data; import com.alibaba.excel.annotation.ExcelProperty; import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;Data ExcelIgnoreUnannotated public class ExportPromoteUnitResult {private String id;ExcelProperty(value &qu…

贪心算法记录 - 下

135. 分发糖果 困难 n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。 你需要按照以下要求&#xff0c;给这些孩子分发糖果&#xff1a; 每个孩子至少分配到 1 个糖果。相邻两个孩子评分更高的孩子会获得更多的糖果。 请你给每个孩子分发糖果&#xff0c…

一文搞懂指令周期,机器周期和时钟周期

如图&#xff1a; 指令周期 > 机器周期 > 时钟周期 指令周期&#xff1a;一个指令&#xff0c;从取值到执行的全部周期。一个指令执行过程包括取值&#xff0c;译码和执行阶段。 机器周期&#xff1a;,取指、间址、执行和中断等 时钟周期&#xff1a;时钟频率的倒数&am…

什么样的JSON编辑器才好用

简介 JSON&#xff08;JavaScript Object Notation&#xff09;是一种轻量级的数据交换格式&#xff0c;易于人阅读和编写&#xff0c;同时也便于机器解析和生成。随着互联网和应用程序的快速发展&#xff0c;JSON已经成为数据传输和存储的主要格式之一。在处理和编辑JSON数据…

python查询并安装项目所依赖的所有包

引言 如果需要进行代码的移植&#xff0c;肯定少不了在另一台pc或者服务器上进行环境的搭建&#xff0c;那么首先是要知道在已有的工程的代码中用到了哪些包&#xff0c;此时&#xff0c;如果是用人工去一个一个的代码文件中去查看调用了哪些包&#xff0c;这个工作甚是繁琐。…

推荐一款三维数值建模软件:3DEC

3DEC是一种用于土壤、岩石、地下水、结构支撑和砖石等高级岩土工程分析的三维数值建模软件。该软件的数值公式基于离散元法(DEM)进行不连续建模。UDEC是它的二维版本。不连续材料是一组离散的块。不连续性充当着块之间的边界条件。允许块的大位移和旋转。常见的结构可以直接从地…

HTML5教程(一)- 网页与开发工具

1. 什么是网页 网页 基于浏览器阅读的应用程序&#xff0c;是数据&#xff08;文本、图像、视频、声音、链接等&#xff09;展示的载体常见的是以 .html 或 .htm 结尾的文件 网站 使用 HTML 等制作的用于展示特定内容相关的网页集合。 2. 网页的组成 浏览器 代替用户向服务…

Cout输出应用举例

Cout输出应用 在main.cpp里输入程序如下&#xff1a; #include <iostream> //使能cin(),cout(); #include <stdlib.h> //使能exit(); #include <sstream> #include <iomanip> //使能setbase(),setfill(),setw(),setprecision(),setiosflags()和res…

根据用户选择的行和列数据构造数据结构(跨行跨列)

方案一 这段代码的功能是根据用户选择的行和列数据&#xff0c;生成一个适合复制粘贴的字符串表格。代码会先按列的 id 从小到大排序&#xff0c;再根据行列的选择关系将数据按顺序填入表格&#xff0c;每行之间使用换行符分隔&#xff0c;每列之间使用制表符分隔。如果某一行…

【汇编语言】第一个程序(一)—— 一个源程序从写出到执行的过程

文章目录 前言1. 第一步&#xff1a;编写汇编源程序2. 第二步&#xff1a;对源程序进行编译连接3. 第三步&#xff1a;执行可执行文件中的程序结语 前言 &#x1f4cc; 汇编语言是很多相关课程&#xff08;如数据结构、操作系统、微机原理&#xff09;的重要基础。但仅仅从课程…

多元线性回归【正规方程/sklearn】

多元线性回归【正规方程/sklearn】 1. 基本概念1.1 线性回归1.2 一元简单线性回归1.3 最优解1.4 多元线性回归 2. 正规方程求最优解2.1 线性回归的损失函数&#xff08;最小二乘法&#xff09;2.2 推导正规方程2.3 正规方程练习2.4 使用sklearn计算多元线性方程2.5 凸函数 3. 线…

比例数据可视化(Python实现板块层级图绘制)——Instacart Market Basket Analysis

【实验名称】 实验一&#xff1a;绘制板块层级图 【实验目的】 1. 掌握数据文件读取 2. 掌握数据处理的方法 3. 实现板块层级图的绘制 【数据介绍】Instacart Market Basket Analysis 1. 数据说明 数据共有300 0000orders&#xff0c; 20 0000users&#xff0c; …

electron 打包

安装及配置 安装electron包以及electron-builder打包工具 # 安装 electron cnpm install --save-dev electron # 安装打包工具 cnpm install electron-builder -D 参考的package.json文件 其中description和author为必填项目 {"name": "appfile",&qu…

十一、数据库配置

一、Navicat配置 这个软件需要破解 密码是&#xff1a;123456&#xff1b; 新建连接》新建数据库 创建一个表 保存出现名字设置 双击打开 把id设置为自动递增 这里就相当于每一次向数据库添加一个语句&#xff0c;会自动增长id一次 二、数据库的增删改查 1、Vs 建一个控…