动态规划

题目来自 cyc2018 精选的力扣题

动态规划中包含3个重要的概念:

  1. 最优子结构
  2. 边界
  3. 状态转移公式。以爬楼梯为例,最优子结构为 \(f(10) = f(9) + f(8)\),边界是 \(f(1) = 1, f(2) = 2\),状态转移公式 \(f(n) = f(n-1) + f(n-2)\)

斐波那契数列

70. 爬楼梯

本质上为斐波那契数列。递归会超时(python 可设置缓存,不会超时),要用动态规划或直接应用通项公式

定义一个数组 \(dp\) 存储上楼梯的方法数(为了方便讨论,数组下标从 \(1\) 开始),\(dp[i]\) 表示走到第 \(i\) 个台阶的方法数目。
\(i\) 个台阶可以从第 \(i - 1\)\(i - 2\) 个台阶再走一次到达,走到第 \(i\) 个台阶的方法数为走到第 \(i - 1\) 和第 \(i - 2\) 个楼梯的方法数之和。

198. 打家劫舍

定义 \(dp\) 数组用来存储最大的抢劫量,其中 \(dp[i]\) 表示抢到第 \(i\) 个住户时的最大抢劫量。
由于不能抢劫邻近住户,如果抢劫了第 \(i - 1\) 个住户,那么就不能再抢劫第 \(i\) 个住户,所以:\(dp[i] = \max (dp[i - 2]+nums[i], dp[i - 1])\)

衍生题:环形街区
\([1, n]\)\([0, n - 1]\) 两者的最大值

矩阵路径

64. 最小路径和

62. 不同路径

排列组合

多重集排列问题(代码上是这个思路)

路径方向为多重集,有 向右向下 两种(方向)元素,两种元素的重数分别为 \(n_{1}\)\(n_{2}\),有 \(n = n_{1} + n_{2}\),则排列数为 \(\left(\begin{array}{l}{n} \cr {n_{1}} & {n_{2}}\end{array}\right) = \frac{n!}{n_{1}!n_{2}!}\)

⭐动态规划

\(dp[i][j]\) 是到达 \(i, j\) 的路径数,有 \(dp[i][j] = dp[i-1][j] + dp[i][j-1]\)

数组区间

303. 区域和检索 - 数组不可变

求数组区间 \((i, j)\) 的和

题中强调:会多次调用 sumRange 方法
因此预先求出所有 \((0, k)\) 的和,再 sum[j] - sum[i - 1]

413. 等差数列划分

等差数列满足:至少有 3 个元素、任意两个相邻(隔开不算)元素之差相同

暴力

每一对元素(之间至少隔着一个元素),根据两个元素之间的所有元素差值是否相等来判断是不是等差数列

💣动态规划

\(dp[i]\) 表示\(A[i]\) 为结尾(不是总的)的等差递增子区间的个数,对于 \(A = [0, 1, 2, 3, 4]\),有以下结论:

dp[2] = 1
    [0, 1, 2]
dp[3] = dp[2] + 1 = 2
    [0, 1, 2, 3], // [0, 1, 2] 之后加一个 3
    [1, 2, 3]     // 新的递增子区间
dp[4] = dp[3] + 1 = 3
    [0, 1, 2, 3, 4], // [0, 1, 2, 3] 之后加一个 4
    [1, 2, 3, 4],    // [1, 2, 3] 之后加一个 4
    [2, 3, 4]        // 新的递增子区间

综上,在 A[i] - A[i-1] == A[i-1] - A[i-2] 时,dp[i] = dp[i-1] + 1

这里的 \(dp[i]\) 不是最终的结果,而是每步的结果。思考的时候过于死板

⭐分割整数

343. 整数拆分

没思路

贪心思想(举例得出规律)

数字 \(n\) 可由 \(a\)\(x\)\(1\)\(b\) 相加而成。是否有优先级最高的因子 \(x\) 存在,有以下判断:

\(2 = 1 + 1\)\(1 * 1 < 2\),因此 \(2\)\(1 + 1\) 更优;
\(3 = 1 + 2\)\(1 * 2 < 3\),因此 \(3\)\(1\)\(2\) 更优;
\(4 = 2 + 2\)\(2 * 2 = 4\),因此可以认为 \(4\)\(2\) 等价,因此见到 \(4\) 就拆分;
\(5 = 2 + 3\);因为每个 \(5\) 都可以拆分为 \(2+3\),而 \(2 * 3 = 6 > 5\),因此见到 \(5\) 就拆分。
\(6 = 3 + 3 = 2 + 2 + 2\);因为 \(3 * 3 > 2 * 2 * 2 > 6\)。因此见到 \(6\) 就拆分,并且 \(3\) 是比 \(2\) 更优的因子。

