代码随想录二刷——动规篇章

dp 动态规划篇



路径种数问题

63.不同路径 II

不同路径2
dp[i][j]表示到这个位置的走法,遍历到对应的obstacle位置如果是1,这个点就保持不变是0就行

def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:
        m,n = len(obstacleGrid), len(obstacleGrid[0])
        if obstacleGrid[0][0] == 1 or obstacleGrid[m-1][n-1]: return 0
        dp = [[0]*n for _ in range(m)]
        for j in range(n):
            if obstacleGrid[0][j] != 1: dp[0][j] = 1
            else: break
        for i in range(m):
            if obstacleGrid[i][0] != 1: dp[i][0] = 1
            else: break
        for i in range(1,m):
            for j in range(1,n):
                if obstacleGrid[i][j] == 1: continue
                else: dp[i][j] = dp[i-1][j]+dp[i][j-1]
        return dp[m-1][n-1]

拆分问题

343. 整数拆分

整数拆分
写的时候有点把一个整数拉开,去乘的感觉了,但是还是没写出来
怎么拉开去算,还需要一个算子j从1-i, 然后dp[i]就被拆成 j * (i-j), 一个个去试
背过吧还是

def integerBreak(self, n: int) -> int:
        dp = [0] * (n+1)
        dp[1] = 1
        for i in range(2,n+1):
            for j in range(1,i+1):
                dp[i] = max(dp[i], j*(i-j), j*dp[i-j])
        return dp[n]

96.不同的二叉搜索树

不同的二叉搜索树
怎么说呢,二叉搜索树是 如果不空 ,左<根<右
dp[i]就是i个结点的种数
然后用一个j,j -> 1-i分别去盘j当头的情况,也很好盘么,dp[i] += dp[j-1] * dp[i-j]
背过吧

def numTrees(self, n: int) -> int:
        dp = [0] * (n+1)     #dp[i]表示由i个结点组成的二叉搜索树有多少种
        dp[0] = 1
        for i in range(1,n+1):
            for j in range(1,i+1):
                dp[i] += dp[j-1] * dp[i-j]  # j 是1-i轮流做头结点,然后左边子树情况*右边子树情况
        return dp[n]

背包问题

0-1背包,最多选一个,选or不选

状态压缩之后的代码,遍历顺序固定

#  n, m  物品个数, 容积  
dp = [0] * (m+1)
for i in range(0, n):
	for j in range(m, weight[i]-1, -1):
		dp[j] = max(dp[j], dp[j - weight[i]] + value[i])

406.分割等和子集

dp[j]的数值一定是小于等于j的。
如果dp[j] == j 说明,集合中的子集总和正好可以凑成总和j,理解这一点很重要。
分割等和子集

1049. 最后一块石头的重量 II

最后一块石头的重量 II
解的所在的话,最后我们计算的dp[target], 就是target = sum // 2的容积内,最接近数组之和一半的数,由于我们是向下取整,另一半sum-dp[target], 一定稍微大一点;所以最后的解,sum-dp[target] - dp[tartget]

494.目标和

目标和
要填正负号,必定有整数部分,假设和为x;我们就设法找到填满容积为x的背包有多少种方法就行

def findTargetSumWays(self, nums: List[int], target: int) -> int:
        # 正数部分和是x, 负数部分和是sum-x, x-(sum-x) = target
        # dp[j]表示填满j容积有多少种方法
        if sum(nums) < abs(target) or (sum(nums)+target) % 2 != 0: return 0
        n, m = len(nums), (sum(nums)+target) // 2
        dp = [0] * (m+1)
        dp[0] = 1
        for i in range(n):
            for j in range(m, nums[i]-1, -1):
                dp[j] += dp[j-nums[i]]
        return dp[m]

解释一下dp[j]的推导,dp[j] += dp[j-nums[i]]
就是
不考虑nums[i]的情况下,填满容量为j的背包,有dp[j]种方法。

那么考虑nums[i]的话(只要搞到nums[i]),凑成dp[j]就有dp[j - nums[i]] 种方法。

例如:dp[j],j 为5,

已经有一个1(nums[i]) 的话,有 dp[4]种方法 凑成 dp[5]。
已经有一个2(nums[i]) 的话,有 dp[3]种方法 凑成 dp[5]。
已经有一个3(nums[i]) 的话,有 dp[2]种方法 凑成 dp[5]
已经有一个4(nums[i]) 的话,有 dp[1]种方法 凑成 dp[5]
已经有一个5 (nums[i])的话,有 dp[0]种方法 凑成 dp[5]
那么凑成dp[5]有多少方法呢,也就是把 所有的 dp[j - nums[i]] 累加起来。

