Leetcode——动态规划

10.正则表达式的匹配
44.通配符的匹配

上述两个问题都属于完全背包问题。
正则表达式的匹配:关于这道题目,需要注意的是对于*符号,它会与前一个字符形成新的模式。因此,遇到 字符 + *符号,需要单独处理。
通配符的匹配:这道题目是正则表达式匹配的低配版。它不需要考虑前后字符之间的关联,只需要考虑边界条件就足够了。

53.最大子数组和

最开始的想法:我用计算给定数组的前缀和,并记录下前缀和的最大值和最小值,最后的结果是最大值 - 最小值。但是这种做法有一个问题:

  1. 无法保证前缀和最大值的下标是否大于前缀和最小值的下标

好了,假设你意识到了上述问题,于是你使用下标i和j,下标i表示前缀和最大值的索引,下标j表示前缀和最小值的索引。然后再对程序做一个改进:每次都要更新res。这还有一个问题:

  1. 上述更新i和j是根据dp[i] >= dp[k] 以及 dp[j] <= dp[k] 来更新的。这种更新方式基于一种假设:数组的前缀和是递增的。但是对于全负数序列,数组的前缀和是递减的,因此这种方法对于全负数序列不适用。

于是我们开始思考其他方法。忽然,灵机一动,假设数组dp[i]表示以下标i为结尾的最大子数组的和,那么此时更新方式有两种考量:

  1. 加入上一个数据形成的子数组序列,即dp[i - 1] + nums[i]
  2. 自己另起炉灶,敢为人先,从当前元素开始形成一个长度为1的子数组

那么这两种考量哪种更好呢?我们不知道,但我们永远取它们中间的最大值来更新dp[i]。

55.跳跃游戏

方法:一步一步跳,并更新可跳步数

62.不同路径
63.不同路径II

方法:二维动态规划。注意到状态的无后效性,我们可以使用滚动数组对其进行优化。

64.最小路径和

方法:二维动态规划。注意到状态的无后效性,我们可以使用滚动数组对其进行优化。

70.爬楼梯

方法:动态规划(而且是最简单的那种)。
不过嘞,这道题目还是有给我们一点启发:能用数组就不要使用vector

72.编辑距离

方法:动态规划。

动态规划数组dp[i][j]表示使得word1的前i个字符与word2的前j个字符相匹配所需要的最小操作数。 如果word1[i] = word2[j],那么dp[i][j] = dp[i - 1][j - 1] 否则,我们就选择增加一个字符、删除一个字符或者替换一个字符
其中,替换一个字符的动态规划方程:dp[i][j] = dp[i - 1][j - 1] + 1;
删除一个字符的动态规划方程:dp[i][j] = dp[i][j - 1] + 1
增加一个字符的动态规划方程: dp[i][j] = dp[i - 1][j] + 1; 因此,dp[i][j] = min(dp[i - 1][j - 1] ,dp[i - 1][j],dp[i][j - 1]) + 1;

87.扰乱字符串
我不会做

方法:该题目属于区间DP问题,可用动态规划来解决。
题解:宫水三叶题解

91.解码方法

回溯超时了,然后想到了动态规划。

动态规划方程:f[i] = f[i - 1] + f[i - 2](没考虑特殊情况)
考虑一位解码:f[i] += f[i - 1],要求s[i] != '0'
考虑两位解码:f[i] += f[i - 2],要求s[i - 1] != '0',10 * s[i - 1] + s[i] <= 26,而且i >= 2
1. 如果i < 2,且满足s[i - 1] != '0',10 * s[i - 1] + s[i] <= 26
\quad 1. s[i] == '0',f[i] = 1
\quad 2. s[i] != '0', f[i] = 2
2. 如果s[i] = s[i - 1] = '0',直接返回0.

97.交错字符串
我不会做

想到了要利用动态规划,但并不知道怎么做。

115.不同的子序列

