leetcode动态规划之子序列问题汇总

子序列问题这部分,目前写了12道题,分为如下三个模块:

  • 普通子序列问题
  • 编辑距离问题
  • 回文问题

普通子序列问题分为:

  • 最长递增子序列
  • 最长连续递增子序列
  • 最长重复子数组(需要减一)
  • 最长重复子序列(需要减一)
  • 最大子数组和

编辑距离问题分为:

  • 判断子序列(贪心简单)(需要减一)
  • 不同的子序列(需要减一)
  • 两个字符串的删除操作(需要减一)
  • 编辑距离(需要减一)

回文问题分为:

  • 回文子串(中心扩展法简单点)(这三个都是i+1和j-1,从坐下到右上)
  • 最长回文子串
  • 最长回文子序列

上面这些问题主要区分就是是否连续:

  • 如果题目已经说了,求连续的什么什么,那肯定时连续
  • 如果是数组,求子数组,那是连续
  • 如果是字符串,求子串,大概率是连续,得看看题目中有没有说明
  • 一般说子序列,无特殊说明的话,是不连续

连续与否的区别是什么:(多数情况下)

  • 连续:需要比较当前时刻的值和上一时刻的值,如满足,则怎么怎么样;一般没有else;那这样算出来的dp,不一定是那个时刻最大的,需要一个临时变量来比较下,暂存;
  • 不连续:需要每次比较的时候来判断,相等条件怎么怎么样,不等条件下怎么怎么样,每次都对dp做了最大值的处理,这样最后的dp理论上就是最大值;

上面这些问题,在写代码的时候,可以按照动规五部曲套,具体的写代码流程大致是这样:

  1. 先求出数组(或字符串)的长度
  2. 定义dp数组,如果是二维的话就把里面的也定义了
  3. 把需要初始化的初始化了,不写的就模式是0或false等等,需要看具体的题目
  4. 循环遍历处理,每道题的其实位置,循环终止位置可能有区别,需要看具体题目
  5. 递推公式也有所区别,常见的区别点在于:要不要起一个变量取最大值,相等的时候判断,不等的时候要不要处理等
  6. 返回结果是dp最后的还是临时变量的

300.最长递增子序列(不连续) - 解析 **
题目:给定一个数组,求最长子序列(不要求连续)长度
思路:求这种子序列、最长之类的问题,动态规划可能是一种解法(当然不一定能解)
思考:
1.dp数组定义
这道题的dp数组怎么定义。首先给定的是一个一维数组,那么先定义一个dp[i],那么含义是什么呢,就是下标为i的情况下最长子序列长度为dp[i];
2.递推公式
在推导公式的时候,要想一下dp[i]的定义,那么公式就是0 - i-1的每个dp中的最长的,再+1(这个时候才求出了dp[i], 那么dp[i]就是最长吗?不一定,所有要再max比较一下),所以递推公式为:if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);
那么这个max是什么意思呢,为什么要来这么一下呢?还是上面那个解释,对于下标i,我们要比较0 - i-1中最长的子序列,再+1
3.初始化
还是看dp的含义,标识每个位置的最长长度,所以都初始化为1
4.代码简述:先定义一个一位dp,都初始化为1;两次循环,内层的注意下遍历范围是0 - i-1,然后符合递增条件的,需要比较0 - i-1中的每个dp值+1,得到最大dp值;再起一个变量保存最大结果

func lengthOfLIS(nums []int) int {
    n := len(nums)
    dp := make([]int, n+1)
    for i := 0; i <= n; i++ {
        dp[i] = 1
    }
    res := 1
    for i := 1; i < n; i++ {
        for j := 0; j < i; j++ {
            if nums[i] > nums[j] {
                dp[i] = max(dp[i], dp[j]+1)
            }
        }
        res = max(res, dp[i])
    }
    return res
}
func max(a, b int) int {
    if a > b {
        return a
    }
    return b
}

674.最长连续递增子序列(连续)
题目:和上一题一样,就是要求了要连续
思路:
要求连续的话,会简单一点,因为不需要再内层的for循环来比较每个0 - i-1来得到最大值了,直接比较后来判断就行

func findLengthOfLCIS(nums []int) int {
    n := len(nums)
    dp := make([]int, n+1)
    for i := 0; i <= n; i++ {
        dp[i] = 1
    }
    res := dp[0]
    for i := 1; i < n; i++ {
        if nums[i] > nums[i-1] {
            dp[i] = dp[i-1]+1
        }    
        res = max(res, dp[i])
    }
    return res
}

