基本思想
问题 的最优解如果可以由 子问题 的最优解推导得到,则可以先求解子问题的最优解,再构造原问题的最优解;若子问题有较多的重复出现,则可以自底向上从最终子问题向原问题逐步求解。
特点
- 求解一个问题的最优解
- 整体问题的最优解是依赖各个子问题的最优解
- 把大问题分解成若干个小问题,这些小问题之间还有相互重叠的更小的子问题
- 从上往下分析问题,从下往上求解问题
模型抽象
以 背包问题 为基础
m容量 n种物品 weight重量 value价值
各种问题无非就是在这几个量上做文章,这次整理这几个题就看到了 有/无 容量 的,value会变化 的等等。
例题
常规背包问题及其变体
建立 d p [ n ] [ m ] dp[n][m] dp[n][m] 的数组,保存 使用前n种物品 在 容量为m 的情况下的最大价值
判断条件: 小于最大容量,也就是背包装得下
474.一和零
这道题稍微有点不一样是有两个容量,因此在建立数组的时候变成了
d
p
[
n
]
[
m
1
]
[
m
2
]
dp[n][m_1][m_2]
dp[n][m1][m2]
1402.做菜顺序
这个题倒是常规的背包问题,不过它这里的物品价值不是个常量,所以在算总价值的时候要用个简单的计算,其实根本思路还是一样的。
这还是个 困难 的题,前一天晚上没做这篇整理思路不太清晰,今天整理了一下其他几个题,再做这个题就很通顺的一次通过了。不过就是 执行用时 和 内存消耗 比较大。。。目前就整理解题思路,怎么进一步优化后续再说吧
无容量的问题及其变体
建立 d p [ n ] dp[n] dp[n] 的数组,保存 使用前n种物品 的最大价值
判断条件: 符合某种条件,第一种 是背包装得下,下面例题 是比前一个数要大(上升)
300.最长上升子序列
这个题没有容量的问题了,变成了符合 上升序列 这个条件,但实际上跟 第一种 是一个意思,满足某种条件的时候算上当前情况就行了。
978.最长湍流子数组
这个题也是没有容量的,要符合 湍流子数组 的条件。相邻两个大小关系要是相反的,所以用 dp[n][2] 来存,[2] 表示该大于或者该小于。
动态规划 vs 贪心算法
面试题14-I"剪绳子"
这个题就很灵活,不是那种死板的背包问题,而是真的要把问题给分解成子问题,没办法去套用方法。而且这个题很烦啊 = = 长度是2或者3的时候返回的是1跟2,但用于计算更长的长度的时候,2跟3的结果就是2和3。。。。就不用拆分了。。。
另外这个题可以用贪心来做,长度大于5的时候要让3多一点,长度是4的时候要剪成两个2。
贪心我目前感觉就像是在 耍流氓 - - 我看见这个题,我要是能想出来 贪心 什么,我就知道怎么解,要是没发现,就凉了。而且贪心策略也基本上只能是提出个策略然后去证明。。(也许后续学习的过程中还会再来修正,目前就是这么理解的。。。头大 LOL
面试题14-II“剪绳子II”
啊这。。好像在返回的时候 $ %(1000000007)$ 就行了啊
股票类型问题
121.买卖股票的最佳时机
122.买卖股票的最佳时机II
123.买卖股票的最佳时机III
188.买卖股票的最佳时机IV
309.最佳买卖股票时机含冷冻期
714.买卖股票的最佳时机含手续费
剑指Offer63.股票的最大利润
其他例题
面试题47“礼物的最大价值”
典型的动态规划,不过可能不是那种背包问题。
可以很容易找到 转移方程 :
f
(
i
,
j
)
=
m
a
x
[
f
(
i
,
j
−
1
)
,
f
(
i
−
1
,
j
)
]
+
g
r
i
d
(
i
,
j
)
f(i,j)=max[f(i,j−1),f(i−1,j)]+grid(i,j)
f(i,j)=max[f(i,j−1),f(i−1,j)]+grid(i,j)
面试题48“最长不含重复字符的子字符串”
动态规划嘛,就是下一个状态能由上一个状态推算出来,能 转移,就能动态规划。定义
f
(
i
)
f(i)
f(i)表示以第
i
i
i个字符为结尾的 不包含重复字符的子字符串的最长长度 ,那每次算
f
(
i
)
f(i)
f(i)的时候,我们已经知道了
f
(
i
−
1
)
f(i-1)
f(i−1),接着咋整呢?要做个分类讨论了:
- 如果第 i i i个字符之前没出现过,那么 f ( i ) = f ( i − 1 ) + 1 f(i) = f(i-1) + 1 f(i)=f(i−1)+1;
- 如果第
i
i
i个字符之前出现过了,那么就复杂点了。计算一下第
i
i
i个字符和它 上次 出现的位置的距离,记为
d
d
d。
- d ≤ f ( i − 1 ) d \leq f(i-1) d≤f(i−1)。说明第 i i i个字符出现在了 f ( i − 1 ) f(i-1) f(i−1)里面了。但!第 i i i个字符的两次相邻出现之间就没有其他重复字符了,所以 f ( i ) = d f(i) = d f(i)=d
- d > f ( i − 1 ) d > f(i-1) d>f(i−1)。说明第 i i i个字符上一次出现已经是在 f ( i − 1 ) f(i-1) f(i−1)外面了,所以 f ( i ) = f ( i − 1 ) + 1 f(i) = f(i-1) + 1 f(i)=f(i−1)+1
在上述过程中,需要有个辅助的数组来记录一下各个字符上一次出现的位置 i n d e x index index,然后就可以实现算法了。
面试题42“连续子数组的最大和”
这个题有个类似于找规律的解法,直接上代码看吧。重点不是这个:
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
for i in range(1, len(nums)):
nums[i] += max(nums[i - 1], 0)
return max(nums)
意思是说,前面的最大连续和 如果是正的我就可以要它,不然还不如就我自己,不能要拖后腿的。
接下来是进阶:
动态规划 的解决法
面试题49“丑数”
这题看着是真复杂。。。基本思路还是一样的,但是考虑的因素多了,感觉就有点转不过弯了 QAQ
先逆向思维一下,某个数是第
n
n
n个丑数,它无非就是由之前的某个丑数乘上2或者3或者5。之前这个数就有三个可能
x
a
x_a
xa,
x
b
x_b
xb和
x
c
x_c
xc。所以我们只要判断
x
a
×
2
x_a \times 2
xa×2,
x
b
×
3
x_b \times 3
xb×3和
x
c
×
5
x_c \times 5
xc×5哪个更小,就是这个丑数了。计算过程中要维护这三个变量,并且每当某个变量“中奖了”就给它 +1 就行了。
代码也附上:
class Solution:
def nthUglyNumber(self, n: int) -> int:
dp, a, b, c = [1] * n, 0, 0, 0
for i in range(1, n):
n2, n3, n5 = dp[a] * 2, dp[b] * 3, dp[c] * 5
dp[i] = min(n2, n3, n5)
if dp[i] == n2: a += 1
if dp[i] == n3: b += 1
if dp[i] == n5: c += 1
return dp[-1]
面试题60“n个骰子的点数”
- 基于递归求骰子点数,时间效率不够高
- 基于循环求骰子点数,时间性能好
- 动态规划
d p [ i ] [ j ] dp[i][j] dp[i][j] 表示使用前i个骰子,和为j的条件下共有多少个情况
考虑到转移的问题,第i个骰子和为j的时候,也就是i-1个骰子和分别为j-1到j-6的六种情况数量之和,代码如下:
for k in range(6):
dp[i][j] += dp[i-1][j-k]
面试题63“股票的最大利润”
动态规划可以解决,其实最主要的问题是要保留第i-1个数字以前的最小值,才能知道第i个数字的时候的最大利润。
参考链接
1. https://www.cnblogs.com/hithongming/p/9229871.html
2. https://blog.csdn.net/weixin_40984271/article/details/82560731