474.一和零

一和零
这个容积有两个维度
dp[i][j]表示有i个0和j个1,最多能选几个
dp[i][j] = max(dp[i][j], dp[i-zeronum][j-onenum]+1) #不选,和 选

def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
        zeronum, onenum = 0, 0
        dp = [[0] * (n+1) for _ in range(m+1)]
        for str in strs:
            zeronum = str.count('0')
            onenum = str.count('1')
            for i in range(m, zeronum-1, -1):
                for j in range(n, onenum-1, -1):
                    dp[i][j] = max(dp[i][j], dp[i-zeronum][j-onenum]+1)
        return dp[m][n]

背过吧

完全背包,每个物品可以选无限多次

完全背包和01背包不同之处就在于, 完全背包的内层遍历容积也要正序遍历
刚刚在复习01背包的时候,为了不让前面的物品被重复放入,倒序遍历容积;在完全背包可以重复放入,就正序遍历容积就行了

322.零钱兑换(用最少个数物品填满背包)

求怎么装能有的最大价值或者一个方案能最多选多少个物品的话

dp[j] = max(dp[j], dp[j-nums[i]]+val[i]/1)

如果说,求一个方案最少能用几个物品填满背包的话,

dp[j] = min(dp[j], dp[j-nums[i]]+1)

这道题代码

def coinChange(self, coins: List[int], amount: int) -> int:
        dp = [10001] * (amount+1)
        dp[0] = 0
        for c in coins:
            for j in range(c, amount+1):
                dp[j] = min(dp[j], dp[j-c]+1)
        return dp[amount] if dp[amount] != 10001 else -1

279.完全平方数 (用最少的物品填满背包)

279.完全平方数

518. 零钱兑换 II (组合的种数)

零钱兑换 II

如果说有多少种选法的话是组合数,dp[j]表示填满容积为j有多少种组合

dp[j] += dp[j-nums[i]]

并且dp[0] = 1表示说什么都不选也是一种

377. 组合总和 Ⅳ (排列的种数)

377. 组合总和 Ⅳ
组合的种数,就是不同顺序算一种
排列的种数,就是不同顺序的算多种

组合的种数,先遍历物品再遍历容积
排列的种数,先遍历容积再遍历物品
因为是完全背包,两者都要正序, 二者递推公式都是一样的

def combinationSum4(self, nums: List[int], target: int) -> int:
        n,m = len(nums), target
        dp = [0] * (m+1)
        dp[0] = 1
        for j in range(m+1):
            for i in range(n):
                if j >= nums[i]: dp[j] += dp[j-nums[i]]
        return dp[m]

进阶版的爬楼梯就是这个题的解法

139.单词拆分(能不能由字典里的单词组成这个字符串)

单词拆分
也把它看成一个完全背包,j也有一个容积的意味

def wordBreak(self, s: str, wordDict: List[str]) -> bool:
        dp = [False] * (len(s)+1)
        dp[0] = True
        for j in range(1,len(s)+1):
            for i in range(j): 
                if dp[i] and s[i:j] in wordDict:
                    dp[j] = True
        return dp[len(s)]

多重背包

01背包,每种物品一个,选还是不选
完全背包,每种物品无限多个,选或不选
多重背包的话,就是每件物品多了一个件数,我们在遍历的时候还有遍历一下件数

def test_multi_pack1():
    '''版本:改变遍历个数'''
    weight = [1, 3, 4]
    value = [15, 20, 30]
    nums = [2, 3, 2]
    bag_weight = 10

    dp = [0] * (bag_weight + 1)
    for i in range(len(weight)):
        for j in range(bag_weight, weight[i] - 1, -1):
            # 以上是01背包,加上遍历个数
            for k in range(1, nums[i] + 1):
                if j - k * weight[i] >= 0:
                    dp[j] = max(dp[j], dp[j - k * weight[i]] + k * value[i])

    print(" ".join(map(str, dp)))

if __name__ == '__main__':
    test_multi_pack1()

也可以把ki数组给展开,就是把物品数组给扩大,相应的weight和value数组加进重复的,然后用01背包

打家劫舍

打家劫舍1

打家劫舍
打家劫舍就是隔一个,当前这个不取和取,来一个最大值

