这是整理的LeetCode上的动态规划专题
背包问题
我们将LeetCode上的背包问题进行一个整理,希望之后再遇到以下问题能够快速解决。
组合问题
-
如果是0-1背包,即数组中的元素不可重复使用,nums放在外循环,target在内循环,且内循环倒序;
for num in nums: for i in range(target, nums-1, -1):
-
如果是完全背包,即数组中的元素可重复使用,nums放在外循环,target在内循环。且内循环正序
for num in nums: for i in range(nums, target+1):
-
如果组合问题需考虑元素之间的顺序,需将target放在外循环,将nums放在内循环。
for i in range(1, target+1): for num in nums:
377. 组合总和 Ⅳ
给你一个由 不同 整数组成的数组 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
这明显是一个完全背包问题,而且组合问题需考虑元素之间的顺序。
本题也可以转换为爬楼梯问题,楼梯的阶数一共为target,每次爬楼的阶数可为nums中的数目。
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
vector<long> dp(target+1, 0);
dp[0] = 1;
for(int i = 1; i <= target; i++){
for(int j = 0; j < nums.size(); j++){
if(i >= nums[j]){
dp[i] += dp[i - nums[j]];
if(dp[i]>INT_MAX) dp[i]%=INT_MAX;
}
}
}
return dp[target];
}
};
494. 目标和
给定一个非负整数数组,a1, a2, …, an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。
返回可以使最终数组和为目标数 S 的所有添加符号的方法数。
示例:
输入:nums: [1, 1, 1, 1, 1], S: 3
输出:5
解释:
-1+1+1+1+1 = 3
+1-1+1+1+1 = 3
+1+1-1+1+1 = 3
+1+1+1-1+1 = 3
+1+1+1+1-1 = 3
一共有5种方法让最终目标和为3。
这里数组中的每个元素都使用,但是可以看作是0-1背包的改版 -1-1背包问题。
我们定义
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]表示数组nums中
0
−
i
0-i
0−i的元素进行加减可得到j的方法数量。
因为数组nums为非负数组,因此我们首先求nums数组的和sum,
d
p
dp
dp数组开的大小为
(
n
u
m
s
.
s
i
z
e
(
)
,
2
∗
s
u
m
+
1
)
(nums.size(), 2*sum+1)
(nums.size(),2∗sum+1)
转移方程为:
d
p
[
i
]
[
j
]
=
d
p
[
i
−
1
]
[
j
−
n
u
m
s
[
i
]
]
+
d
p
[
i
−
1
]
[
j
+
n
u
m
s
[
i
]
]
dp[i][j] = dp[i-1][j-nums[i]] + dp[i-1][j+nums[i]]
dp[i][j]=dp[i−1][j−nums[i]]+dp[i−1][j+nums[i]]
class Solution {
public:
// dp[i][j]定义为数组nums中0-i的元素进行加减可以得到j的方法数量
int findTargetSumWays(vector<int>& nums, int target) {
int sum = 0;
for(int i=0; i<nums.size(); ++i){
sum+=nums[i];
}
if(abs(target) > sum) return 0;
vector<vector<int>> dp(nums.size(), vector<int>(2*sum+1, 0));
if (nums[0] == 0) {
dp[0][sum] = 2;
} else {
dp[0][sum+nums[0]] = 1;
dp[0][sum-nums[0]] = 1;
}
for(int i=1; i<nums.size(); ++i){
for(int j=0; j<2*sum+1; ++j){
int l = (j-nums[i])>=0 ? j-nums[i] : 0;
int r = (j+nums[i])<2*sum+1 ? j+nums[i] : 0;
dp[i][j] = dp[i-1][l] + dp[i-1][r];
}
}
return dp[nums.size()-1][sum+target];
}
};
518. 零钱兑换 II
给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。
示例 1:
输入: amount = 5, coins = [1, 2, 5]
输出: 4
解释: 有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
示例 2:
输入: amount = 3, coins = [2]
输出: 0
解释: 只用面额2的硬币不能凑成总金额3。
示例 3:
输入: amount = 10, coins = [10]
输出: 1
这题和爬楼梯本质是一样的。
爬楼梯题目是可爬一层或两层,一共n层楼,共有多少种爬楼方式。
这里的硬币如果是[1, 2],金额数为n,共有多少凑成此数目的组合数。
class Solution {
public:
int change(int amount, vector<int>& coins) {
vector<int> dp(amount+1, 0);
// dp[i]:凑成金额i有多少种方式
dp[0] = 1;
for(int j=0; j<coins.size(); ++j){
for(int i=1; i<=amount; ++i){
if(i >= coins[j]){
dp[i] += dp[i-coins[j]];
}
}
}
return dp[amount];
}
};
139. 单词拆分
给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
说明:
- 拆分时可以重复使用字典中的单词。
- 你可以假设字典中没有重复的单词。
示例 1:
输入: s = “leetcode”, wordDict = [“leet”, “code”]
输出: true
解释: 返回 true 因为 “leetcode” 可以被拆分成 “leet code”。
示例 2:
输入: s = “applepenapple”, wordDict = [“apple”, “pen”]
输出: true
解释: 返回 true 因为 “applepenapple” 可以被拆分成 “apple pen apple”。
注意你可以重复使用字典中的单词。
示例 3:
输入: s = “catsandog”, wordDict = [“cats”, “dog”, “sand”, “and”, “cat”]
输出: false
416. 分割等和子集
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1:
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。
示例 2:
输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。
474. 一和零
给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
请你找出并返回 strs 的最大子集的大小,该子集中 最多 有 m 个 0 和 n 个 1 。
如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。
示例 1:
输入:strs = [“10”, “0001”, “111001”, “1”, “0”], m = 5, n = 3
输出:4
解释:最多有 5 个 0 和 3 个 1 的最大子集是 {“10”,“0001”,“1”,“0”} ,因此答案是 4 。
其他满足题意但较小的子集包括 {“0001”,“1”} 和 {“10”,“1”,“0”} 。{“111001”} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。
示例 2:
输入:strs = [“10”, “0”, “1”], m = 1, n = 1
输出:2
解释:最大的子集是 {“0”, “1”} ,所以答案是 2
322. 零钱兑换
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
你可以认为每种硬币的数量是无限的。
示例 1:
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
我们发现每种硬币的数量是无限的,那么可以套用完全背包问题模板,具体代码如下:
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
//dp[i]:组合成i金额所需要的最少的硬币个数
vector<int> dp(amount+1, 1e4+1);
dp[0] = 0;
for(int i=0; i<coins.size(); ++i){
for(int j=coins[i]; j<=amount; ++j){
dp[j] = min(dp[j], dp[j-coins[i]] + 1);
}
}
return dp[amount] == 1e4+1 ? -1:dp[amount];
}
};
斐波那契数列专题
###70. 爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例 1:
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1 阶 + 1 阶
2 阶
本题就是典型的斐波那契数列,如果用递归解决,那么会出现很多重复子问题,将会很耗时,因此我们只需要保存之前的结果。
定义一个数组dp存储上楼梯的方法数,
d
p
[
i
]
=
d
p
[
i
−
1
]
+
d
p
[
i
−
2
]
dp[i] = dp[i-1] + dp[i-2]
dp[i]=dp[i−1]+dp[i−2]
我们可以继续优化,仅用两个变量来存储dp[i-1]和dp[i-2]即可。
class Solution {
public:
int climbStairs(int n) {
if(n <= 2) return n;
int pre2 = 1, pre1 = 2;
for(int i=2; i<n; ++i){
int cur = pre2 + pre1;
pre2 = pre1;
pre1 = cur;
}
return pre1;
}
};
198. 打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 1:
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
dp[i]表示抢到第i家获得的最高金额,dp[i] = max(dp[i-2] + nums[i], dp[i-1])
class Solution {
public:
int rob(vector<int>& nums) {
// if(nums.size() == 1) return nums[0];
// vector<int> dp(nums.size(), 0); // dp[i]表示抢到第i家获得的最高金额
//dp[i] = max(dp[i-2] + nums[i], dp[i-1])
// dp[0] = nums[0];
// dp[1] = max(dp[0], nums[1]);
// for(int i=2; i<nums.size(); ++i){
// dp[i] = max(dp[i-2] + nums[i], dp[i-1]);
// }
// return dp[nums.size()-1];
int pre2 = 0, pre1 = 0;
for(int i=0; i<nums.size(); ++i){
int cur = max(pre2 + nums[i], pre1);
pre2 = pre1;
pre1 = cur;
}
return pre1;
}
};
213. 打家劫舍 II
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
示例 1:
输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
示例 2:
输入:nums = [1,2,3,1]
输出:4
解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
class Solution {
public:
int rob(vector<int>& nums) {
if(nums.size() == 1) return nums[0];
int res;
// 如果偷了第一家,那么最后一家就不能偷
int pre2 = 0, pre1 = 0;
for(int i=0; i<nums.size()-1; ++i){
int cur = max(pre2 + nums[i], pre1);
pre2 = pre1;
pre1 = cur;
}
res = pre1;
// 如果偷了最后一家,那么第一家就不能偷
pre2 = 0, pre1 = 0;
for(int i=nums.size()-1; i>0; --i){
int cur = max(pre2 + nums[i], pre1);
pre2 = pre1;
pre1 = cur;
}
return max(pre1, res);
}
};
其他
32. 最长有效括号
给你一个只包含 ‘(’ 和 ‘)’ 的字符串,找出最长有效(格式正确且连续)括号子串的长度。
示例 1:
输入:s = “(()”
输出:2
解释:最长有效括号子串是 “()”
示例 2:
输入:s = “)()())”
输出:4
解释:最长有效括号子串是 “()()”
class Solution {
public:
int longestValidParentheses(string s) {
int res = 0, n = s.length();
vector<int> dp(n, 0);
for(int i=1; i<n; i++){
if (s[i] == ')') {
if (s[i-1] == '(') {
dp[i] = (i >= 2 ? dp[i-2]:0) + 2;
} else if (i-dp[i-1]>0 && s[i-dp[i-1]-1] == '(') {
// 假设有这个例子 ()(()())
// 在遇到第最后一个')'的时候,我们不仅需要加上dp[6](就是倒数第二个')'的最长括号长度),还需要加上dp[1](就是第一个')'的最长括号长度),并加上自身的2
dp[i] = dp[i-1] + ((i-dp[i-1])>=2 ? dp[i-dp[i-1]-2] : 0) + 2;
}
res = max(dp[i], res);
}
}
return res;
}
};
72. 编辑距离
给你两个单词 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’)
为什么使用动态规划来解决本题,首先我们看到题目需要求的是“最少操作数”,这种求最优大概率就是dp能解决。
然后我们分析一下状态转换,
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j] 代表 word1 到 i 位置转换成 word2 到 j 位置需要最少步数。
如果word1第i个字母与word2第j个字母相等,那么很容易得到
d
p
[
i
]
[
j
]
=
d
p
[
i
−
1
]
[
j
−
1
]
dp[i][j] = dp[i-1][j-1]
dp[i][j]=dp[i−1][j−1]
否则,就是经过三个操作加一,插入一个字符就是
d
p
[
i
−
1
]
[
j
−
1
]
dp[i-1][j-1]
dp[i−1][j−1],删除一个字符表示
d
p
[
i
−
1
]
[
j
]
dp[i-1][j]
dp[i−1][j],替换一个字符表示
d
p
[
i
]
[
j
−
1
]
dp[i][j-1]
dp[i][j−1]
d
p
[
i
]
[
j
]
=
m
i
n
(
d
p
[
i
−
1
]
[
j
−
1
]
,
d
p
[
i
−
1
]
[
j
]
,
d
p
[
i
]
[
j
−
1
]
)
+
1
dp[i][j] = min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]) + 1
dp[i][j]=min(dp[i−1][j−1],dp[i−1][j],dp[i][j−1])+1
class Solution {
public:
int minDistance(string word1, string word2) {
int dp[505][505];
int len1 = word1.length(), len2 = word2.length();
for (int i = 0; i <= len2; i++) {
dp[0][i] = i;
}
for (int i = 0; i <= len1; i++) {
dp[i][0] = i;
}
for (int i = 1; i <= len1; i++) {
for (int j = 1; j <= len2; j++) {
if (word1[i-1] == word2[j-1]) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = min(dp[i - 1][j - 1], min(dp[i - 1][j], dp[i][j - 1])) + 1;
}
}
}
return dp[len1][len2];
}
};