上述两个问题都属于完全背包问题。
正则表达式的匹配:关于这道题目,需要注意的是对于*符号,它会与前一个字符形成新的模式。因此,遇到 字符 + *符号,需要单独处理。
通配符的匹配:这道题目是正则表达式匹配的低配版。它不需要考虑前后字符之间的关联,只需要考虑边界条件就足够了。
最开始的想法:我用计算给定数组的前缀和,并记录下前缀和的最大值和最小值,最后的结果是最大值 - 最小值。但是这种做法有一个问题:
- 无法保证前缀和最大值的下标是否大于前缀和最小值的下标
好了,假设你意识到了上述问题,于是你使用下标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]。
方法:一步一步跳,并更新可跳步数
方法:二维动态规划。注意到状态的无后效性,我们可以使用滚动数组对其进行优化。
方法:二维动态规划。注意到状态的无后效性,我们可以使用滚动数组对其进行优化。
方法:动态规划(而且是最简单的那种)。
不过嘞,这道题目还是有给我们一点启发:能用数组就不要使用vector
方法:动态规划。
动态规划数组
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问题,可用动态规划来解决。
题解:宫水三叶题解。
回溯超时了,然后想到了动态规划。
动态规划方程:
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.交错字符串
我不会做
想到了要利用动态规划,但并不知道怎么做。
动态规划转移方程:
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])
前后缀分解
方法:前后缀分解。在本题中,枚举两次交易的分界点,该分界点是第二次交易时股票的买入时机,此时答案就是
max
(左区间交易最大值 + 右区间交易最大值)。这样我们只需要求解左区间和右区间的交易最大值即可。左区间的交易最大值:预处理
右区间的交易最大值:和买卖股票的最佳时机使用的方法相同,只不过我们需要逆序遍历,并维护一个存储股票价格最大值变量maxPrices
。由于我们已知了第二次股票的买入时机,那么第二次交易的最大值就是 m a x P r i c e s − p r i c e s [ i ] maxPrices - prices[i] maxPrices−prices[i]。
方法:动态规划。 我第一次做的时候是把这道题当作区间动态规划问题来考虑的。看了题解,把这道题目当作完全背包问题来做时间复杂度和空间复杂度会更低。
- 区间动态规划
- 动态规划数组
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<string> 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
这道题目完全不会做
自右下到左上的动态规划。完全不会做。悲!
交易两次的题目我就不会做,交易 k k k次我就会做吗?可笑!!!
动态规划数组:
\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])
一定要注意边界条件,我没注意边界条件导致解答错误。
题目规定首尾房屋相连,这就意味着第一间房屋和最后一间房屋不可兼得。那么如何保证?假设有 N N N间房屋:
- 如果偷第一间房屋,可偷窃的房屋范围是 [ 1 , N − 1 ] [1,N-1] [1,N−1]
- 如果不偷第一间房屋,可偷窃的房屋范围是 [ 2 , N ] [2,N] [2,N]
按照如上分析,我们有两大类偷窃方法,偷窃的最大金额就是这两类方法计算结果的最大值。
完全不会做。暴风哭泣。。。
数位DP
给定一个数字
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
位
思路和算法:完全背包 或 区间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
思路和算法:
- 动态规划 + 爆搜。动态规划方程:
dp[i] = max(dp[i] , dp[j] + 1),j = 0,1,2,...i - 1
- 动态规划 + 二分。
树形dp,属于我不会做的类型。不过要学习一下y总的代码,能给我很多启发。
动态规划,区间DP。
完全背包问题。
树形DP。
- 记忆化搜索。时间上击败1.62%,空间上击败5%左右
- 动态规划。
记忆化搜索
记忆化搜索利用自下而上的递归方法,遍历每一种所有可能的拆分方案,选择乘积最大的作为结果。同时记忆化搜索会用一个状态数组记录已遍历过的状态。具体步骤如下:
- 定义并使用
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
和最后一个元素的地址搜索最大整除子集
方法;区间DP。
闫氏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)
动态规划数组
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
的方案只有一种,即空集。
背包问题
0-1背包
方法:动态规划之0-1
背包
悲!先尝试了一下dfs
,发现超时,然后尝试剪枝 + 记忆化搜索,结果还是没过(不过评论区有人过了。看来他的代码,发现我的记忆化搜索的状态没表示对,但是还是不理解状态为什么不对)。看了答案,原来是0-1
背包问题,我怎么就没想起来呢?
关于背包问题,有以下关键点:
- 状态表示。状态表示一般是二维数组,即
dp[i][j]
。其第一维j
表示从前i
个物品中选择,第二维j
表示体积/价值等其他可以量化的约束条件。 - 状态属性。
dp[i][j]
可以存放数字:从前i
个物品中选择,总体积/价值不超过 或 等于j
的选法dp[i][j]
可以存放bool
值:是否存在这样一种选法,使得从前i
个物品中选择,总体积/价值等于j
- 背包问题通常可以进行空间优化。之所以进行空间优化,是因为它的状态转移是一个马尔科夫链,即当前的状态仅与上一时刻的状态有关,而与上一个时刻之前的状态无关。
该题目属于变式的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 的字符串数组子集的最大长度。
若考虑空间优化,可以将物品维度的空间去掉,并且逆序遍历并更新动态规划数组。
动态规划数组: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]
。