易推出: 大数字都可以被拆分为多个小因子,以获取更大的乘积,只有 \(2\)\(3\) 不需要拆分。 列出以下贪心法则

  • 第一优先级: \(3\);把数字 \(n\) 拆成尽可能多的 \(3\) 之和;
    • 特殊情况: 拆完后,如果余数是 \(1\);则应把最后的 \(3 + 1\) 替换为 \(2 + 2\),因为后者乘积更大;
  • 第二优先级: \(2\);留下的余数如果是 \(2\),则保留,不再拆为 \(1+1\)

\(n <= 3\) 时,直接返回 \(n - 1\)

动态规划

\(dp[i]\) 表示:数字 \(i\) 拆分为至少两个正整数之和的最大乘积。
有转移方程:\(dp[i] = \max (dp[i], j * dp[i - j])\)
由于 \(i - j <= 3\) 时,\(dp[i - j] = i - j - 1 < i - j\),所以 \(dp[i] = \max (dp[i], j * \max(dp[i - j], i - j))\)

279. 完全平方数

BFS

前面有做过

动态规划

\(dp[i]\) 表示:数字 \(i\) 拆分为完全平方数的最少个数。
有转移方程:\(dp[i] = \min (dp[i], dp[i - square] + 1), square = 1, 4, 9... <= i\)

💣91. 解码方法

字符串中可能包含 "0",因此情况比较复杂

  • s[i] == "0"
    • s[i - 1] = "1" or "2",则 \(dp[i] = dp[i - 2]\)
    • 否则,return 0
  • s[i] != "0"
    • s[i - 1] == "1",则 \(dp[i] = dp[i - 1] + dp[i - 2]\)
    • s[i - 1] == "2" and "1" <= s[i] <= "6",则 \(dp[i] = dp[i-1] + dp[i-2]\)
      • 解释: s[i - 1]s[i] 分开译码,为 \(dp[i - 1]\);合并译码,为 \(dp[i - 2]\)
    • 否则,\(dp[i] = dp[i - 1]\)
      • 解释:此时若合并译码,则大于 \(26\)s[i - 1]s[i] 只能分开译码

最长递增子序列

300. 最长上升子序列

动态规划

\(dp[i]\) 表示以 \(S_i\) 结尾的序列的最长递增子序列长度。对于每个 \(i\),向前遍历以寻找递增子序列

376. 摆动序列

动态规划(想不到)

用两个 \(dp[i]\) 数组。\(up[i]\) 表示前 \(i\) 个元素中摆动序列以上升元素结尾的最长子序列长度;\(down[i]\) 反之。

若第 \(i\) 个元素上升就更新 \(up[i]\),如下代码:(\(down[i]\) 同理)

if (nums[i] > nums[j]) {
  up[i] = Math.max(up[i], down[j] + 1);
}

贪心算法

最长公共子序列

二维数组 \(dp\) 用来存储最长公共子序列的长度,其中 \(dp[i][j]\) 表示 \(S1\) 的前 \(i\) 个字符与 \(S2\) 的前 \(j\) 个字符最长公共子序列的长度,状态转移方程:

\(d p[i][j]=\left\lbrace\begin{array}{ll} dp[i-1][j-1]+1 && S1_{i} == S2_{j} \cr \max (dp[i-1][j], dp[i][j-1]) && S1_{i} != S2_{j} \end{array}\right.\)

特别需要注意的是,当 \(S1_{i} != S2_{j}\) 时,\(dp[i][j] != dp[i-1][j-1]\)。比如:abcdadbc\(i\)\(j\)\(4\) 时,若用错误的表达式,则最长公共子序列为 \(2\)

221. 最大正方形

这个转移方程想不到
转移方程:\({dp}(i, j)=\min ({dp}(i-1, j), {dp}(i-1, j-1), {dp}(i, j-1))+1\)
若某格子值为 1 ,则以此为右下角的正方形的、最大边长为:上面的正方形、左面的正方形或左上的正方形中,最小的那个,再加上此格。

⭐🌟0 - 1 背包(难)

此类问题的特点:一般都有选或不选两种选择

不能使用贪心算法

什么是 0 - 1 背包问题?

  • \(n\) 件物品,每件物品的重量为 \(w[i]\),价值为 \(c[i]\)。现有一个容量为 \(V\) 的背包,问如何选取物品放入背包,使得背包内物品的总价值最大。其中每种物品都只有 1 件

\(dp[i][v]\) 表示前 \(i\)物品 \((1 ≤ i ≤ n, 0 ≤ v ≤ V)\) 恰好装入容量\(V\) 的背包中所能获得的最大价值。① 第 \(i\) 件物品不放入 ② 第 \(i\) 件物品放入
状态转移方程:\({dp}[{i}][{v}]=\max ({dp}[{i}-1][{v}], {dp}[{i}-1][{v}-{w}[{i}]]+{c}[{i}])\)
\(\quad(1 ≤ {i} ≤ {n}, {w}[{i}] ≤ {v} ≤ {V})\)