def rob(self, nums: List[int]) -> int:
        if len(nums) < 2: return nums[0]
        dp = [0] * len(nums)
        dp[0] = nums[0]
        dp[1] = max(nums[0], nums[1])
        for i in range(2,len(nums)):
            dp[i] = max(dp[i-1], dp[i-2]+nums[i])
        return dp[len(nums)-1]

打家劫舍 II

打家劫舍2,就是首尾是一个圈,分别把不带首位和不带尾位的数组来做dp,找一个最大的结果
打家劫舍 II

def rob(self, nums: List[int]) -> int:
        if len(nums) == 1: return nums[0]
        if len(nums) == 2: return max(nums[0], nums[1])

        def func(nums):
            n = len(nums)
            dp = [0]*n
            dp[0], dp[1] = nums[0], max(nums[1], nums[0])
            for i in range(2,n):
                dp[i] = max(dp[i-1], dp[i-2]+nums[i])
            return dp[n-1]
        return max(func(nums[1:]), func(nums[:-1]))

打家劫舍3

这个就纯建议背过了
打家劫舍3

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def rob(self, root: Optional[TreeNode]) -> int:
        if not root: return 0
        if not root.left and not root.right: return root.val

        def dfs(node): #对于每一个结点我们选和不选有两种情况,我们拿一个元组去存(不选的值,选的值)
            if not node: return (0,0)
            left = dfs(node.left)
            right = dfs(node.right)
            
            #不选,选子树最大的
            val1 = max(left[0],left[1])+max(right[0],right[1])
            #选,本身结点的值加上不选左孩子,不选右孩子
            val2 = node.val + left[0] + right[0]
            return (val1,val2)
        res = dfs(root)
        return max(res)

打家劫舍4

打家劫舍4

股票问题

121.买卖股票的最佳时机

买卖股票的最佳时机
这道题就是找到任意两个数组元素之间的最大的正差,可以维护一个最小左值,res初始化为0,找一个最大的正差
1.维护最小左边界

def maxProfit(self, prices: List[int]) -> int:
        low = 10000
        res = 0
        for i in range(len(prices)):
            low = min(low, prices[i])
            res = max(res, prices[i]-low)
        return res

2.dp
这个题就是只能买卖一次
状态定义dp[i][0]第i天持有,dp[i][1]第i天不持有

def maxProfit(self, prices: List[int]) -> int:
        dp = [[0] * 2 for _ in range(len(prices))]
        dp[0][0] = -prices[0]
        dp[0][1] = 0
        for i in range(1,len(prices)):
            dp[i][0] = max(dp[i-1][0], -prices[i])
            dp[i][1] = max(dp[i-1][1], dp[i-1][0]+prices[i])
        return dp[len(prices)-1][1]

122. 买卖股票的最佳时机 II

122. 买卖股票的最佳时机 II

这个题就是说能反复买卖多次,但是一次最多持有一股
1.就是只收割正利润

def maxProfit(self, prices: List[int]) -> int:
        cnt = 0
        for i in range(1, len(prices)):
            w = prices[i] - prices[i-1]
            if w > 0: cnt += w
        return cnt 

2.dp
这个和上一个只能买卖一次的区别就在于,持有状态的状态转移

def maxProfit(self, prices: List[int]) -> int:
        m = len(prices)
        dp = [[0] * 2 for _ in range(m)]
        dp[0][0], dp[0][1] = -prices[0], 0
        for i in range(1, m):
            dp[i][0] = max(dp[i-1][0], dp[i-1][1]-prices[i]) #持有,上个时态就持有,上个时态不持有现在买
            dp[i][1] = max(dp[i-1][1], dp[i-1][0]+prices[i])
        return dp[m-1][1]
  1. 买卖股票的最佳时机含手续费
    买卖股票的最佳时机含手续费
    和买卖股票2 是一样的,就是持有的时候不止要支付price[i]还要支付手续费
def maxProfit(self, prices: List[int], fee: int) -> int:
        dp = [[0] * 2 for _ in range(len(prices))]
        dp[0][0] = -prices[0] - fee
        for i in range(1, len(prices)):
            dp[i][0] = max(dp[i-1][0], dp[i-1][1]-prices[i]-fee) #持有
            dp[i][1] = max(dp[i-1][1], dp[i-1][0] + prices[i]) #不持有
        return dp[-1][1]

123. 买卖股票的最佳时机 III

123. 买卖股票的最佳时机 III
最多可以买卖两次,这意味着可以买卖一次,可以买卖两次,也可以不买卖
所以说这个题每天有五个状态,
0 —— 没有操作
1 —— 第一次买入
2 —— 第一次卖出
3 —— 第二次买入
4 —— 第二次卖出