动态规划转移方程
d p [ i ] [ j ] = { d p [ i − 1 ] [ j − 1 ] = d p [ i − 1 ] [ j ] s [ i ] = t [ j ] d p [ i − 1 ] [ j ] s [ i ] ≠ t [ j ] dp[i][j] = \begin{cases} dp[i - 1][j - 1] = dp[i - 1][j] & s[i]=t[j]\\ dp[i - 1][j] & s[i]\neq t[j] \end{cases} dp[i][j]={dp[i1][j1]=dp[i1][j]dp[i1][j]s[i]=t[j]s[i]=t[j]

118.杨辉三角
119.杨辉三角II
120.三角形的最小路径和
121.买卖股票的最佳时机
122.买卖股票的最佳时机II

若第i天持有股票,有两种情况:

  1. i - 1天持有
  2. i - 1天卖出,但第i天买入股票

若第i 天没有持有股票,也有两种情况:

  1. i - 1天没有持有
  2. i - 1天没有卖出,但第i天卖出

我们使用dp[i][0]表示第i天不持有股票时的收益,dp[i][1]表示第第i天持有股票时的收益,那么状态转移方程如下:
d p [ i ] [ 0 ] = m a x ( d p [ i − 1 ] [ 0 ] , d p [ i − 1 ] [ 1 ] + p r i c e s [ i ] ) d p [ i ] [ 1 ] = m a x ( d p [ i − 1 ] [ 1 ] , d p [ i − 1 ] [ 0 ] − p r i c e s [ i ] ) 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]) dp[i][0]=max(dp[i1][0],dp[i1][1]+prices[i])dp[i][1]=max(dp[i1][1],dp[i1][0]prices[i])

前后缀分解

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

方法:前后缀分解。在本题中,枚举两次交易的分界点,该分界点是第二次交易时股票的买入时机,此时答案就是max(左区间交易最大值 + 右区间交易最大值)。这样我们只需要求解左区间和右区间的交易最大值即可。

左区间的交易最大值:预处理
右区间的交易最大值:和买卖股票的最佳时机使用的方法相同,只不过我们需要逆序遍历,并维护一个存储股票价格最大值变量maxPrices。由于我们已知了第二次股票的买入时机,那么第二次交易的最大值就是 m a x P r i c e s − p r i c e s [ i ] maxPrices - prices[i] maxPricesprices[i]

139.单词拆分

方法:动态规划。 我第一次做的时候是把这道题当作区间动态规划问题来考虑的。看了题解,把这道题目当作完全背包问题来做时间复杂度和空间复杂度会更低。

  • 区间动态规划
    1. 动态规划数组dp[i][j]表示字符串s[i : j]是否可由字符串列表wordDict拼接而成。
    2. 动态规划转移方程:dp[i][j] = dp[i][j] || hash.count(s[i : j])||(dp[i][k] && dp[k][j]),其中, i < = k < j i <= k < j i<=k<j
    3. 为了快速查找单词s[i : j]是否出现在字符串列表,我们使用哈希表存储字符串列表中的单词,即unordered_set<string> hash(wordDict.begin(),wordDict.end())
  • 完全背包问题
    1. 动态规划数组dp[i]表示字符串s[0 : i]是否可由字符串列表wordDict拼接而成
    2. 动态规划转移方程:dp[i] = dp[i] || hash.count(s[0 : i])||(dp[k] && dp[i]),其中, 0 < = k < i 0 <= k < i 0<=k<i

152.乘积最大子数组

这道题目完全不会做

174. 地下城游戏

自右下到左上的动态规划。完全不会做。悲!

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

交易两次的题目我就不会做,交易 k k k次我就会做吗?可笑!!!

198.打家劫舍

动态规划数组
\quad resres[i]表示小偷走到第i个房间偷盗的最大金额。

动态规划方程
\quad 小偷走到第i个房间,有两种选择 \quad

  1. 不偷。此时res[i] = res[i - 1]; \quad
  2. 偷。此时res[i] = res[i - 2] + nums[i] \quad
    因此,动态规划方程是 r e s [ i ] = m a x ( r e s [ i − 1 ] , r e s [ i − 2 ] + n u m s [ i ] ) res[i] = max(res[i - 1],res[i - 2] + nums[i]) res[i]=max(res[i1],res[i2]+nums[i])

边界条件 \quad

  1. 假设只有一间屋子,偷盗的最大金额是nums[0] \quad
  2. 假设只有两件屋子,偷盗的最大金额是max(nums[0],nums[1])

