【力扣】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 语言代码实现
- 十四、结论
一、引言
在LeetCode的算法题中,子序列相关的问题广泛存在,例如“最长递增子序列”、“最长公共子序列”等。这些问题虽然具体题意不同,但本质上都可通过动态规划(Dynamic Programming, DP)来解决。动态规划是一种通过分解子问题并重用子问题解的算法技术,非常适用于这类具有“最优子结构”的问题。
本篇文章将以多个典型的LeetCode题目为例,逐步提炼出解决子序列问题的通用方法论,帮助您系统掌握动态规划解决子序列问题的核心技巧。
力扣(LeetCode)题目的链接列表:
- 300. 最长递增子序列
- 674. 最长连续递增序列
- 718. 最长重复子数组
- 1143. 最长公共子序列
- 53. 最大子序和
- 392. 判断子序列
- 115. 不同的子序列
- 583. 两个字符串的删除操作
- 72. 编辑距离
- 647. 回文子串
- 516. 最长回文子序列
二、动态规划方法论深度提炼
动态规划解题方法通常包含以下四个核心步骤:
- 确定子问题:识别问题的子问题如何划分,并使得较小问题的解可复用于较大问题的解。
- 构建最优子结构:确保通过每个子问题的最优解,组合得到整个问题的最优解。
- 定义状态转移方程:状态转移方程是动态规划的核心,通过它定义出从一个子问题解递推到另一个子问题解的规则。
- 初始化和递归顺序:设置初始值,明确递归或迭代顺序。
子序列问题的通用解法模式
在LeetCode的子序列问题中,动态规划的具体实现有以下共通模式:
- dp数组的定义:
dp
数组一般用来表示问题最优解中的特定属性,例如“最长子序列长度”、“出现次数”、“和”等。dp数组的定义决定了每个子问题的状态表示。 - 状态转移方程设计:
- 对于“最长递增子序列”类问题,通常通过比较当前元素与前面元素的关系,递推得到最长子序列的长度。
- 对于“最长公共子序列”类问题,通常根据两个字符串的字符是否相等来递归或迭代。
- 对于“编辑距离”类问题,通常需要综合增删改三种操作的最小代价。
- 初始化与递归顺序:大部分问题中,dp数组会有边界初始值或特定的初始化设定,并采用自底向上或自顶向下的递归顺序。
三、通用方法论应用示例:最长递增子序列(LeetCode题目300)
题目描述:
题意:给定一个整数数组nums
,找到其中最长的严格递增子序列的长度。
示例:
输入: nums = [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长递增子序列之一是 [2,3,7,101],因此长度为 4。
解题思路:
- dp数组定义:我们定义
dp[i]
为以nums[i]
结尾的最长递增子序列的长度。 - 状态转移方程:对于每一个
i
,遍历0
到i-1
的所有元素j
,如果nums[j] < nums[i]
,则可以将nums[i]
接在以nums[j]
结尾的递增子序列后面,因此有dp[i] = max(dp[i], dp[j] + 1)
。 - 初始化:由于单个元素本身也是一个递增子序列,因此
dp
数组的每个元素初始值都设为1
。 - 递归顺序:从前往后遍历每个元素,依次计算以每个元素结尾的最长子序列长度。
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)来实现:
- dp数组定义:定义
dp[i]
为以nums[i]
结尾的最长连续递增子序列的长度。 - 状态转移方程:如果
nums[i] > nums[i-1]
,说明当前元素可以与前一个元素构成递增子序列,因此dp[i] = dp[i-1] + 1
。否则,dp[i]
重置为1
,表示以nums[i]
结尾的新起始子序列。 - 初始化:每个位置的递增序列至少包含自身一个元素,因此将所有
dp[i]
初始化为1
。 - 递归顺序:从左到右遍历数组,每次更新
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)
接下来,我们将介绍最长重复子数组问题,这一题与前面的最长递增序列问题有所不同,因为它涉及两个数组的匹配问题。
题目描述
给定两个整数数组nums1
和nums2
,返回两个数组中公共的、长度最长的重复子数组的长度。
示例
输入: nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7]
输出: 3
解释: 长度最长的重复子数组是 [3,2,1],长度为3。
解题思路:
- dp数组定义:我们定义
dp[i][j]
为在nums1
中以第i-1
位结尾和在nums2
中以第j-1
位结尾的最长重复子数组的长度。 - 状态转移方程:如果
nums1[i-1] == nums2[j-1]
,则dp[i][j] = dp[i-1][j-1] + 1
;否则,dp[i][j] = 0
。 - 初始化:
dp[i][0]
和dp[0][j]
初始化为0
,因为任一数组为空时没有公共子数组。 - 递归顺序:从左到右、从上到下填充二维
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
数组使用二维结构,以便对比nums1
和nums2
的每个位置组合。 - 每次找到一个相同元素时,延长重复子数组的长度;否则,将当前子问题解置零。
- 通过对
dp
数组的累积更新,记录并获取最长的公共子数组长度。
好的,接下来是最长公共子序列问题(LeetCode题目1143)的详细解析。
六、最长公共子序列(LeetCode题目1143)
题目描述
给定两个字符串text1
和text2
,返回这两个字符串的最长公共子序列的长度。如果不存在公共子序列,则返回0。子序列不要求在原字符串中是连续的,但顺序必须一致。
示例
输入: text1 = "abcde", text2 = "ace"
输出: 3
解释: 最长公共子序列是 "ace",因此返回3。
解题思路:
最长公共子序列问题与前面的最长重复子数组不同,它关注的是子序列(不连续)的匹配,而不是子数组(连续)的匹配。该问题可使用动态规划解决,步骤如下:
- dp数组定义:定义
dp[i][j]
为字符串text1
前i
个字符和字符串text2
前j
个字符的最长公共子序列的长度。 - 状态转移方程:如果
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])
。这个方程表示如果当前字符匹配,则可以在前一个状态基础上延长子序列;如果不匹配,则取两个可能情况的最大值。 - 初始化:如果任一字符串为空(即
dp[i][0]
或dp[0][j]
),则公共子序列长度为0
。 - 递归顺序:按行或按列填充二维
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。
解题思路:
该题目要求寻找最大子序和,属于“区间最大和”问题。可以通过动态规划来解决:
- dp数组定义:定义
dp[i]
为以nums[i]
结尾的最大子序和。 - 状态转移方程:若前一个位置的
dp[i-1]
值大于0,则将其加入当前元素形成新子数组;否则,当前元素单独作为新子序列的起点。因此,dp[i] = max(dp[i-1] + nums[i], nums[i])
。 - 初始化:
dp[0] = nums[0]
,因为以第一个元素结尾的最大子序和即为nums[0]
。 - 递归顺序:从左到右遍历数组,逐步累积计算最大和。
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)
最后,我们来看一个简单但巧妙的子序列问题,即如何判断一个字符串是否是另一个字符串的子序列。
题目描述
给定字符串s
和t
,判断s
是否是t
的子序列。可以假设两个字符串均仅包含英文小写字母。
示例
输入: s = "abc", t = "ahbgdc"
输出: true
解释: s 是 t 的子序列。
解题思路:
这个问题不需要完整的动态规划来解决,因为子序列要求顺序一致但不连续。我们可以通过双指针或DP判断每个字符的匹配情况。
- dp数组定义:使用双指针分别遍历
s
和t
,若s[i] == t[j]
,则移动s
的指针,否则仅移动t
的指针。 - 状态转移方程:无经典的DP状态转移方程,本题可用指针遍历简化实现。
- 初始化:双指针从字符串起始位置开始。
- 递归顺序:顺序遍历
s
和t
。
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" 的方式。
解题思路:
在该题中,我们需要统计t
在s
中作为子序列出现的次数。可以使用动态规划,通过分析字符是否匹配,递推出每个子问题的解。具体步骤如下:
- dp数组定义:定义
dp[i][j]
为s
的前i
个字符中t
的前j
个字符出现的次数。 - 状态转移方程:
- 当
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]
,表示字符不匹配的情况下,只能通过忽略当前字符来求解。
- 当
- 初始化:
dp[i][0] = 1
,即t
为空时,是任何字符串的子序列,子序列出现次数为1。 - 递归顺序:按行遍历
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)
接下来,我们将探讨两个字符串的删除操作这一问题,该题的本质是在两个字符串之间寻找相似部分,从而最小化删除次数。
题目描述
给定两个单词word1
和word2
,返回使得两个单词相同所需的最小步数。每步可以删除任意一个字符串中的一个字符。
示例
输入: word1 = "sea", word2 = "eat"
输出: 2
解释: 将 "sea" 变为 "ea" 并删除 "t",最少需要 2 步。
解题思路:
该题的核心在于最小化两个字符串的删除操作,具体实现可以通过找到最长公共子序列来进行。步骤如下:
- dp数组定义:定义
dp[i][j]
为将word1
的前i
个字符与word2
的前j
个字符变为相同所需的最小步数。 - 状态转移方程:
- 当
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
。
- 当
- 初始化:
dp[i][0] = i
,表示word2
为空时,需要删除word1
的所有字符;同理,dp[0][j] = j
。 - 递归顺序:从左到右逐步构建二维
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)
编辑距离是经典的动态规划问题之一,主要关注插入、删除和替换操作的最小代价。
题目描述
给定两个单词word1
和word2
,计算将word1
转换成word2
所需的最小操作数。可以对一个单词执行插入、删除或替换操作。
示例
输入: word1 = "horse", word2 = "ros"
输出: 3
解释: horse -> rorse (替换 'h' 为 'r') -> rose (删除 'r') -> ros (删除 'e')
解题思路:
该问题可通过动态规划解决,逐步最小化每次编辑的代价:
- dp数组定义:
dp[i][j]
为将word1
的前i
个字符变为word2
的前j
个字符所需的最小编辑次数。 - 状态转移方程:
- 如果
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
。
- 如果
- 初始化:当
word2
为空时,dp[i][0] = i
;同理,dp[0][j] = j
。 - 递归顺序:逐步填充整个
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"]。
解题思路:
这道题要求我们统计字符串中的回文子串数量,可以利用动态规划来判断每个子串是否为回文,并记录回文的数量。
- dp数组定义:定义
dp[i][j]
表示从字符s[i]
到s[j]
的子串是否是回文子串。若为回文子串,则dp[i][j] = true
,否则为false
。 - 状态转移方程:
- 当
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
。
- 当
- 初始化:长度为1的子串均为回文,因此对每个位置
i
,dp[i][i] = true
。 - 递归顺序:从右下角向左上角遍历
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" 是最长的回文子序列。
解题思路:
该问题可以使用动态规划通过递归判断字符的对称性来解决。
- dp数组定义:定义
dp[i][j]
为字符串s
从位置i
到j
的子串的最长回文子序列长度。 - 状态转移方程:
- 若
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])
。
- 若
- 初始化:对于每个位置
i
,dp[i][i] = 1
,即单个字符的回文长度为1。 - 递归顺序:从下到上、从左到右填充
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
数组定义、状态转移方程设计、初始化、递归顺序等方面,我们展示了如何构建并解决常见的子序列问题。动态规划不仅要求细致的代码实现,更强调对问题的整体分解与子问题的递推组合,这些方法论将有助于提升算法的解决能力。希望本文对您深入理解和掌握动态规划有所帮助!
相关文章:
【力扣】GO解决子序列相关问题
文章目录 一、引言二、动态规划方法论深度提炼子序列问题的通用解法模式 三、通用方法论应用示例:最长递增子序列(LeetCode题目300)Go 语言代码实现 四、最长连续递增序列(LeetCode题目674)Go 语言代码实现 五、最长重…...
Ubuntu20.04安装VM tools并实现主机和虚拟机之间文件夹共享
1、Ubuntu20.04安装VM tools 参考这个,很详细:Ubuntu 20.04 安装 VMwareTools 教程 2、实现主机与VMware虚拟机共享文件夹 设置共享文件夹参考:windows和虚拟机互传文件的三种方式 挂载操作参考:主机与VMware虚拟机共享文件夹&…...
Linux 学习笔记(十七)—— 文件系统
终极目标:理解 inode 和 软硬连接; 文件系统:Ext2; 文件 文件内容 文件属性; ——> 磁盘上存储的文件 存储的文件内容 存储的文件属性; Linux系统中:文件内容使用数据块存储,文件属性使用inode(固定…...
【计算机网络 - 基础问题】每日 3 题(五十八)
✍个人博客:https://blog.csdn.net/Newin2020?typeblog 📣专栏地址:http://t.csdnimg.cn/fYaBd 📚专栏简介:在这个专栏中,我将会分享 C 面试中常见的面试题给大家~ ❤️如果有收获的话,欢迎点赞…...
Netty入门基础:IO模型中BIO\NIO概念及区别【附演示代码】
文章目录 😀BIO💢实战demo 🌈NIO🏍Buffer核心属性核心方法 🎗Channel🎈Selector核心方法 🧨实战demo 🎨粘包与半包 😀BIO 传统IO模型,同步阻塞,每…...
vue2 使用环境变量
一. 在根目录下创建.env.xxx文件 .env 基础系统变量,无论何种环境,都可使用其中配置的值,其他环境中的变量会覆盖.env中的同名变量。 .env.development 开发环境 .env.production 生产环境 .env.staging 测试环境 二. 内容格式 vue2 使用是以…...
数据预处理
继续提取代码片段: 12. **导入iris数据集并查看前5行数据**: python from sklearn.datasets import load_iris iris load_iris() X iris.data print(iris数据集的维度为:, X.shape) print(iris数据集的前5行数据为:\n, X[:5]) …...
django宠物领养管理系统-计算机毕业设计源码26858
目录 1 绪论 1.1 选题背景与意义 1.2国内外研究现状 1.3论文结构与章节安排 2系统分析 2.1 可行性分析 2.2 系统流程分析 2.2.1 数据流程 3.3.2 业务流程 2.3 系统功能分析 2.3.1 功能性分析 2.3.2 非功能性分析 2.4 系统用例分析 2.5本章小结 3 系统总体设计 3…...
使用TeamViewer远程局域网内的两台电脑
有个场景,有人还不知道TV可以局域网操作,记录一下。 主要就是修改设置,将取消激活改为接受 然后输入受控端的ip即可...
GUI简介、Swing的常用组件、java程序的运行过程、class文件、JAR、runable_jar、双括号初始化
GUI简介 GUI:图形用户界面,在计算机中采用图形的方式显示用户界面 java的GUI开发 AWT:java最早推出的GUI编程开发包,界面风格跟随操作系统SWT:eclipse就是java使用SWT开发的Swing:在AWT的基础上扩充了功能…...
@Autowired和@Resource和getBean()区别
今天遇到一个对我来说很奇葩的错误,我想在Service中注入bean,我这里使用了Autowired和Resource都不能注入,导致初始化失败,使用了getBean()方法就可以注入。从来没有遇到过这个问题。后来我查询了一下,才明白了原理。我…...
Merlion笔记(四):添加一个新的预测模型
文章目录 1 模型配置类2 模型类3 运行模型:一个简单的例子4 可视化5 定量评估6 定义一个基于预测器的异常检测器 本文提供了一个示例,展示如何向 Merlion 添加一个新的预测模型,遵循 CONTRIBUTING.md 中的说明。建议在阅读本篇文章之前,先查…...
【论文阅读】ESRGAN
学习资料 论文题目:增强型超分辨率生成对抗网络(ESRGAN: Enhanced Super-Resolution Generative Adversarial Networks)论文地址:[1809.00219] ESRGAN:增强型超分辨率生成对抗网络代码:xinntao / ESRGAN&am…...
电脑异常情况总结
文章目录 笔记本无症状息屏黑屏 笔记本无症状息屏黑屏 🍎 问题描述: 息屏导致黑屏;依次操作计算机--》右键--》管理--》事件查看器--》Windows日志--》系统;从息屏到异常黑屏之间出现了很多错误,如下:事件…...
[项目详解][boost搜索引擎#1] 概述 | 去标签 | 数据清洗 | scp
目录 一、前言 二、项目的相关背景 三、搜索引擎的宏观原理 四、搜索引擎技术栈和项目环境 五、正排索引 VS 倒排索引--原理 正排索引 分词 倒排索引 六、编写数据去除标签和数据清洗模块 Parser 1.数据准备 parser 编码 1.枚举文件 EnumFile 2.去标签ParseHtml(…...
PL/I语言的起源?有C语言,有B语言和A语言吗?为什么shell脚本最开始可能有#!/bin/bash字样?为什么不支持嵌套注释?
PL/I语言的起源 在20世纪50~60年代,当时主流的编程语言是COBOL/FORTRAN/ALGOL等,IBM想要设计一门通用的编程语言,已有的编程语言无法实现此要求,故想要设计一门新语言,即是PL/I. PL/I是Programming Language/One的缩写…...
gin入门教程(3):创建第一个 HTTP 服务器
首先设置golang github代理,可解决拉取git包的时候,无法拉取的问题: export GOPROXYhttps://goproxy.io再查看自己的go版本: go version我这里的版本是:go1.23.2 linux/arm64 准备工作做好之后就可以进行开发了 3.…...
Vue+ECharts+iView实现大数据可视化大屏模板
Vue数据可视化 三个大屏模板 样式还是比较全的 包括世界地图、中国地图、canvas转盘等 项目演示: 视频: vue大数据可视化大屏模板...
el-table 表格设置必填项
el-table 表格设置必填项 要在 el-table 中集成 el-form 来设置必填项,并进行表单验证,可以使用 Element UI 提供的表单验证功能。下面是一个详细的示例,展示了如何在 el-table 中使用 el-form 来设置必填项,并进行验证。 示例代…...
vivo 轩辕文件系统:AI 计算平台存储性能优化实践
在早期阶段,vivo AI 计算平台使用 GlusterFS 作为底层存储基座。随着数据规模的扩大和多种业务场景的接入,开始出现性能、维护等问题。为此,vivo 转而采用了自研的轩辕文件系统,该系统是基于 JuiceFS 开源版本开发的一款分布式文件…...
Vue学习笔记(四)
事件处理 我们可以使用 v-on 指令 (通常缩写为 符号) 来监听 DOM 事件,并在触发事件时执行一些 JavaScript。用法为 v-on:click"methodName" 或使用快捷方式 click"methodName" 事件处理器的值可以是: 内联事件处理器࿱…...
发送短信,验证码
短信 注册阿里云的账号 开通短信服务 测试短信服务是否可用 导入jar <!-- 短信相关 --><dependency><groupId>com.aliyun</groupId><artifactId>aliyun-java-sdk-core</artifactId><version>4.6.0</version><…...
国内大语言模型哪家更好用?
大家好,我是袁庭新。 过去一年,AI大语言模型在爆发式增长,呈现百家争鸣之态。国内外相关厂商积极布局,并相继推出自家研发的智能化产品。 我在工作中已习惯借助AI来辅助完成些编码、创作、文生图等任务,甚至对它们产…...
OTP一次性密码、多因子认证笔记
文章目录 双因子认证(多因子认证)otp算法(ONE-TIME PASSWORD)otp算法大概分为几部 otp的机制服务端客户端(app端)两种主流算法otp流程图 otp是通用的吗 手机验证码天天在用,但是居然不知道这个是otp,伤自尊了,必须弄清原理。 先要知道几个概念…...
玉米生长阶段检测系统源码&数据集全套:改进yolo11-dysample
改进yolo11-DLKA等200全套创新点大全:玉米生长阶段检测系统源码&数据集全套 1.图片效果展示 项目来源 人工智能促进会 2024.10.24 注意:由于项目一直在更新迭代,上面“1.图片效果展示”和“2.视频效果展示”展示的系统图片或者视…...
【机器学习】决策树算法
目录 一、决策树算法的基本原理 二、决策树算法的关键概念 三、决策树算法的应用场景 四、决策树算法的优化策略 五、代码实现 代码解释: 在机器学习领域,决策树算法是一种简单直观且易于理解的分类和回归方法。它通过学习数据特征和决策规则&#…...
P2818 天使的起誓
天使的起誓 题目描述 Tenshi 非常幸运地被选为掌管智慧之匙的天使。在正式任职之前,她必须和其他新当选的天使一样要宣誓。 宣誓仪式是每位天使各自表述自己的使命,他们的发言稿放在 n n n 个呈圆形排列的宝盒中。这些宝盒按顺时针方向被编上号码 1…...
数字信号处理实验简介
数字信号处理(Digital Signal Processing,简称DSP)是电子工程、通信、计算机科学等领域中的一个重要分支,它涉及到对离散时间信号进行分析、处理和合成的理论和方法。数字信号处理课程的实验环节通常旨在帮助学生将理论知识应用于实际问题中,通过实践加深对DSP概念和技术的…...
Flask-SQLAlchemy 组件
一、ORM 要了解 ORM 首先了解以下概念。 什么是持久化 持久化 (Persistence),即把数据(如内存中的对象)保存到可永久保存的存储设备中(如磁盘)。持久化的主要应用是将内存中的数据存储在关系型的数据库中,…...
Could not retrieve mirrorlist http://mirrorlist.centos.org错误解决方法
文章目录 背景解决方法 背景 今天在一台新服务器上安装nginx,在这个过程中需要安装相关依赖,在使用yum install命令时,发生了以下报错内容: Could not retrieve mirrorlist http://mirrorlist.centos.org/?release7&archx8…...
制作视频用什么app/广东seo网站优化公司
Dice Loss 最先是在VNet 这篇文章中被提出,后来被广泛的应用在了医学影像分割之中。1、Dice系数与Dice LossDice系数是一种集合相似度度量函数,通常用于计算两个样本的相似度,取值范围在[0,1]:其中 |X∩Y| 是X和Y之间的交集&#…...
朝阳网站搭建公司/设计公司企业网站
/* (程序头部注释开始) * 程序的版权和版本声明部分 * Copyright (c) 2011, 烟台大学计算机学院学生 * All rights reserved. * 文件名称:设计一个工资类(Salary)--完成各个功能 * 作 者: 雷恒鑫 * 完成日期: 2012年03 月13日 * 版 本 号&…...
做网做网站建设/鹤壁seo推广
初级版轮播图,实现左右按钮切换图片,下方小点切换图片,简单的自动轮播 代码:(缺点,固定图片张数和宽度高度,每次用时都需要复制,代码累赘,多处功能不完善) …...
长春公司做网站/免费推广的平台
{"moduleinfo":{"card_count":[{"count_phone":1,"count":1}],"search_count":[{"count_phone":4,"count":4}]},"card":[{"des":"阿里技术人对外发布原创技术内容的最大平台&…...
营销型网站服务/台州seo网站排名优化
Windows7及以上系统用的时间长了,你会发现,打开一个文件,word,txt或者网页什么的,在对应窗口的任务栏上点击右键,会出现一大堆以前打开过相同类型的历史文件,点击任意一个,就能重开该…...
万网 网站/信息如何优化上百度首页公司
大家好,我是农民兄弟乔哥,信用分应该说是大家非常关心的一个事,每天发文章都会不小心踩坑,触碰到规章制度,信用分被扣,信用分就是你在头条里的一个重要的约束条件,当信用分低于60就会关闭所有权…...