状态转移,要么是上一个状态就这样转移过来的,要么是做了买卖对应上一个状态转移
dp[i][0] = dp[i - 1][0]
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i])
dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] + prices[i])
dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i])
dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i])

def maxProfit(self, prices: List[int]) -> int:
        m = len(prices)
        dp = [[0] * 5 for _ in range(m)]
        dp[0][1], dp[0][3] = -prices[0], -prices[0]  #初始化第一次买和第二次买

        for i in range(1,m):
            dp[i][0] = dp[i-1][0]     #不做操作
            dp[i][1] = max(dp[i-1][1], dp[i-1][0]-prices[i]) #第一次买
            dp[i][2] = max(dp[i-1][2], dp[i-1][1]+prices[i]) #第一次卖
            dp[i][3] = max(dp[i-1][3], dp[i-1][2]-prices[i]) #第二次买
            dp[i][4] = max(dp[i-1][4], dp[i-1][3]+prices[i]) #第二次卖
        return dp[m-1][4]

188. 买卖股票的最佳时机 IV

买卖股票的最佳时机 IV
最多可以买卖k次
上一题抽象成规律,奇数买,偶数卖

def maxProfit(self, k: int, prices: List[int]) -> int:
        m = len(prices)
        if m == 0: return 0
        dp = [[0]*(2*k+1) for _ in range(m)]
        for j in range(1, 2*k, 2):
            dp[0][j] = -prices[0]
        for i in range(1,m):
            for j in range(0, 2*k-1, 2):
                dp[i][j+1] = max(dp[i-1][j+1], dp[i-1][j]-prices[i])   #奇数买
                dp[i][j+2] = max(dp[i-1][j+2], dp[i-1][j+1]+prices[i]) #偶数卖
        return dp[-1][2*k]

309.最佳买卖股票时机含冷冻期

最佳买卖股票时机含冷冻期
可以多次买卖,但是卖了之后,下一天不能买卖,进入冷冻期
这个也是挺难的,有四个状态
持有:
状态1:之前就买了之后没有操作,和今天买

不持有:
状态2:两天前就卖了,昨天是冷冻期,今天保持不持有
状态3:今天卖出了股票

今天是冷冻期:
状态4:今天是冷冻期那么昨天一定是卖出

状态转移

dp[i][0] = max(dp[i - 1][0], max(dp[i - 1][3], dp[i - 1][1]) - prices[i]);  #持有
dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]); #不持有中的2
dp[i][2] = dp[i - 1][0] + prices[i];      #不持有中的3,今天卖掉
dp[i][3] = dp[i - 1][2];   #今天是冷冻期

代码

def maxProfit(self, prices: List[int]) -> int:
        dp = [[0] * 4 for _ in range(len(prices))]
        dp[0][0] = -prices[0]
        for i in range(1, len(prices)):
            dp[i][0] = max(dp[i-1][0], max(dp[i-1][3], dp[i-1][1])-prices[i])#持有股票 分为前一天就持有股票, 今天买(又分为前一天是冷冻期和保持不持有股票状态)
            dp[i][1] = max(dp[i-1][1], dp[i-1][3])#不持有股票状态中的保持卖出的状态,分为前一天就卖出,和前一天是冷冻期
            dp[i][2] = dp[i-1][0] + prices[i] #今天就卖出,前一天肯定是持有
            dp[i][3] = dp[i-1][2]   #冷冻期,前一天肯定是卖出
        return max(dp[-1][3], max(dp[-1][2], dp[-1][1]))

线性dp、区间dp

线性dp

300. 最长递增子序列

最长递增子序列
找连续递增子序列

def lengthOfLIS(self, nums: List[int]) -> int:
        n = len(nums)
        dp = [1] * n
        res = 1
        for i in range(1,n):
            for j in range(i):
                if nums[i] > nums[j]: dp[i] = max(dp[i], dp[j]+1)
            res = max(res, dp[i])
        return res

674. 最长递增子串

最长连续递增序列找连续递增子串

def findLengthOfLCIS(self, nums: List[int]) -> int:
        n = len(nums)
        dp = [1] * n
        for i in range(1,n):
            if nums[i] > nums[i-1]: dp[i] = dp[i-1] + 1   
        return max(dp)

1143.最长公共子序列

最长公共子序列
不相交的线,最长公共子序列的可视化版