一定要注意边界条件,我没注意边界条件导致解答错误

213.打家劫舍II

题目规定首尾房屋相连,这就意味着第一间房屋和最后一间房屋不可兼得。那么如何保证?假设有 N N N间房屋:

  1. 如果偷第一间房屋,可偷窃的房屋范围是 [ 1 , N − 1 ] [1,N-1] [1,N1]
  2. 如果不偷第一间房屋,可偷窃的房屋范围是 [ 2 , N ] [2,N] [2,N]

按照如上分析,我们有两大类偷窃方法,偷窃的最大金额就是这两类方法计算结果的最大值。

221. 最大正方形

完全不会做。暴风哭泣。。。

数位DP

233。数字1的个数

给定一个数字abcdefg,统计数字1的最佳做法是统计每一位数字1的个数。
例:
考虑d位,若

  1. d=0,在d位数字为1的数共有abc(000 ~ abc - 1,共abc个) × \times ×efg
  2. d=1,在d位数字为1的数共有 abc × \times × 1000 + + + efg + + + 1位
  3. d>1,在d位数字为1的数共有 (abc+1) × \times × efg

279.完全平方数

思路和算法:完全背包 或 区间DP

区间DP

n可以分解为更小的数字,这些数字必然落在区间 [ 0 , n ] [0,\sqrt{n}] [0,n ]内(不会证明)。
dp[i]表示和为i的最小平方数的最小数量,那么dp[j] = min(dp[i],dp[i - j * j] + dp[j * j])

完全背包

f[i][j]表示在只选择前i个完全平方数的条件下,和为j的完全平方数的最小数量。
对于第i个完全平方数(假设数值为t),我们有如下选择

  1. 0个,则f[i][j] = f[i - 1][j - 0 * t] + 0
  2. 1个,则f[i][j] = f[i - 1][j - 1 * t] + 1
  3. 2个,则f[i][j] = f[i - 1][j - 2 * t] + 2
    ⋅ ⋅ ⋅ \cdot\cdot\cdot
  4. k个,则则f[i][j] = f[i - 1][j - k * t] + k

根据以上分析,可以得到动态规划方程为dp[i][j] = min(dp[i - 1][j - k * t] + k),0 <= k <= j / k

300.最长递增子序列

思路和算法:

  1. 动态规划 + 爆搜。动态规划方程:dp[i] = max(dp[i] , dp[j] + 1),j = 0,1,2,...i - 1
  2. 动态规划 + 二分。

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

310.最小高度树

树形dp,属于我不会做的类型。不过要学习一下y总的代码,能给我很多启发。

312.戳气球

动态规划,区间DP。

322.零钱兑换

完全背包问题。

337.打家劫舍III

树形DP。


343.整数拆分

  1. 记忆化搜索。时间上击败1.62%,空间上击败5%左右
  2. 动态规划。
记忆化搜索

记忆化搜索利用自下而上的递归方法,遍历每一种所有可能的拆分方案,选择乘积最大的作为结果。同时记忆化搜索会用一个状态数组记录已遍历过的状态。具体步骤如下:

  1. 定义并使用INT_MIN记忆化数组memmem[n][k]表示整数n拆分为k个数字后对应的最大乘积。
  2. 枚举k的取值,2 <= k < n,对于每一个k,执行递归程序
  3. 递归程序
    1. 如果mem[n][k]不是INT_MIN,返回mem[n][k]
    2. 如果k == 1,直接把n复制给mem[n][k],并返回n
    3. n进行整数拆分,记拆分的第一个数字是i,若n - i >= k - 1,才继续递归,并利用递归返回的结果更新mem[n][k],更新方式为mem[n][k] = max(dfs(n - i,k - 1) * i,mem[n][k])
    4. 返回mem[n][k]

问题1:为什么是自下而上递归?
回答:因为自上而下递归无法记录记忆化数组

问题2:在递归程序中,为什么n - i >= k - 1,才继续递归?
回答:若n - i < k - 1,说明整数n - i不足以拆分为k - 1个数字,因此只有n - i >= k - 1才继续递归。