空间优化后:(从大到小枚举)
\({dp}[{v}]=\max ({dp}[{v}], {dp}[{v}-{w}[{i}]]+{c}[{i}])\)
\(\quad(1 ≤ {i} ≤ {n}, {V} ≥ {v} ≥ {w}[{i}])\)

兄弟问题:完全背包问题(从小到大枚举)

  • \(n\) 种物品,每种物品的单件重量为 \(w[i]\),价值为 \(c[i]\)。现有一个容量为 \(v\) 的背包,问如何选取物品放入背包,使得背包内物品的总价值最大。其中每种物品都有无穷件

空间优化后:(从小到大枚举)
\({dp}[{v}]=\max ({dp}[{v}], {dp}[{v}-{w}[{i}]]+{c}[{i}])\)
\(\quad(1 ≤ {i} ≤ {n}, {w}[{i}] ≤ {v} ≤ {V})\)

子问题:多维费用的 0-1 背包问题

416. 分割等和子集

动态规划

\(dp[i][j]\) 表示从数组的 \([1, i]\) 这个子区间内挑选一些正整数,每个数只能用一次,是否存在数的和恰好等于 \(j\) 的情况(boolean 类型)

看题解:这题很好
https://leetcode-cn.com/problems/partition-equal-subset-sum/solution/0-1-bei-bao-wen-ti-xiang-jie-zhen-dui-ben-ti-de-yo/

DFS

494. 目标和

01 背包其实不是这种解法的重点,重点是怎么把题目转化成求解 01 背包的形式

本题的 DFS 无法剪枝,为暴力解法

动态规划

思路正常版

\(dp[i][j]\) 表示用数组中的前 \(i\) 个元素,组成和为 \(j\) 的方案数:\(dp[i][j] = dp[i - 1][j - nums[i]] + dp[i - 1][j + nums[i]]\)
递推形式:
\(dp[i][j + nums[i]] += dp[i - 1][j]\)
\(dp[i][j - nums[i]] += dp[i - 1][j]\)

由于数组中所有数的和不超过 \(1000\),那么 \(j\) 的最小值可以达到 \(-1000\)。在很多语言中,是不允许数组的下标为负数的,因此我们需要给 \(dp[i][j]\) 的第二维预先增加 \(1000\)

巧妙版

转换为子集和
https://leetcode-cn.com/problems/target-sum/solution/c-dfshe-01bei-bao-by-bao-bao-ke-guai-liao/

474. 一和零

多维费用的 0-1 背包问题(此题为二维)

本题有 0 字符串和 1 字符串两种背包,每个字符串的 0 1 个数为费用(体积),每个字符串的价值设为 1
\(dp(i, j)\) 表示使用 \(i\)\(0\)\(j\)\(1\) 最多能拼出的字符串数目。

转移方程(空间优化):
\(dp[i][j] = \max (dp[i][j], 1 + dp[i - zeroNums][j - oneNums])\)
因为要用到前一个字符串的状态,所以费用 i j 要倒序遍历

322. 零钱兑换

完全背包问题(正序)

总金额 amount 为背包容量,单个硬币的面额为物品单件的重量,物品的价值在此题未用到

139. 单词拆分

因为单词可以被用多次,所以是完全背包问题。
求解顺序的完全背包问题时,对物品的迭代应该放在最里层,对背包的迭代放在外层,只有这样才能让物品按一定顺序放入背包中

因为单词的顺序是严格的,所以遍历单词表必须放在内循环

\(dp[i]\) 表示字符串 s 的前 \(i\) 长度的子串能否被拆分

39. 组合总和

仍然是完全背包问题

它的三个兄弟问题在 搜索 - 回溯 专题

股票交易

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

允许多次买卖,但有冷冻期

状态定义

\(dp[i][j]\) 表示 \([0, i]\) 区间内,到第 \(i\) 天(从 \(0\) 开始)状态为 \(j\) 时的最大收益

这里 \(j\) 取三个值:
0 表示不持股;1 表示持股;2 表示处在冷冻期

状态转移方程

  • 不持股可以由这两个状态转换而来:(1)昨天不持股,今天什么都不操作,仍然不持股。(2)昨天持股,今天卖了一股
  • 持股可以由这两个状态转换而来:(1)昨天持股,今天什么都不操作,仍然持股;(2)昨天处在冷冻期,今天买了一股
  • 处在冷冻期只可以由不持股转换而来(冷冻期其实也算不持股)
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][2] - prices[i]);
dp[i][2] = dp[i - 1][0];