def longestCommonSubsequence(self, text1: str, text2: str) -> int:
        m, n = len(text1), len(text2)
        dp = [[0] * (n+1) for _ in range(m+1)]
        for i in range(1,m+1):
            for j in range(1, n+1):
                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[-1][-1]

718.最长公共子串

最长公共子串

def findLength(self, nums1: List[int], nums2: List[int]) -> int:
        m,n = len(nums1), len(nums2)
        dp = [[0]*(n+1) for _ in range(m+1)]
        res = 0
        for i in range(1,m+1):
            for j in range(1,n+1):
                if nums1[i-1] == nums2[j-1]: dp[i][j] = dp[i-1][j-1]+1
                res = max(res, dp[i][j])
        return res

总结,子串的状态转移来源只可能有一种,子序列的话要多

53.最大子数组和

最大子数组和
贪心
维护一个cnt, 当cnt<0就给之前累加的和清零,然后再从下个开始累加

def maxSubArray(self, nums: List[int]) -> int:
        cnt = 0
        res = nums[0]
        for i in range(len(nums)):
            cnt += nums[i]
            if cnt > res: res = cnt
            if cnt < 0: cnt = 0
        return res

dp

def maxSubArray(self, nums: List[int]) -> int:
        dp = [0] * len(nums)
        dp[0] = nums[0]
        res = nums[0]
        for i in range(1,len(nums)):
            dp[i] = max(dp[i-1]+nums[i], nums[i])
            res = max(res, dp[i])
        return res

编辑距离

392.判断子序列

判断子序列
dp[i][j]定义为以下标i-1, j-1为结尾的字符串S 和 字符串T 的相同子序列的长度

if (s[i - 1] == t[j - 1]),
那么dp[i][j] = dp[i - 1][j - 1] + 1;,因为找到了一个相同的字符,相同子序列长度自然要在dp[i-1][j-1]的基础上加1

if (s[i - 1] != t[j - 1]),
此时相当于t要删除元素,t如果把当前元素t[j - 1]删除,那么dp[i][j] 的数值就是 看s[i - 1]与 t[j - 2]的比较结果了,即:dp[i][j] = dp[i][j - 1];

def isSubsequence(self, s: str, t: str) -> bool:
        m, n = len(s), len(t)
        dp = [[0] * (n+1) for _ in range(m+1)]
        for i in range(1,m+1):
            for j in range(1,n+1):
                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.不同的子序列

不同的子序列
dp[i][j]:以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]。

2.状态转移

这一类问题,基本是要分析两种情况

s[i - 1] 与 t[j - 1]相等
s[i - 1] 与 t[j - 1] 不相等

1) 当s[i - 1] 与 t[j - 1]相等时,dp[i][j]可以有两部分组成:

一部分是用s[i - 1]来匹配,那么个数为dp[i - 1][j - 1]。
一部分是不用s[i - 1]来匹配,个数为dp[i - 1][j]。

dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];

例如: s:bagg 和 t:bag ,s[3] 和 t[2]是相同的,但是字符串s也可以不用s[3]来匹配,即用s[0]s[1]s[2]组成的bag。

当然也可以用s[3]来匹配,即:s[0]s[1]s[3]组成的bag。

2) 当s[i - 1] 与 t[j - 1]不相等时
dp[i][j]只有一部分组成,不用s[i - 1]来匹配,即:dp[i - 1][j]

3.base case

从递推公式dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]; 和 dp[i][j] = dp[i - 1][j]; 中可以看出dp[i][0] 和dp[0][j]是一定要初始化的

dp[0][j]一定都是0,s如论如何也变成不了t

dp[i][0] 表示:以i-1为结尾的s可以随便删除元素,出现空字符串的个数。

那么dp[i][0]一定都是1,因为也就是把以i-1为结尾的s,删除所有元素,出现空字符串的个数就是1。

dp[0][0]应该是1,空字符串s,可以删除0个元素,变成空字符串t。

4.遍历顺序和解的位置

正序就行,解在dp[m][n]

def numDistinct(self, s: str, t: str) -> int:
        m, n = len(s), len(t)
        dp = [[0] * (n+1) for _ in range(m+1)]
        for i in range(m+1):
            dp[i][0] = 1
        for j in range(1, n+1):
            dp[0][j] = 0
        
        for i in range(1, m+1):
            for j in range(1, n+1):
                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]
        return dp[m][n]

583.两个字符串的删除操作

两个字符串的删除操作

这个题就是编辑距离,可以增 可以删 的情况 少了一种替换的情况

