算法训练3.2.1 基础DP
斐波那契数
斐波那契数 (通常用 F(n)
表示)形成的序列称为 斐波那契数列 。该数列由 0
和 1
开始,后面的每一项数字都是前面两项数字的和。也就是:
F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1
给定 n
,请计算 F(n)
。
示例 1:
输入:n = 2
输出:1
解释:F(2) = F(1) + F(0) = 1 + 0 = 1
示例 2:
输入:n = 3
输出:2
解释:F(3) = F(2) + F(1) = 1 + 1 = 2
示例 3:
输入:n = 4
输出:3
解释:F(4) = F(3) + F(2) = 2 + 1 = 3
提示:
0 <= n <= 30
题目解析
此题是动态规划最简单的题目之一。
状态转移方程式:题目已经明确告诉我们,
F(n) = F(n - 1) + F(n - 2)
容器规模:数组表示的范围是
[0, n]
,因此数组大小定义为n+1
base case:对于任意的
i
,都只依赖于i - 1
和i - 2
,因此我们只需要将数组下标为0
和下标为1
预先填好,后面所有的值都按照式子即可填满表中所有的值。填表顺序:从下标为
2
开始往后填表即可
过程如下:
代码展示
1 |
|
打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 1:
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:
输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。
提示:
1 <= nums.length <= 100
0 <= nums[i] <= 400
题目解析
此题本质上是斐波那契数列。
- 状态转移方程式:
dp[i]
表示考虑[0, i]
的子数组按照规则进行偷窃,能获取的最大价值。对于下标为i
的元素,可以选和不选,如果选的话则只能考虑i - 2
(因为不能偷相邻的),否则则考虑i - 1
,因此式子为:dp[i] = max(dp[i - 1], dp[i - 2] + nums[i])
- 容器规模:
i
的含义是数组下标,因此范围是[0, nums.length - 1]
,因此容器大小是nums.length
- base case:对于任意一个元素
i
只依赖i - 1
和i - 2
,因此只需要预先填上nums[0]
和nums[1]
即可。 - 填表顺序:从下标为
2
开始按序填满数组即可。
代码展示
1 |
|
最小路径和
给定一个包含非负整数的 m x n
网格 grid
,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
示例 1:
输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。
示例 2:
输入:grid = [[1,2,3],[4,5,6]]
输出:12
提示:
m == grid.length
n == grid[i].length
1 <= m, n <= 200
0 <= grid[i][j] <= 100
题目解析
状态转移方程式:
dp[i][j]
表示走到(i, j)
这个位置的最小路径和的值。而走到位置(i, j)
只有两个位置可以走过来,也就是(i - 1, j)
和(i, j - 1)
,因此我们取最小的即可,也就是dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j]
容器规模:
i
和j
的范围分别是m
和n
,因此定义dp[m][n]
即可base case:对于任意一个位置
(i, j)
都依赖于(i - 1, j)
和(i, j - 1)
。因此我们只需要填满第一行f(0, j)
即可,接下来依次填满填满即可(f(1, j), f(2, j))
。有一定基础的同学不难看出,这个依赖关系与“完全背包”一致。填表顺序:从
i
等于1
开始,一行一行从左往右填表即可。
代码展示
1 |
|
不同路径
一个机器人位于一个 m x n
网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
示例 1:
输入:m = 3, n = 7
输出:28
示例 2:
输入:m = 3, n = 2
输出:3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
1.向右 -> 向下 -> 向下
2.向下 -> 向下 -> 向右
3.向下 -> 向右 -> 向下
示例 3:
输入:m = 7, n = 3
输出:28
示例 4:
输入:m = 3, n = 3
输出:6
提示:
1 <= m, n <= 100
题目数据保证答案小于等于 2 * 10^9
题目解析
状态转移方程式:
dp[i][j]
表示走到位置(i, j)
有多少种走法。走到位置(i, j)
只有两个方向(i - 1, j)
和(i, j - 1)
容器规模:
i
和j
的范围分别是[0, m - 1]
和[0, n - 1]
,因此dp[m][n]
base case:对于任意位置
(i, j)
都依赖于(i - 1, j)
和(i, j -1)
,因此我们只需要填满第一行f(0, j)即可,接下来依次填满填满即可(f(1, j), f(2, j) … f(m - 1, j))
。有一定基础的同学不难看出,这个依赖关系与“完全背包”一致。填表顺序:从i等于1开始,一行一行从左往右填表即可。
代码展示
1 |
|
不同路径 II
一个机器人位于一个 m x n
网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用 1
和 0
来表示。
示例 1:
输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出:2
解释:3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
1.向右 -> 向右 -> 向下 -> 向下
2.向下 -> 向下 -> 向右 -> 向右
示例 2:
输入:obstacleGrid = [[0,1],[0,0]]
输出:1
提示:
m == obstacleGrid.length
n == obstacleGrid[i].length
1 <= m, n <= 100
obstacleGrid[i][j] 为 0 或 1
代码展示
1 |
|
杨辉三角
给定一个非负整数 numRows
,生成「杨辉三角」的前 numRows
行。
在「杨辉三角」中,每个数是它左上方和右上方的数的和。
示例 1:
输入: numRows = 5
输出: [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]]
示例 2:
输入: numRows = 1
输出: [[1]]
提示:
1 <= numRows <= 30
题目解析
- 状态转移方程式:
dp[i][j]
表示位置i
和j
的值。dp[i][j] = dp[i - 1][j] + dp[i - 1][j - 1]
- 容器规模:第
i
层是一个长度为i
的数组。 - base case:任意一个位置
(i, j)
都依赖于(i - 1, j)
和(i, j -1)
,因此只需要填满第一行即可。 - 填表顺序:一行一行填即可。
代码展示
1 |
|
三角形最小路径和
给定一个三角形 triangle
,找出自顶向下的最小路径和。
每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。也就是说,如果正位于当前行的下标 i
,那么下一步可以移动到下一行的下标 i
或 i + 1
。
示例 1:
输入:triangle = [[2],[3,4],[6,5,7],[4,1,8,3]]
输出:11
解释:如下面简图所示:
2
3 4
6 5 7
4 1 8 3
自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。
示例 2:
输入:triangle = [[-10]]
输出:-10
提示:
1 <= triangle.length <= 200triangle[0].length == 1
triangle[i].length == triangle[i - 1].length + 1
-10^4 <= triangle[i][j] <= 10^4
题目解析
此题与杨辉三角一致。
- 状态转移方程式:
dp[i][j]
表示走到位置(i, j)
的最小路径和。因此dp[i][j] = min(dp[i - 1][j], dp[i - 1][j - 1]) + triangle[i][j]
- 容器规模:第
i
层是一个长度为i
的数组。 - base case:任意一个位置
(i, j)
都依赖于(i - 1, j)
和(i, j - 1)
,因此只需要填满第一行即可。 - 填表顺序:一行一行填即可。
代码展示
1 |
|
完全平方数
给你一个整数 n
,返回 和为 n
的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1
、4
、9
和 16
都是完全平方数,而 3
和 11
不是。
示例 1:
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4
示例 2:
输入:n = 13
输出:2
解释:13 = 4 + 9
提示:
1 <= n <= 10^4
题目描述
- 状态转移方程式:
dp[i]
的含义为i
的完全平方数的最少数量。枚举思路是把[1, i]
的每个数字都尝试一次。也就是dp[i] = min(dp[i - j^2] + 1)
,其中, - 容器规模:
i
的范围是[0, n]
,因此 dp[n + 1] - base case:
dp[1] = 1
,i
的完全平方数的数量是1
- 填表顺序,从
2
开始往后填即可。
代码展示
1 |
|
组合总和 Ⅳ
给你一个由 不同 整数组成的数组 nums
,和一个目标整数 target
。请你从 nums
中找出并返回总和为 target
的元素组合的个数。
题目数据保证答案符合 32 位整数范围。
示例 1:
输入:nums = [1,2,3], target = 4
输出:7
解释:
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。
示例 2:
输入:nums = [9], target = 3
输出:0
提示:
1 <= nums.length <= 200
1 <= nums[i] <= 1000
nums 中的所有元素 互不相同
1 <= target <= 1000
题目描述
此题的枚举思路如下:
以样例1为例:对于数字7,我们可以选择1、2或者3,不同的选择最后求和的即可。
- 状态转移方程式:
dp[i]
表示用nums
组成i
有多少种组合方式。dp[i] = Σ(dp[i - num])
其中num∈nums
。 - 容器规模:
i
的范围是[0, n]
。 - base case:
dp[0] = 1
,0 的组合就只有一种(什么都不选) - 顺序:从 1 到 n 填表
代码展示
1 |
|
最长递增子序列
给你一个整数数组 nums
,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7]
是数组 [0,3,1,6,2,2,7]
的子序列。
示例 1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
示例 2:
输入:nums = [0,1,0,3,2,3]
输出:4
示例 3:
输入:nums = [7,7,7,7,7,7,7]
输出:1
提示:
1 <= nums.length <= 2500
10^4 <= nums[i] <= 10^4
题目解析
- 状态转移方程式:
dp[i]
表示包含第i
位的最长递增子序列的长度。因此dp[i] = max(dp[i], dp[j] + 1)
,其中j∈[0, i - 1]
- 容器规模:
i∈[0, length - 1]
- base case:
dp
数组全部赋值为1
(最少肯定是1) - 顺序:从
1
开始往后填表即可
代码展示
1 |
|
最长连续递增序列
给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。
连续递增的子序列 可以由两个下标 l
和 r
(l < r
)确定,如果对于每个 l <= i < r
,都有 nums[i] < nums[i + 1]
,那么子序列 [nums[l], nums[l + 1], …, nums[r - 1], nums[r]]
就是连续递增子序列。
示例 1:
输入:nums = [1,3,5,4,7]
输出:3
解释:最长连续递增序列是 [1,3,5], 长度为3。
尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为 5 和 7 在原数组里被 4 隔开。
示例 2:
输入:nums = [2,2,2,2,2]
输出:1
解释:最长连续递增序列是 [2], 长度为1。
提示:
1 <= nums.length <= 104
-10^9 <= nums[i] <= 10^9
题目解析
- 状态转移方程式:
dp[i]
表示包含第i
位的最长连续递增序列的长度。if nums[i] > nums[i - 1]: dp[i] = dp[i - 1] + 1
- 容器规模:
i∈[0, nums.length - 1]
- base case:
dp
数组全部赋值为 1 - 顺序:
i
从 1 往后
代码展示
1 |
|
最长重复子数组
给两个整数数组 nums1
和 nums2
,返回 两个数组中 公共的 、长度最长的子数组的长度 。
示例 1:
输入:nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7]
输出:3
解释:长度最长的公共子数组是 [3,2,1] 。
示例 2:
输入:nums1 = [0,0,0,0,0], nums2 = [0,0,0,0,0]
输出:5
提示:
1 <= nums1.length, nums2.length <= 1000
0 <= nums1[i], nums2[i] <= 100
题目解析
LCS属于经典的动态规划问题,同时也是面试场考题,同学们一定要熟悉!
状态转移方程式:
dp[i][j]
表示包含nums1
的第i
个数字和包含nums2
的第j
个数字的最长重复子数组的长度。if nums1[i] == nums[j]: dp[i][j] = dp[i - 1][j - 1] + 1; else: dp[i][j] = 0
容器规模:
i
的范围是[0, len1]
,j
的范围是[0, len2]
base case:由于依赖关系如下因此 basecase 应该是第一行和第一列(红色部分)因此,basecase是
dp[0][j]
和dp[i][0]
,二者均为 0。填表顺序:从[1, 1]开始从左往右,从上到下填表即可。
代码展示
1 |
|
最长公共子序列
给定两个字符串 text1
和 text2
,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0
。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace”
是 “abcde”
的子序列,但 “aec”
不是 “abcde”
的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
示例 1:
输入:text1 = “abcde”, text2 = “ace”
输出:3
解释:最长公共子序列是 “ace” ,它的长度为 3 。
示例 2:
输入:text1 = “abc”, text2 = “abc”
输出:3
解释:最长公共子序列是 “abc” ,它的长度为 3 。
示例 3:
输入:text1 = “abc”, text2 = “def”
输出:0
解释:两个字符串没有公共子序列,返回 0 。
提示:
1 <= text1.length, text2.length <= 1000
text1 和 text2 仅由小写英文字符组成。
题目解析
同样属于LCS问题中的一类,属于面试常考题,一定要熟悉。
- 状态转移方程式:
dp[i][j]
表示text1
的前i
个字符和text2
的前j
个字符的最长公共子序列的长度。如果text1[i - 1]
与text2[j - 1]
相同,那么找到了一个公共元素,所以dp[i][j] = dp[i - 1][j - 1] + 1
;如果text1[i - 1]
与text2[j - 1]
不相同,那就看看text1[0, i - 2]
与text2[0, j - 1]
的最长公共子序列和text1[0, i - 1]
与text2[0, j - 2]
的最长公共子序列,取最大的。即:dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
; - 容器规模:
i
属于[0, text1.length]
,j
属于[0, text2.length]
- base case:
dp[0][j]
和dp[i][0]
二者均为0。 - 填表顺序:从
[1, 1]
开始从左往右,从上到下填表即可。
代码展示
1 |
|
两个字符串的删除操作
给定两个单词 word1
和 word2
,返回使得 word1
和 word2
相同 所需的 最小步数。
每步 可以删除任意一个字符串中的一个字符。
示例 1:
输入: word1 = “sea”, word2 = “eat”
输出: 2
解释: 第一步将 “sea” 变为 “ea” ,第二步将 “eat”变为 “ea”
示例 2:
输入:word1 = “leetcode”, word2 = “etco”
输出:4
提示:
1 <= word1.length, word2.length <= 500
word1 和 word2 只包含小写英文字母
题目解析
- 状态转移方程式:
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], dp[i][j-1]) + 1
- 容器规模:
i
属于[0,len1]
,j
属于[0,len2]
- base case:依赖关系与“最长公共子序列”一致,因此basecase是
dp[0][j]
和dp[i][0]
,其中dp[0][j] = j
,dp[i][0] = i
- 填表顺序:从
[1, 1]
开始从左往右,从上到下填表即可。
代码展示
1 |
|
编辑距离
给你两个单词 word1
和 word2
, 请返回将 word1
转换成 word2
所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
示例 1:
输入:word1 = “horse”, word2 = “ros”
输出:3
解释:
horse -> rorse (将 ‘h’ 替换为 ‘r’)
rorse -> rose (删除 ‘r’)
rose -> ros (删除 ‘e’)
示例 2:
输入:word1 = “intention”, word2 = “execution”
输出:5
解释:
intention -> inention (删除 ‘t’)
inention -> enention (将 ‘i’ 替换为 ‘e’)
enention -> exention (将 ‘n’ 替换为 ‘x’)
exention -> exection (将 ‘n’ 替换为 ‘c’)
exection -> execution (插入 ‘u’)
提示:
0 <= word1.length, word2.length <= 500
word1 和 word2 由小写英文字母组成
题目解析
- 状态转移方程式:
dp[i][j]
考虑word1
的前i
个字符和word2
的前j
个字符转为相同的最小步数。如果word1[i]==word2[j]
那么说明这一位是不需要删除的,此时dp[i][j]=dp[i-1][j-1]
。否则,要么删除word1
的第i
个字符,此时dp[i][j] = dp[i - 1][j] + 1
;要么插入一个字符在word1
后面,与word2
匹配,此时dp[i][j] = dp[i][j - 1] + 1
; 要么将word1
的第i
个字符替换成与word2
的第j
个字符一致,此时dp[i][j] = dp[i - 1][j - 1] + 1
,在三种操作中选取最小的即可。 - 容器规模:
i
属于[0,len1]
,j
属于[0,len2]
- base case:依赖关系与“最长公共子序列”一致,因此basecase是
dp[0][j]
和dp[i][0]
,其中dp[0][j] = j, dp[i][0] = i
- 填表顺序:从
[1, 1]
开始从左往右,从上到下填表即可。
代码展示
1 |
|
回文子串
给你一个字符串 s
,请你统计并返回这个字符串中 回文子串 的数目。
回文字符串 是正着读和倒过来读一样的字符串。
子字符串 是字符串中的由连续字符组成的一个序列。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
示例 1:
输入:s = “abc”
输出:3
解释:三个回文子串: “a”, “b”, “c”
示例 2:
输入:s = “aaa”
输出:6
解释:6个回文子串: “a”, “a”, “a”, “aa”, “aa”, “aaa”
提示:
1 <= s.length <= 1000
s 由小写英文字母组成
题目解析
所谓的区间 dp 就是我们定义的 dp 状态是一个区间,然后通过区间的收缩进行状态的转移。
状态转移方程式:
dp[i][j]
表示区间[i,j]
内的子串是否是回文串,这个子串是否是回文串取决于区间端点s[i]
和s[j]
是否相等,且[i + 1, j - 1]
是否为回文串。即dp[i][j] = s[i] == s[j] && dp[i + 1][j - 1]
容器规模:
i
和j
的范围均为[0,length - 1]
base case:
i == j
和i + 1==j
的时候。依赖关系如下任何区间(i, j)
最终都会依赖值对角线的值上(蓝色和黄色的格子),因此i==j
和i+1==j
是basecase。当i==j
的时候,dp[i][j]
表示的是只有一个字符,dp[i][j]=true
;当i + 1==j
的时候,dp[i][j]
表示的是两个连续的字符,因此dp[i][j]
是true
或者false
取决于s[i]==s[j]
。填表顺序:先遍历
j
,再遍历i
,i
逆序遍历。(这样才可以保证我们填表的时候依赖的值已经填好了)
代码展示
1 |
|
最长回文子序列
给你一个字符串 s
,找出其中最长的回文子序列,并返回该序列的长度。
子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。
示例 1:
输入:s = “bbbab”
输出:4
解释:一个可能的最长回文子序列为 “bbbb” 。
示例 2:
输入:s = “cbbd”
输出:2
解释:一个可能的最长回文子序列为 “bb” 。
提示:
1 <= s.length <= 1000
s 仅由小写英文字母组成
题目解析
状态转移方程式:
dp[i][j]
表示区间[i,j]
内的回文子序列的长度,如果区间端点s[i]
和s[j]
相等,则说明这两个端点属于回文子序列中的一部分,即dp[i][j] = dp[i + 1][j - 1] + 2
;否则要么包含s[i]
,要么包含s[j]
,取最大即可,也就是dp[i][j] = max(dp[i + 1][j], dp[i][j - 1])
容器规模:
i
和j
的范围均为[0,length - 1]
base case:
i == j
和i + 1==j
的时候。依赖关系如下任何区间(i, j)
最终都会依赖值对角线的值上(蓝色和黄色的格子),因此i==j
和i+1==j
是basecase。当i==j
的时候,dp[i][j]
表示的是只有一个字符,dp[i][j]=1
;当i + 1==j
的时候,dp[i][j]
表示的是两个连续的字符,因此dp[i][j]
是取决于s[i]==s[j]
,相等的时候是2
,不等则为1
。填表顺序:先遍历
j
,再遍历i
,i
逆序遍历。(这样才可以保证我们填表的时候依赖的值已经填好了)
代码展示
1 |
|