初值

持股的初值为 -price[0](想象成花了这么多钱买入)

输出

返回最后一天不持股的值

714. 买卖股票的最佳时机含手续费

允许多次买卖,但有手续费

状态定义

\(dp[i][j]\) 表示 \([0, i]\) 区间内,到第 \(i\) 天(从 \(0\) 开始)状态为 \(j\) 时的最大收益

这里 \(j\) 取两个值:
0 表示不持股;1 表示持股

状态转移方程

  • 不持股可以由这两个状态转换而来:(1)昨天不持股,今天什么都不操作,仍然不持股。(2)昨天持股,今天卖了一股(要减去手续费
  • 持股可以由这两个状态转换而来:(1)昨天持股,今天什么都不操作,仍然持股;(2)昨天不持股,今天买了一股
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i] - fee);
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);

初值和输出

与上题(冷冻期)相同

💣买卖股票的最佳时机 III & IV

限定交易次数为 n 次

状态定义

\(dp[i][j][k]\) 表示 \([0, i]\) 区间内,到第 \(i\) 天已经进行了第 \(j\) 次买入,且交易状态为 \(k\) 时的最大收益

这里 \(j\) 取 n 个值:表示最多可进行 n 次交易

\(k\) 取三个值:
0 表示不持股;1 表示持股

状态转移方程

  • 不持股可以由这两个状态转换而来:(1)之前已经进行了 j 次买卖,昨天不持股,今天什么都不操作,仍然不持股。(2)昨天持股,并且是第 j 次买卖,今天卖了股票
  • 持股可以由这两个状态转换而来:(1)昨天持股,今天什么都不操作,仍然持股;(2)已经进行了 j - 1 次买卖,昨天不持股,今天买了一股,进行第 j 次持股

状态转移方程照着之前的题目修改会更快想出来

dp[i][j][0] = Math.max(dp[i - 1][j][0],dp[i - 1][j][1] + prices[i]);
if (j == 0) {
    dp[i][j][1] = Math.max(dp[i - 1][j][1], -prices[i]);
} else {
    dp[i][j][1] = Math.max(dp[i - 1][j][1], dp[i - 1][j - 1][0] - prices[i]);
}

初值(这题的初值很重要)

所有不持股的状态值初始化的时候为 0,所有持股的状态值都设置为一个很大的负数或 -price[0]

持股状态的初值一开始只给第一次交易赋值了(第二次、第三次……都为 0),这样在对 dp[i][j][1] 的赋值时,会出问题

for (int i = 0; i < dealNums; i++) {
    dp[0][i][1] = -prices[0];
}

输出

返回最后一天、最后一次不持股的值

字符串编辑

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

本质为求最长公共子序列

650. 只有两个键的键盘

有一个性质:若两个数有 n 倍关系,那么翻 n 倍,就加 n 步
比如 3 需要 3 步,那么 15 就要 3 + 5 = 8 步

72. 编辑距离(困难)

思路和最长公共子序列其实很相似,但是更难理解

参考链接

状态定义

\(dp[i][j]\) 代表 word1\(i\) 位置转换成 word2\(j\) 位置需要最少步数

状态转移方程

word1[i] == word2[j]\(dp[i][j] = dp[i-1][j-1]\)
word1[i] != word2[j]\(dp[i][j] = min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]) + 1\)

其中,\(dp[i-1][j-1]\) 表示替换操作,\(dp[i-1][j]\) 表示删除操作,\(dp[i][j-1]\) 表示插入操作,解释如下:

以 word1 为 "horse",word2 为 "ros",且 dp[5][3] 为例,即要将 word1 的前 5 个字符转换为 word2 的前 3 个字符,也就是将 horse 转换为 ros,因此有:
  1. $dp[i-1][j-1]$,即先将 word1 的前 4 个字符 hors 转换为 word2 的前 2 个字符 ro,然后将第五个字符 word1[4](因为下标基数以 0 开始) 由 e 替换为 s(即替换为 word2 的第三个字符,word2[2])
  2. $dp[i][j-1]$,即先将 word1 的前 5 个字符 horse 转换为 word2 的前 2 个字符 ro,然后在末尾补充一个 s,即插入操作
  3. $dp[i-1][j]$,即先将 word1 的前 4 个字符 hors 转换为 word2 的前 3 个字符 ros,然后删除 word1 的第 5 个字符

初值

注意,针对第一行,第一列要单独考虑,我们引入 '' 下图所示:

第一行,是 word1 = '' 为空变成 word2 最少步数,就是插入操作
第一列,是变为 word2='' 为空,需要的最少步数,就是删除操作

posted @ 2020-04-25 09:33  PeteLau  阅读(207)  评论(0)    收藏  举报