10.正则表达式的匹配
44.通配符的匹配
上述两个问题都属于完全背包问题。
正则表达式的匹配:关于这道题目,需要注意的是对于*符号,它会与前一个字符形成新的模式。因此,遇到 字符 + *符号,需要单独处理。
通配符的匹配:这道题目是正则表达式匹配的低配版。它不需要考虑前后字符之间的关联,只需要考虑边界条件就足够了。
53.最大子数组和
最开始的想法:我用计算给定数组的前缀和,并记录下前缀和的最大值和最小值,最后的结果是最大值 - 最小值。但是这种做法有一个问题:
- 无法保证前缀和最大值的下标是否大于前缀和最小值的下标
好了,假设你意识到了上述问题,于是你使用下标i和j,下标i表示前缀和最大值的索引,下标j表示前缀和最小值的索引。然后再对程序做一个改进:每次都要更新res。这还有一个问题:
- 上述更新i和j是根据dp[i] >= dp[k] 以及 dp[j] <= dp[k] 来更新的。这种更新方式基于一种假设:数组的前缀和是递增的。但是对于全负数序列,数组的前缀和是递减的,因此这种方法对于全负数序列不适用。
于是我们开始思考其他方法。忽然,灵机一动,假设数组dp[i]表示以下标i为结尾的最大子数组的和,那么此时更新方式有两种考量:
- 加入上一个数据形成的子数组序列,即dp[i - 1] + nums[i]
- 自己另起炉灶,敢为人先,从当前元素开始形成一个长度为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[i−1][j−1]=dp[i−1][j]dp[i−1][j]s[i]=t[j]s[i]=t[j]
118.杨辉三角
119.杨辉三角II
120.三角形的最小路径和
121.买卖股票的最佳时机
122.买卖股票的最佳时机II
若第
i
天持有股票,有两种情况:
- 第
i - 1
天持有- 第
i - 1
天卖出,但第i
天买入股票若第
i
天没有持有股票,也有两种情况:
- 第
i - 1
天没有持有- 第
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[i−1][0],dp[i−1][1]+prices[i])dp[i][1]=max(dp[i−1][1],dp[i−1][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] maxPrices−prices[i]。
139.单词拆分
方法:动态规划。 我第一次做的时候是把这道题当作区间动态规划问题来考虑的。看了题解,把这道题目当作完全背包问题来做时间复杂度和空间复杂度会更低。
- 区间动态规划
- 动态规划数组
dp[i][j]
表示字符串s[i : j]
是否可由字符串列表wordDict
拼接而成。- 动态规划转移方程:
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- 为了快速查找单词
s[i : j]
是否出现在字符串列表,我们使用哈希表存储字符串列表中的单词,即unordered_set
。hash(wordDict.begin(),wordDict.end()) - 完全背包问题
- 动态规划数组
dp[i]
表示字符串s[0 : i]
是否可由字符串列表wordDict
拼接而成- 动态规划转移方程:
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.打家劫舍
动态规划数组:
\quadres
。res[i]
表示小偷走到第i
个房间偷盗的最大金额。动态规划方程:
\quad 小偷走到第i
个房间,有两种选择 \quad
- 不偷。此时
res[i] = res[i - 1]
; \quad- 偷。此时
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[i−1],res[i−2]+nums[i])边界条件: \quad
- 假设只有一间屋子,偷盗的最大金额是
nums[0]
\quad- 假设只有两件屋子,偷盗的最大金额是
max(nums[0],nums[1])
一定要注意边界条件,我没注意边界条件导致解答错误。
213.打家劫舍II
题目规定首尾房屋相连,这就意味着第一间房屋和最后一间房屋不可兼得。那么如何保证?假设有 N N N间房屋:
- 如果偷第一间房屋,可偷窃的房屋范围是 [ 1 , N − 1 ] [1,N-1] [1,N−1]
- 如果不偷第一间房屋,可偷窃的房屋范围是 [ 2 , N ] [2,N] [2,N]
按照如上分析,我们有两大类偷窃方法,偷窃的最大金额就是这两类方法计算结果的最大值。
221. 最大正方形
完全不会做。暴风哭泣。。。
233。数字1的个数
给定一个数字
abcdefg
,统计数字1
的最佳做法是统计每一位数字1
的个数。
例:
考虑d
位,若
d=0
,在d
位数字为1的数共有abc(000 ~ abc - 1,共abc个)
× \times ×efg
位d=1
,在d
位数字为1的数共有abc
× \times ×1000
+ + +efg
+ + + 1位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
),我们有如下选择
- 选
0
个,则f[i][j] = f[i - 1][j - 0 * t] + 0
- 选
1
个,则f[i][j] = f[i - 1][j - 1 * t] + 1
- 选
2
个,则f[i][j] = f[i - 1][j - 2 * t] + 2
⋅ ⋅ ⋅ \cdot\cdot\cdot ⋅⋅⋅- 选
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.最长递增子序列
思路和算法:
- 动态规划 + 爆搜。动态规划方程:
dp[i] = max(dp[i] , dp[j] + 1),j = 0,1,2,...i - 1
- 动态规划 + 二分。
309.最佳买卖股票时机含冷冻期
310.最小高度树
树形dp,属于我不会做的类型。不过要学习一下y总的代码,能给我很多启发。
312.戳气球
动态规划,区间DP。
322.零钱兑换
完全背包问题。
337.打家劫舍III
树形DP。
343.整数拆分
记忆化搜索利用自下而上的递归方法,遍历每一种所有可能的拆分方案,选择乘积最大的作为结果。同时记忆化搜索会用一个状态数组记录已遍历过的状态。具体步骤如下:
INT_MIN
记忆化数组mem
,mem[n][k]
表示整数n
拆分为k
个数字后对应的最大乘积。k
的取值,2 <= k < n
,对于每一个k
,执行递归程序mem[n][k]
不是INT_MIN
,返回mem[n][k]
k == 1
,直接把n
复制给mem[n][k]
,并返回n
n
进行整数拆分,记拆分的第一个数字是i
,若n - i >= k - 1
,才继续递归,并利用递归返回的结果更新mem[n][k]
,更新方式为mem[n][k] = max(dfs(n - i,k - 1) * i,mem[n][k])
;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]=1≤j<iminmax(dp[i],j×(i−j),j×dp[i−j])
368.最大整除子集
这道题目太有意思了。话不多说,下面开始分析:
假设现在有一个最大整除子集nums
,把nums
按照从小到大的顺序排列,那么对于任意i,j
,且i < j
,都有nums[j] % nums[i] = 0
,而且由于整除具有传递性,因此对于任意的k,k < i
,都有nums[j] % nums[k] = 0
。
由上述最大整除子集的特点,在保证一个序列有序的前提下,要判断一个数字x
属于哪个集合,只需要判断该数字能否被集合中的最大元素整除。因此,只需要维护一个集合中的最大元素即可。
题目要求求解最大整除子集的集合,因此还需要维护以下两个数组:
len
表示。len[i]
表示以第i
个元素结尾的整除子集的长度addr
数组,addr[i]
表示以第i
个元素结尾的整除子集的上一个元素的地址(索引)。该数组借鉴了链表的思想,以此达到搜索最大整除子集的目的分析到这一步,算法步骤已经很明确了:
对nums
排序,保证序列有序
遍历nums
,假设当前遍历到第i
个元素
i
个元素开始逆序遍历,假设遍历到的是第j
个元素,那么有nums[i] > nums[j]
nums[i]
是否是nums[j]
的倍数,且len[i] < len[j] + 1
,更新len[i],addr[i]
。遍历len
数组,查找整除子集的最大长度及其最后一个元素的地址
利用addr
和最后一个元素的地址搜索最大整除子集
375.猜数字大小II
方法;区间DP。
集合:f[i][j]表示当选择的数字在区间[i,j]中时所有的猜法
属性:所有猜法当中所需要金额的最小值
若当前猜测的数字是k
( i ≤ k ≤ j i \le k \le j i≤k≤j),会有以下三种情况:
0
元f[k + 1][j] + k
元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][j−1])+k),i≤k≤j
376.摆动序列
可以借鉴最长子序列和乘积最大子数组的做法,具体而言:
题目要求求解最长子序列的长度,只不过附加了一个限制条件:最长子序列是摆动序列,那么可以借鉴一下最长子序列的求解方法。
使用一个二维数组表示状态。具体而言:
dp[i][0]
表示最后一个元素呈上升趋势的所有子序列dp[i][1]
表示最后一个元素呈下降趋势的所有子序列dp[i][0]
表示所有子序列中最长子序列的长度dp[i][1]
表示所有子序列中最长子序列的长度对于dp[i][0]
,由于子序列的最后一个元素要呈现上升趋势,那么倒数第二个元素是呈现下降趋势,因此它必然是由dp[j][1]
转移而来, 0 ≤ j < i 0 \le j < i 0≤j<i。
对于dp[i][1]
,由于子序列的最后一个元素要呈现下降趋势,那么倒数第二个元素是呈现上升趋势,因此它必然是由dp[j][0]
转移而来, 0 ≤ j < i 0 \le j < i 0≤j<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),0≤j<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),0≤j<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[i−nums[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],0≤j<nums.size()
dp[0] = 1
,表示组合总和为0
的方案只有一种,即空集。
416. 分割等和子集
方法:动态规划之0-1
背包
悲!先尝试了一下dfs
,发现超时,然后尝试剪枝 + 记忆化搜索,结果还是没过(不过评论区有人过了。看来他的代码,发现我的记忆化搜索的状态没表示对,但是还是不理解状态为什么不对)。看了答案,原来是0-1
背包问题,我怎么就没想起来呢?
关于背包问题,只讲这么几点:
dp[i][j]
。其第一维j
表示从前i
个物品中选择,第二维j
表示体积/价值等其他可以量化的约束条件。dp[i][j]
可以存放数字:从前i
个物品中选择,总体积/价值不超过 或 等于j
的选法dp[i][j]
可以存放bool
值:是否存在这样一种选法,使得从前i
个物品中选择,总体积/价值等于j