def minDistance(self, word1: str, word2: str) -> int:
        m, n = len(word1), len(word2)
        dp = [[0] * (n+1) for _ in range(m+1)]
        for i in range(m+1):
            dp[i][0] = i
        for j in range(n+1):
            dp[0][j] = j
        
        for i in range(1,m+1):
            for j in range(1, n+1):
                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[m][n]

72.编辑距离

编辑距离
word1[i-1] == word2[j-1],相等直接转化
word1[i-1] != word2[j-1], 增删改,操作+1

def minDistance(self, word1: str, word2: str) -> int:
        m, n = len(word1), len(word2)
        dp = [[0] * (n+1) for _ in range(m+1)]
        for i in range(m+1):
            dp[i][0] = i
        for j in range(n+1):
            dp[0][j] = j
        
        for i in range(1,m+1):
            for j in range(1, n+1):
                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[m][n]

区间dp

647.回文子串数目

回文子串
1.状态定义

布尔类型的dp[i][j]:表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]为true,否则为false。

2.状态转移

整体上是两种,就是s[i]与s[j]相等,s[i]与s[j]不相等这两种。

当s[i]与s[j]不相等,那没啥好说的了,dp[i][j]一定是false。

当s[i]与s[j]相等时,这就复杂一些了,有如下三种情况

情况一:下标i 与 j相同,同一个字符例如a,当然是回文子串
情况二:下标i 与 j相差为1,例如aa,也是回文子串
情况三:下标:i 与 j相差大于1的时候,例如cabac,此时s[i]与s[j]已经相同了,我们看i到j区间是不是回文子串就看aba是不是回文就可以了,那么aba的区间就是 i+1 与 j-1区间,这个区间是不是回文就看dp[i + 1][j - 1]是否为true。

3.base case

这道题开的数组是高宽都是n的,没有特殊的base case
初始化全为False就行

4.遍历顺序以及解的位置

由dp[i+1][j-1], 遍历顺序i是反序的,j是正序的

解的位置就是计数器的值

class Solution:
    def countSubstrings(self, s: str) -> int:
        n = len(s)
        dp = [[False] * n for _ in range(n)]
        cnt = 0
        for i in range(n-1, -1, -1):
            for j in range(i, n):
                if s[i] != s[j]: dp[i][j] = False
                else:
                    if j - i <= 1:
                        dp[i][j] = True
                        cnt += 1
                    elif dp[i+1][j-1]:
                        dp[i][j] = True
                        cnt += 1
        return cnt

516.最长回文子序列

最长回文子序列

这道题和上面的不同在于这道题是子序列,不需要连续
第二个区别是求的最长的,我们可以把dp数组的状态定义为长度

1.状态定义

dp[i][j]:字符串s在[i, j]范围内最长的回文子序列的长度为dp[i][j]。

这道题代码里我开的还是n行n列的数组,正好映射到s的下标那种

2.状态转移

在判断回文子串的题目中,关键逻辑就是看s[i]与s[j]是否相同

如果s[i]与s[j]相同,那么dp[i][j] = dp[i + 1][j - 1] + 2;

但是这题来说,j可不是从i开始,子串那题,我们j是从i开始,是因为有 a 这种单个字母需要被判回文子串的情况,而这个需要统计的是长度;我们j从i+1开始遍历

如果s[i]与s[j]不相同,说明s[i]和s[j]的同时加入 并不能增加[i,j]区间回文子串的长度,那么分别加入s[i]、s[j]看看哪一个可以组成最长的回文子序列。

加入s[j]的回文子序列长度为dp[i + 1][j]。

加入s[i]的回文子序列长度为dp[i][j - 1]。

那么dp[i][j]一定是取最大的,即:dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);

3.base case

由于上面我说的i j不可能重合,所以这题我们需要初始化dp[i][i] = 1, 根据定义单个的就是1, 当然了如果我们在代码里特判一下也行,那样的话j就可以从i开始遍历

其余的我们就初始化为0就行

4.遍历顺序以及解的位置

遍历顺序我们i倒序, j从i+1开始,正序遍历
解的位置就是dp[0][n], 从头到尾序列中最长回文序列是多长

class Solution:
    def longestPalindromeSubseq(self, s: str) -> int:
        n = len(s)
        dp = [[0] * n for _ in range(n)]
        for i in range(n):
            dp[i][i] = 1
        
        for i in range(n-1, -1, -1):
            for j in range(i+1, n):
                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][n-1]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值