func max(a, b int) int {
    if a > b {
        return a
    }
    return b
}

718.最长重复子数组(连续)
题目:两个数组,要最长的、公共的、重复的子数组(相当于上一道题的连续子序列)的长度
思路:两个数组了,应该就要定义二维dp数组了
1.dp数组
dp[i][j],代表以下标i-1结尾的数组1,和j-1结尾的数组2,最长的连续子数组
这里的-1是为了初始化方便以及写代码好写(从这道题开始,后面会有不少定义-1的),要注意定义了-1后,比较的时候也要用-1来比较
但是需要注意的是,在循环的时候,循环终止条件是<=n而不是n-1,同时最后返回的也是dp[m][n],这个情况一直到回文子串那里有变化
2.递推公式
不要求递增,只要连续且相等就行;
由于这道题只能从dp[i-1][j-1]递推出来,dp[i][j] = dp[i-1][j-1] + 1; 同时需要一个变量来存最大值

func findLength(nums1 []int, nums2 []int) int {
    m := len(nums1)
    n := len(nums2)
    dp := make([][]int, m+1)
    for i := 0; i <= m; i++ {
        dp[i] = make([]int, n+1)
    }
    dp[0][0] = 0
    res := 0
    for i := 1; i <= m; i++ {
        for j := 1; j <= n; j++ {
            if nums1[i-1] == nums2[j-1] {
                dp[i][j] = dp[i-1][j-1] + 1
            }
            res = max(res, dp[i][j])
        }
    }
    return res
}

func max(a, b int) int {
    if a > b {
        return a
    }
    return b
}

1142.最长公共子序列(不连续)
题目:要返回两个字符串的公共子序列最长长度
思路:和上一道题很像,不要求递增,不要求连续;
1.dp数组
表示长度为[0,i-1]的字符串text1和长度为[0,j-1]的字符串text2,最长公共子序列长度为dp[i][j]
2.递推公式
因为不要求连续,所以判断两个字符串的-1下标要分为相等和不相等;
若相等,则dp[i-1][j-1] + 1; 若不相等(不相等为什么还要考虑呢),那就看看text1[0, i - 2]与text2[0, j - 1]的最长公共子序列 和 text1[0, i - 1]与text2[0, j - 2]的最长公共子序列,取最大的,即dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
最后的dp就是结果
疑问:为什么这个就要考虑不相等,为什么上一个就要新开一个变量来存最后的结果;
因为上一道题要求了连续,连续的话dp[i][j]只能由dp[i-1][j-1]来推导出来;而本题不要求连续,可以由dp[i-1][j-1] dp[i-1][j] dp[i][j-1] 都是可以推导的

func longestCommonSubsequence(text1 string, text2 string) int {
    m := len(text1)
    n := len(text2)
    dp := make([][]int, m+1)
    for i := 0; i <= m; i++ {
        dp[i] = make([]int, n+1)
    }
    dp[0][0] = 0 // 其实上面已经初始化为0了,不需要再初始化一遍
    for i := 1; i <= m; i++ {
        for j := 1; j <= n; 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[m][n]
}
func max(a, b int) int {
    if a > b {
        return a
    }
    return b
}

53.最大子数组和(连续)
题目:一个数组,求最大的连续子数组,返回和
思考:
1.dp数组:
dp[i]:包括下标i(以nums[i]为结尾)的最大连续子序列和为dp[i]。
2.递推公式
遍历一遍数组,对于每个值,有两种情况,要不就是将这个值加到之前的dp[i-1]上,要不就是从这个值开始,重新开始;至于选择哪种,直接比较取两者中大的那个就可以;
当然得到此时的dp[i]后,需要顺便看一下是不是最大值,就是用一个变量存一下;
3.初始化
这会就不能初始化为0了,而是nums[0]
这道题为什么要一个变量存一下呢,因为dp[i]只是表示这个位置为结尾的最大值,而不是整体的

func maxSubArray(nums []int) int {
    n := len(nums)
    dp := make([]int, n+1)
    dp[0] = nums[0]
    res := nums[0]
    for i := 1; i < n; i++ {
        dp[i] = max(dp[i-1] + nums[i], nums[i])
        res = max(res, dp[i])
    }
    return res
}
func max(a, b int) int {
    if a > b {
        return a
    }
    return b
}

从这里开始是编辑距离问题

392.判断子序列
题目:两个字符串,一个s是否为另一个t的子序列(不连续)
思考:根据上面的规律,不连续的可能不用变量存最大值,且每个值都需要处理
1.确定dp数组
dp[i][j]:以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度为dp[i][j],注意定义的时候先定义成长度
2.递推公式
不连续的话:
if (s[i - 1] == t[j - 1]),那么dp[i][j] = dp[i - 1][j - 1] + 1;
if (s[i - 1] != t[j - 1]),那么由于s <=t,dp[i][j] = dp[i][j - 1]

func isSubsequence(s string, t string) bool {
    m := len(s)
    n := len(t)
    dp := make([][]int, m+1)
    for i := range dp {
        dp[i] = make([]int, n+1)
    }
    for i := 1; i <= m; i++ {
        for j := 1; j <= n; 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]
            }
        }
    }
    return dp[m][n] == len(s)
}