动态规划

动态规划方程: d p [ i ] = min ⁡ 1 ≤ j < i m a x ( d p [ i ] , j × ( i − j ) , j × d p [ i − j ] ) dp[i] = \min\limits_{1 \leq j < i} max(dp[i],j \times (i - j),j \times dp[i - j]) dp[i]=1j<iminmax(dp[i],j×(ij),j×dp[ij])


368.最大整除子集
这道题目太有意思了。话不多说,下面开始分析:

最大整除子集的特点

假设现在有一个最大整除子集nums,把nums按照从小到大的顺序排列,那么对于任意i,j,且i < j,都有nums[j] % nums[i] = 0,而且由于整除具有传递性,因此对于任意的k,k < i,都有nums[j] % nums[k] = 0

由上述最大整除子集的特点,在保证一个序列有序的前提下,要判断一个数字x属于哪个集合,只需要判断该数字能否被集合中的最大元素整除。因此,只需要维护一个集合中的最大元素即可。

思路和算法

题目要求求解最大整除子集的集合,因此还需要维护以下两个数组:

  1. 整除子集的长度,用len表示。len[i]表示以第i个元素结尾的整除子集的长度
  2. addr数组,addr[i]表示以第i个元素结尾的整除子集的上一个元素的地址(索引)。该数组借鉴了链表的思想,以此达到搜索最大整除子集的目的

分析到这一步,算法步骤已经很明确了:

  1. nums排序,保证序列有序

  2. 遍历nums,假设当前遍历到第i个元素

    1. 从第i个元素开始逆序遍历,假设遍历到的是第j个元素,那么有nums[i] > nums[j]
    2. nums[i]是否是nums[j]的倍数,且len[i] < len[j] + 1,更新len[i],addr[i]
  3. 遍历len数组,查找整除子集的最大长度及其最后一个元素的地址

  4. 利用addr和最后一个元素的地址搜索最大整除子集


375.猜数字大小II

方法;区间DP。

闫氏DP分析法

状态表示

集合:f[i][j]表示当选择的数字在区间[i,j]中时所有的猜法
属性:所有猜法当中所需要金额的最小值

状态计算

若当前猜测的数字是k( i ≤ k ≤ j i \le k \le j ikj),会有以下三种情况:

  1. 猜中,支付0
  2. 猜小了,支付的金额是f[k + 1][j] + k
  3. 猜大了,支付的金额是f[i][j - 1] + k

因此,当猜测的数字是k时,要保证能够赢得游戏的胜利,所需要的准备的金额是max(f[k + 1][j],f[i][j - 1]) + k

由于f[i][j]是所有猜法所需要金额的最小值,因此动态规划方程如下:
f [ i ] [ j ] = m i n ( f [ i ] [ j ] , m a x ( f [ k + 1 ] [ j ] , f [ i ] [ j − 1 ] ) + k ) , i ≤ k ≤ j f[i][j] = min(f[i][j],max(f[k + 1][j],f[i][j - 1]) + k), i \le k \le j f[i][j]=min(f[i][j],max(f[k+1][j],f[i][j1])+k),ikj


376.摆动序列
可以借鉴最长子序列和乘积最大子数组的做法,具体而言:

借鉴最长子序列和最大子数组的动态规划方法

题目要求求解最长子序列的长度,只不过附加了一个限制条件:最长子序列是摆动序列,那么可以借鉴一下最长子序列的求解方法。

状态表示

使用一个二维数组表示状态。具体而言:

集合
  1. dp[i][0]表示最后一个元素呈上升趋势的所有子序列
  2. dp[i][1]表示最后一个元素呈下降趋势的所有子序列
属性
  1. dp[i][0]表示所有子序列中最长子序列的长度
  2. dp[i][1]表示所有子序列中最长子序列的长度
状态计算

对于dp[i][0],由于子序列的最后一个元素要呈现上升趋势,那么倒数第二个元素是呈现下降趋势,因此它必然是由dp[j][1]转移而来, 0 ≤ j < i 0 \le j < i 0j<i

