前言
题单LeetCode 热题 100的动态规划部分,给出解析。
70. Climbing Stairs
题目描述
You are climbing a staircase. It takes n
steps to reach the top.
Each time you can either climb 1
or 2
steps. In how many distinct ways can you climb to the top?
Example 1:
1 | Input: n = 2 |
Example 2:
1 | Input: n = 3 |
Constraints:
1 <= n <= 45
解题方法
这个爬楼梯问题可以分析为经典的动态规划问题:
问题分析:
到达第 n
级台阶的方法:
- 从
n - 1
级台阶走 1 步到达。 - 从
n - 2
级台阶走 2 步到达。
则到达第 n
级台阶的总方法数 f(n)
可以表示为前两级台阶的方法数之和:
$$
f(n) = f(n-1) + f(n-2)
$$
这个递推公式与斐波那契数列类似:
初始条件:
$f(1)=1$,只有一种方法到达第一级台阶;$f(2)=2$,有两种方法到达第二级台阶(一步一步走或者一步直接两级)。
代码实现
1 | class Solution: |
变量解释:
prev1
表示前一级台阶的爬法数,prev2
表示前两级台阶的爬法数。current
表示当前台阶的爬法数,根据公式,current = prev1 + prev2
prev2 = prev1
:将prev1
的值赋给prev2
,相当于在下一次迭代中,prev2
就代表了当前台阶的前两级台阶的爬法数。prev1 = current
:将current
的值赋给prev1
,相当于在下一次迭代中,prev1
就代表了当前台阶的爬法数。return prev1
:当循环结束后,prev1
就表示爬到第n
级台阶的总方法数,返回这个值作为最终结果。
复杂度分析
时间复杂度:$O(n)$
空间复杂度:$O(1)$
118. Pascal’s Triangle
题目描述
Given an integer numRows
, return the first numRows of Pascal’s triangle .
In Pascal’s triangle , each number is the sum of the two numbers directly above it as shown:
Example 1:
1 | Input: numRows = 5 |
Example 2:
1 | Input: numRows = 1 |
Constraints:
1 <= numRows <= 30
解题方法
这个问题要求我们生成帕斯卡三角形的前 numRows
行。帕斯卡三角形的构造规则是,每个数字等于它正上方的两个数字之和,第一行和第二行都是由 1 组成的。
解题思路:
- 第一行:只有一个元素 1。
- 第二行:也是两个元素,都是 1。
- 从第三行开始:每个元素是上一行中相邻两个元素的和。比如第
i
行的第j
个元素是triangle[i-1][j-1] + triangle[i-1][j]
,其中triangle[i-1]
是上一行。 - 边界条件:每一行的第一个和最后一个元素都是 1。
解法:
我们可以通过循环构造每一行,然后把每一行添加到最终的结果中。
代码实现
1 | class Solution: |
复杂度分析
时间复杂度:$O(n^2)$,其中 $n$ 是 $numRows$。
空间复杂度:$O(1)$。
198. House Robber
题目链接
You are a professional robber planning to rob houses along a street. Each house has a certain amount of money stashed, the only constraint stopping you from robbing each of them is that adjacent houses have security systems connected and it will automatically contact the police if two adjacent houses were broken into on the same night.
Given an integer array nums
representing the amount of money of each house, return the maximum amount of money you can rob tonight without alerting the police.
Example 1:
1 | Input: nums = [1,2,3,1] |
Example 2:
1 | Input: nums = [2,7,9,3,1] |
Constraints:
1 <= nums.length <= 100
0 <= nums[i] <= 400
解题方法
动态规划需要找到子问题,原问题是“从全部房子中能够偷到的最大金额”,子问题设为“从 k
个房子中能偷到的最大金额”,用 f(k)
表示。
子问题是参数化的,我们定义的子问题中有一个参数 k
。如果有 n
个房子的话,一共有 n
个子问题。动态规划实际上是通过求这些子问题的解,来得出原问题的解。这些子问题需要具备两种性质:
- 原问题能够用子问题表示。
- 一个子问题的解能够用其他子问题的解求出。
状态转移方程:
对于每个房子有两种选择:
- 抢当前房子:那么最大金额应该是
dp[i-2] + nums[i]
,即抢了当前房子后,再加上抢到i-2
房子的最大金额。 - 不抢当前房子:那么最大金额应该是
dp[i-1]
,即前一个房子抢到的最大金额。
综合以上两种情况,状态转移方程为: $$dp[i]=max(dp[i−1],dp[i−2]+nums[i])$$
初始条件:
- $dp[0] = nums[0]$:因为只有一个房子时,只能抢这个房子。
- $dp[1] = max(nums[0], nums[1])$:只有两个房子时,选择抢金额较大的一个。
最终结果:返回 $dp[n-1]$。
代码实现
1 | class Solution: |
空间复杂度优化:
在上面的动态规划解法中,我们使用了一个长度为 n
的数组 dp
来存储每个房子抢劫到的最大金额。然而,我们注意到在计算 dp[i]
时,只需要用到 dp[i-1]
和 dp[i-2]
的值。因此,我们可以使用两个变量来替代整个数组,从而将空间复杂度优化为 $O(1)$。
优化思路:
我们只需要两个变量 prev1
和 prev2
来分别表示 dp[i-1]
和 dp[i-2]
,然后逐步更新这两个变量即可。
1 | class Solution: |
代码解释:
- 特殊情况处理:如果只有一个房子,直接返回那间房子的金额。
- 初始化:
prev2
代表dp[i-2]
,prev1
代表dp[i-1]
。 - 迭代更新:从第三个房子开始,计算当前房子的最大金额,然后更新
prev2
和prev1
。 - 返回结果:最后,
prev1
中存储的就是抢劫到最后一个房子的最大金额。
复杂度分析
时间复杂度:仍然是 $O(n)$,因为我们需要遍历每一个房子。
空间复杂度:从 $O(n)$ 优化为 $O(1)$,只需要常量级的额外空间。
279. Perfect Squares
题目描述
Given an integer n
, return the least number of perfect square numbers that sum to n
.
A perfect square is an integer that is the square of an integer; in other words, it is the product of some integer with itself. For example, 1
, 4
, 9
, and 16
are perfect squares while 3
and 11
are not.
Example 1:
1 | Input: n = 12 |
Example 2:
1 | Input: n = 13 |
Constraints:
1 <= n <= 10^4
解题方法
动态规划
要解决这个问题,可以使用动态规划的方法。我们需要创建一个数组 dp
,其中 dp[i]
表示求和为 i
所需的最小完全平方数的数量。
算法步骤:
初始化:创建一个大小为
n + 1
的数组dp
,并将所有元素初始化为无穷大
(或一个足够大的数),因为我们要寻找最小值。然后设置dp[0] = 0
,表示和为 0 时不需要任何数。填充 dp 数组:对于每一个从 1 到 n 的数
i
,遍历所有可能的完全平方数j^2
,并更新dp[i]
:- 对于每个
i
,我们检查所有小于等于i
的完全平方数j^2
,其中j
从 1 开始,直到j^2
超过i
。 - 更新状态:
dp[i] = min(dp[i], dp[i - j^2] + 1)
。这里dp[i - j^2] + 1
表示在和为i - j^2
的基础上再加一个完全平方数j^2
。
- 对于每个
返回结果:最终,
dp[n]
即为答案。
BFS
BFS 可以用于从 n
开始,逐步减去所有可能的完全平方数,直到达到 0。每次减去一个完全平方数就记录一步,直到找到最短路径。
算法步骤:
- 预计算完全平方数:计算所有小于等于
n
的完全平方数。 - 使用队列进行 BFS:初始化一个队列,将
n
入队,记录步骤数。 - 遍历队列:每次从队列中取出一个数,检查减去每个完全平方数后的结果:
- 如果结果为 0,返回当前步骤数。
- 如果结果大于 0,继续入队。
数学定理
使用数学定理法,主要是依赖于Lagrange’s Four Square Theorem,该定理表明:任何正整数都可以表示为不超过四个完全平方数的和。这意味着我们只需检查一个数可以表示为多少个完全平方数的和,最多只需考虑 1 到 4 个平方数。
算法步骤:
- 检查特殊情况:
- 如果
n
是一个完全平方数,则直接返回 1。 - 检查
n
是否可以表示为两个完全平方数的和(可以用公式判断)。 - 如果
n
是 4 的某个幂次加上 3 的形式,则返回 4。
- 如果
- 返回结果:
- 如果没有特殊情况,则最多返回 4。
代码实现
动态规划
1 | class Solution: |
BFS
1 | class Solution: |
数学定理
1 | class Solution: |
复杂度分析
方法 | 时间复杂度 | 空间复杂度 |
---|---|---|
动态规划 | $O(n*√n)$,其中 n 是输入值。 | $O(n)$,$dp$ 数组。 |
BFS | 最坏情况下为 $O(n*√n)$。 | $O(n)$,队列。 |
数学定理 | $O(√n)$,主要用于判断是否为完全平方数。 | $O(1)$,常量空间。 |
322. Coin Change
题目描述
You are given an integer array coins
representing coins of different denominations and an integer amount
representing a total amount of money.
Return the fewest number of coins that you need to make up that amount. If that amount of money cannot be made up by any combination of the coins, return -1
.
You may assume that you have an infinite number of each kind of coin.
Example 1:
1 | Input: coins = [1,2,5], amount = 11 |
Example 2:
1 | Input: coins = [2], amount = 3 |
Example 3:
1 | Input: coins = [1], amount = 0 |
Constraints:
1 <= coins.length <= 12
1 <= coins[i] <= 2^31 - 1
0 <= amount <= 10^4
解题方法
我们可以定义一个数组 dp
,其中 dp[i]
表示组成金额 i
所需的最少硬币数量。我们的目标是找到 dp[amount]
的值。
具体步骤如下:
初始化
dp
数组,长度为amount + 1
,初始值都设为amount + 1
,因为这是不可能达到的最大值。唯一的例外是dp[0]
,其值为0
,因为组成金额0
所需的硬币数量为0
。遍历
dp
数组,从1
到amount
。对于每个金额i
,检查每种硬币coin
,如果i >= coin
,则更新dp[i]
,更新的方式是dp[i] = min(dp[i], dp[i - coin] + 1)
。最后,检查
dp[amount]
的值。如果dp[amount]
仍然是初始值amount + 1
,说明无法组成该金额,返回-1
。否则,返回dp[amount]
。
代码实现
1 | class Solution(object): |
复杂度分析
时间复杂度:$O(n)$
空间复杂度:$O(n)$
139. Word Break
题目描述
Given a string s
and a dictionary of strings wordDict
, return true
if s
can be segmented into a space-separated sequence of one or more dictionary words.
Note that the same word in the dictionary may be reused multiple times in the segmentation.
Example 1:
1 | Input: s = "leetcode", wordDict = ["leet","code"] |
Example 2:
1 | Input: s = "applepenapple", wordDict = ["apple","pen"] |
Example 3:
1 | Input: s = "catsandog", wordDict = ["cats","dog","sand","and","cat"] |
Constraints:
1 <= s.length <= 300
1 <= wordDict.length <= 1000
1 <= wordDict[i].length <= 20
s
andwordDict[i]
consist of only lowercase English letters.- All the strings of
wordDict
are unique .
解题方法
这个问题可以使用动态规划来解决。我们可以定义一个布尔数组 dp
,其中 dp[i]
表示字符串 s
的前 i
个字符是否可以用字典中的单词进行分割。
思路:
- 初始化
dp
数组长度为len(s) + 1
,所有值初始化为False
。dp[0]
设为True
,因为空字符串可以被认为是被空的字典成功分割的。 - 遍历字符串
s
的每个位置i
,对于每个位置再遍历字典中的每个单词word
:- 如果
word
的长度小于等于i
,并且s[i-len(word):i]
等于word
,那么如果dp[i-len(word)]
为True
,则将dp[i]
设为True
。
- 如果
- 最终,
dp[len(s)]
的值即为结果,表示整个字符串是否可以被成功分割。
代码实现
1 | class Solution: |
复杂度分析
时间复杂度:$O(n)$
空间复杂度:$O(n)$
300. Longest Increasing Subsequence
题目描述
Given an integer array nums
, return the length of the longest strictly increasing subsequence.
Example 1:
1 | Input: nums = [10,9,2,5,3,7,101,18] |
Example 2:
1 | Input: nums = [0,1,0,3,2,3] |
Example 3:
1 | Input: nums = [7,7,7,7,7,7,7] |
Constraints:
1 <= nums.length <= 2500
-10^4 <= nums[i] <= 10^4
Follow up: Can you come up with an algorithm that runs in O(n log(n))
time complexity?
解题方法
定义一个 dp
数组,dp[i]
代表 nums
以 nums[i]
为结尾的最长递增子序列长度。
状态转移方程:
- 设 $j∈[0,i)$,即每一轮计算新的 $dp[i]$ 时,需要遍历 $[0,i)$ 列表区间
- 当 $nums[i] > nums[j]$ 时,表明 $nums[i]$ 可以接到递增序列的末尾,此时 $dp[i]=dp[j]+1$。
- $nums[i]<=nums[j]$ 时,跳过,因为此时 $nums[i]$ 不能够作为递增子序列的末尾。
- 转移方程:$dp[i]=max(dp[i],dp[j]+1),j∈[0,i),i∈[0,n]$
- 初始化
dp
为值为 1 的数组,因为每个元素都可以单独成为一个子序列。
改进:
将整个 dp
数组设计为一个排序列表,在需要计算最长递增子序列时,使用二分查找遍历 dp
数组,找到需要插入的下标位置,之后进行判断。
- **初始化一个空的列表
sub
**:- 这个列表用于保存当前找到的最长递增子序列的末尾元素。
- **遍历数组
nums
**:- 对于每个元素
num
,使用二分查找来确定num
在sub
中的位置。 - 如果
num
大于sub
中的所有元素(即num
是新的最大值),就将其添加到sub
的末尾。 - 如果
num
不是最大的,找到它在sub
中应该插入的位置,并替换掉该位置的元素。这样可以保持sub
的递增性质,并为后续元素提供更大的可能性。
- 对于每个元素
- 返回
sub
的长度:sub
的长度即为最长严格递增子序列的长度。
代码实现
1 | class Solution: |
改进:
1 | class Solution: |
复杂度分析
时间复杂度:$O(n^2)$到$O(nlogn)$。
空间复杂度:$O(n)$。
152. Maximum Product Subarray
题目描述
Given an integer array nums
, find a subarray that has the largest product, and return the product.
The test cases are generated so that the answer will fit in a 32-bit integer.
Example 1:
1 | Input: nums = [2,3,-2,4] |
Example 2:
1 | Input: nums = [-2,0,-1] |
Constraints:
1 <= nums.length <= 2 * 10^4
-10 <= nums[i] <= 10
- The product of any subarray of
nums
is guaranteed to fit in a 32-bit integer.
解题方法
要找到一个整数数组 nums
中乘积最大的子数组,并返回这个最大乘积,我们可以使用动态规划的思路。这个问题的关键在于考虑负数对乘积的影响。
解决思路
- 初始化变量:
- 维护两个变量
max_product
和min_product
,分别表示当前遍历到的子数组中的最大乘积和最小乘积。 - 初始化
max_product
和min_product
为数组的第一个元素,result
也初始化为第一个元素。
- 维护两个变量
- 遍历数组:
- 对于数组中的每一个元素
num
,我们需要考虑三种情况:num
本身。num
乘以前一步的max_product
。num
乘以前一步的min_product
。
- 因为乘以负数会导致最大值变成最小值,最小值变成最大值,所以我们需要同时更新
max_product
和min_product
。 - 更新
result
,即记录遍历过程中遇到的最大乘积。
- 对于数组中的每一个元素
- 最终结果:
- 遍历结束后,
result
中存储的即为最大子数组乘积。
- 遍历结束后,
代码实现
1 | class Solution: |
代码思想:
- 初始化
max_product
,min_product
和result
为数组的第一个元素。 - 遍历数组,从第二个元素开始更新
max_product
和min_product
。 - 每次更新
result
为当前最大值。 - 返回
result
,即最大乘积的子数组的乘积。
复杂度分析
时间复杂度:$O(n)$
空间复杂度:$O(1)$
416. Partition Equal Subset Sum
题目描述
Given an integer array nums
, return true
if you can partition the array into two subsets such that the sum of the elements in both subsets is equal or false
otherwise.
Example 1:
1 | Input: nums = [1,5,11,5] |
Example 2:
1 | Input: nums = [1,2,3,5] |
Constraints:
1 <= nums.length <= 200
1 <= nums[i] <= 100
解题方法
这道题是典型的「分割等和子集问题」,它可以通过动态规划(Dynamic Programming, DP)来解决。目标是将数组分成两个和相等的子集。如果数组的总和为奇数,则不可能进行这样的划分,因此可以直接返回 false
。
解题思路:
- 数组总和为偶数:我们可以尝试找到一个子集,它的和等于
总和 / 2
。如果能找到这样的子集,说明剩余的元素和也是总和 / 2
,问题就可以解决。 - 动态规划:我们用一个布尔数组
dp
来表示是否可以找到和为某个值的子集。dp[i]
表示能否找到和为i
的子集。初始时,dp[0] = true
(表示空集的和为0是可能的)。
代码实现
1 | class Solution: |
复杂度分析
时间复杂度:$O(n * target)$,其中n
是数组nums
的长度,target
是数组和的一半。
空间复杂度:$O(n)$。
32. Longest Valid Parentheses
题目描述
Given a string containing just the characters '('
and ')'
, return the length of the longest valid (well-formed) parentheses substring.
Example 1:
1 | Input: s = "(()" |
Example 2:
1 | Input: s = ")()())" |
Example 3:
1 | Input: s = "" |
Constraints:
0 <= s.length <= 3 * 10^4
s[i]
is'('
, or')'
.
解题方法
动态规划
使用动态规划(Dynamic Programming,DP)来求解最长有效括号子串问题也是一种常见的方法。动态规划的思路是通过构建一个 dp
数组来记录每个位置的有效括号子串的长度。
思路:
- 设
dp[i]
表示以i
位置结尾的最长有效括号子串的长度。 - 状态转移方程:
- 如果
s[i] == '('
,则dp[i] = 0
,因为以(
结尾的子串不可能是有效的。 - 如果
s[i] == ')'
,我们需要考虑两种情况:- 如果
s[i-1] == '('
,说明我们找到一个有效的括号对()
,此时dp[i] = dp[i-2] + 2
(如果i-2
有效括号子串存在的话)。 - 如果
s[i-1] == ')'
,需要看s[i - dp[i-1] - 1] == '('
,如果是(
,则dp[i] = dp[i-1] + 2 + dp[i - dp[i-1] - 2]
,表示匹配到的括号对子串的长度加上前面有效括号子串的长度。
- 如果
- 如果
栈
这道题的目的是求一个只包含(
和)
的字符串中,最长有效括号子串的长度。
思路:
- 栈法: 使用一个栈来帮助判断括号的匹配。栈中的元素保存的是括号的索引。
- 初始时,栈中放一个
-1
,用来表示一个基准值,方便计算有效子串的长度。 - 遍历字符串的每个字符:
- 如果遇到
(
,将当前索引压入栈中。 - 如果遇到
)
,则弹出栈顶元素,表示匹配到了一个(
。- 如果栈为空,说明当前
)
没有匹配的(
,将当前索引压入栈中作为新的基准值。 - 如果栈不为空,则通过
当前索引 - 栈顶元素
来计算有效括号子串的长度。
- 如果栈为空,说明当前
- 如果遇到
- 初始时,栈中放一个
代码实现
动态规划
1 | class Solution: |
栈
1 | class Solution: |
复杂度分析
方法 | 时间复杂度 | 空间复杂度 |
---|---|---|
动态规划 | $O(n)$ | $O(n)$ |
栈 | $O(n)$ | $O(n)$ |