115.不同的子序列
题目:两个字符串s和t,求s的子序列(不连续)中t的个数
思考:不连续,用下上面的方法,可以理解为s中有多少个t
1.dp数组
dp[i][j]:以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]
2.递推公式
和上面的一样,考虑到不连续,需要两个数组中的每两个-1来进行比较
若相等,dp[i][j]又需要分为两个部分:
第一部分,s[i - 1] 与 t[j - 1]这两个值相等了,将相当于假设没有这两个值的话的dp值,就是dp[i-1][j-1](也就是用s[i-1]);
第二部分,就是不用s[i-1],不用的话就是dp[i-1][j](即s[i-2]结尾,t[i-1]结尾)
最后二者相加,就是最后的递推公式
若不相等,就是上面的不用s[i-1],即公式dp[i-1][j]
3.初始化
根据上面的递推公式,dp[i][j]可以由dp[i-1][j-1] 和 dp[i-1][j]方向推到过来,也就是上方和左上方,那么第一行就需要初始化,这一行是某个字符串出现空字符串的个数,所以初始化为1

func numDistinct(s string, t string) int {
    m := len(s)
    n := len(t)
    dp := make([][]int, m+1)
    for i := range dp {
        dp[i] = make([]int, n+1)
    }

    // 初始化dp数组
    for i := range dp {
        dp[i][0] = 1
    }

    for i := 1; i <= m; i++ {
        for j := 1; j <= n; j++ {
            if s[i-1] == t[j-1] {
                dp[i][j] = dp[i-1][j-1] + dp[i-1][j] // 因为要求有多少种场景,所以是加和,后半部分是那种bagg的case
            } else {
                dp[i][j] = dp[i-1][j]
            }
        }
    }
    return dp[m][n]
}

583.两个字符串的删除操作
题目:两个字符串,都可以删除,最后求两个字符串相等的最小步数
思考:上一道题,其实没用的那个场景,相当于删除,但是只删除一个字符串的;本题是可删除两个字符串的;这个就不是子序列是,相当于连续的子串?
1.dp数组
dp[i][j]表示,以i-1结尾的字符串word1和以j-1结尾的字符串word2,要想达到相等,需要删除元素的最少次数
2.递推公式
当word1[i - 1] 与 word2[j - 1]相同的时候,dp[i][j] = dp[i - 1][j - 1];为啥等于-1呢,就相当于,两个字符串这个位置的值是相同的,那么删了也是一样的;
当word1[i - 1] 与 word2[j - 1]不相同的时候,就会有如下三种场景:
删掉word[i-1],则有dp[i-1][j] + 1
删掉word[j-1], 则有dp[i][j-1] + 1
都删掉,则有dp[i - 1][j - 1] + 2
后面这个1代表1步,2代表2步
3.初始化
这道题是要初始化的,根据dp的定义,表示word2是一个空字符串,那么以i-1结尾的word1,需要删除0-i-1共i个元素(i步),才能成为空串
再次思考:本题是不相等的时候又分了情况,上一题是相同的时候分了情况,是因为这道题求的是最小,上一道求的是个数,就需要符合各种情况都考虑下