对于dp[i][1],由于子序列的最后一个元素要呈现下降趋势,那么倒数第二个元素是呈现上升趋势,因此它必然是由dp[j][0]转移而来, 0 ≤ j < i 0 \le j < i 0j<i

所以,状态计算如下:
d p [ i ] [ 0 ] = m a x ( d p [ i ] [ 0 ] , d p [ j ] [ 1 ] + 1 ) , 0 ≤ j < i dp[i][0] = max(dp[i][0],dp[j][1] + 1),0 \le j < i dp[i][0]=max(dp[i][0],dp[j][1]+1),0j<i
d p [ i ] [ 1 ] = m a x ( d p [ i ] [ 1 ] , d p [ j ] [ 0 ] + 1 ) , 0 ≤ j < i dp[i][1] = max(dp[i][1],dp[j][0] + 1),0 \le j < i dp[i][1]=max(dp[i][1],dp[j][0]+1),0j<i

时间复杂度

O ( n 2 ) O(n^2) O(n2)

空间复杂度

O ( n 2 ) O(n^2) O(n2)


377.组合总和IV

动态规划数组

dp[i]

状态表示

集合

整数i的所有划分方案

属性

划分方案的数量

状态计算

以划分方案的最后一个数字为根据进行划分,因为顺序不同的序列被视为不同的组合,且由于给定的数组的元素各不相同,因此保证了整数i划分方案一定不同。

最后一个数字的取值可以取nums中的任意数字,因此状态计算方法如下:
d p [ i ] = d p [ i ] + d p [ i − n u m s [ j ] ] dp[i] = dp[i] + dp[i - nums[j]] dp[i]=dp[i]+dp[inums[j]],
i > = n u m s [ j ] , 0 ≤ j < n u m s . s i z e ( ) i >= nums[j],0 \le j < nums.size() i>=nums[j],0j<nums.size()

初始状态

dp[0] = 1,表示组合总和为0的方案只有一种,即空集。


背包问题

0-1背包

416. 分割等和子集

方法:动态规划之0-1背包

悲!先尝试了一下dfs,发现超时,然后尝试剪枝 + 记忆化搜索,结果还是没过(不过评论区有人过了。看来他的代码,发现我的记忆化搜索的状态没表示对,但是还是不理解状态为什么不对)。看了答案,原来是0-1背包问题,我怎么就没想起来呢?

关于背包问题,有以下关键点:

  1. 状态表示。状态表示一般是二维数组,即dp[i][j]。其第一维j表示从前i个物品中选择,第二维j表示体积/价值等其他可以量化的约束条件。
  2. 状态属性。
    1. dp[i][j]可以存放数字:从前i个物品中选择,总体积/价值不超过等于j的选法
    2. dp[i][j]可以存放bool值:是否存在这样一种选法,使得从前i个物品中选择,总体积/价值等于j
  3. 背包问题通常可以进行空间优化。之所以进行空间优化,是因为它的状态转移是一个马尔科夫链,即当前的状态仅与上一时刻的状态有关,而与上一个时刻之前的状态无关。

474. 一和零

该题目属于变式的0-1背包问题,其中,字符串数组中的每个字符串可以抽象为背包问题中的物品,字符串中0的个数和1的个数可以抽象为背包问题中物品的体积/成本/代价,字符串的个数可以抽象为物品的价值。

动态规划数组为三维数组: d p [ k ] [ i ] [ j ] dp[k][i][j] dp[k][i][j],该值表示在前 k k k 个字符串中选择物品,且 0 0 0 的个数不超过 i i i , 1 1 1 的个数不超过 j j j 的字符串数组子集的最大长度。

若考虑空间优化,可以将物品维度的空间去掉,并且逆序遍历并更新动态规划数组。


494. 目标和

动态规划数组:dp[i][j]表示仅使用前i个字符的情况下,和为j的总方案数。

动态规划转移方程:dp[i][j] = dp[i - 1][j - nums[i]] + dp[i - 1][j + nums[i]]

需要注意的是,由于j - nums[i]可能为负数,为保证结果的正确性,在创建动态规划数组时需要给第二维度做一个正向偏移,其中正向偏移值为sum = nums[1] + nums[2] + …… + nums[nums.size() - 1]


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值