func minDistance(word1 string, word2 string) int {
    m := len(word1)
    n := len(word2)
    dp := make([][]int, m+1)
    for i := range dp {
        dp[i] = make([]int, n+1)
    }

    // 初始化第一行
    for i := range dp {
        dp[i][0] = i
    }
    for j := range dp[0] {
        dp[0][j] = j
    }

    for i := 1; i <= m; i++ {
        for j := 1; j <= n; j++ {
            if word1[i-1] == word2[j-1] {
                dp[i][j] = dp[i-1][j-1]
            } else {
                dp[i][j] = min(min(dp[i-1][j] + 1, dp[i][j-1] + 1), dp[i-1][j-1] + 2)
            }
        }
    }
    return dp[m][n]
}

func min(a, b int) int {
    if a < b {
        return a
    }
    return b
}

72.编辑距离
题目:两个字符串,可以增删改,变成另一个,求最小步数
思考:
1.dp数组
表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]
2.递推公式
相等的情况,同上一道题=dp[i-1][j-1]
不等的情况,就是分别删(两个字符串)和替换,三者取最小值
3.初始化的逻辑同上

func minDistance(word1 string, word2 string) int {
    m := len(word1)
    n := len(word2)
    dp := make([][]int, m+1)
    for i := range dp {
        dp[i] = make([]int, n+1)
    }

    // 初始化
    for i := range dp {
        dp[i][0] = i
    }
    for j := range dp[0] {
        dp[0][j] = j
    }
    
    for i := 1; i <= m; i++ {
        for j := 1; j <= n; j++ {
            if word1[i-1] == word2[j-1] {
                dp[i][j] = dp[i-1][j-1]
            } else {
                dp[i][j] = min(min(dp[i-1][j] + 1, dp[i][j-1] + 1), dp[i-1][j-1] + 1)
            }
        }
    }
    return dp[m][n]
}

func min(a,b int) int {
    if a < b {
        return a
    }
    return b
}

下面是回文问题:

647 回文子串

题目:判断一个字符串中回文子串的个数
解析:首先这道题用动态规划解,其实不太好想,题目虽然是个字符串,但需要定义一个二维数组dp[i][j],表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]为true,否则为false。
这里有两个注意点,第一个是改成了true or false,第二是二维数组,且h一定大于i,另外根据图来看,dp[i][j]是不是回文是dp[i+1][j-1]推断出来的,相当于数组的左下角

func countSubstrings(s string) int {
    dp := make([][]bool, len(s) + 1)
    for i := range dp {
        dp[i] = make([]bool, len(s) + 1)
    }
    res := 0
    for i := len(s)-1; i >= 0; i-- { // 注意这里下面都是n-1,不然会越界,因为这个dp的定义不是i-1和j-1了
        for j := i; j <= len(s)-1; j++ {
            if s[i] == s[j] {
                if j - i <= 1 {
                    dp[i][j] = true
                    res++
                } else if dp[i+1][j-1] {
                    dp[i][j] = true
                    res++
                }
            }
        } 
    }
    return res
}
5 最长回文子串

题目:求给定字符串的最长回文子串是哪个
解析:和上一道题的思路基本一样,只不过上一题是求个数,这个是定义个变量求最值

func longestPalindrome(s string) string {
    n := len(s)
    maxLen := 0
    leftIndex := 0
    dp := make([][]bool, n+1)
    for i := 0; i <= n; i++ {
        dp[i] = make([]bool, n+1)
        dp[i][i] = true
    } 
    for i := n-1; i >= 0; i-- {
        for j := i; j <= n-1; j++ {
            if s[i] == s[j] {
                if j - i <= 1 || dp[i+1][j-1] == true {
                    dp[i][j] = true
                    if maxLen < j - i {
                        maxLen = j - i
                        leftIndex = i
                    }
                }
            }
        }
    }
    return s[leftIndex: leftIndex + maxLen + 1]

}
516 最长回文子序列

题目:求最长回文子序列的长度
解析:

func longestPalindromeSubseq(s string) int {
    n := len(s)
    dp := make([][]int, n+1)
    for i := 0; i <= n; i++ {
        dp[i] = make([]int, n+1)
        dp[i][i] = 1
    }
    for i := n-1; i >= 0; i-- {
        for j := i+1; j <= n-1; j++ { // 这里必须要+1,因为这道题求的是长度,i==j的时候上面已经初始化为1了
            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]) //求的是长度,都不相等了,就不需要+1了
            }
        }
    }
    return dp[0][n-1] // 右上角
}
func max(a, b int) int {
    if a > b {
        return a
    }
    return b
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值