递归和动态规划都是将原问题拆成多个子问题然后求解,他们之间最本质的区别是,动态规划保存了子问题的解,避免重复计算。
70. Climbing Stairs (Easy)
Leetcode / 力扣
题目描述:有 N 阶楼梯,每次可以上一阶或者两阶,求有多少种上楼梯的方法。
定义一个数组 dp 存储上楼梯的方法数(为了方便讨论,数组下标从 1 开始),dp[i] 表示走到第 i 个楼梯的方法数目。
第 i 个楼梯可以从第 i-1 和 i-2 个楼梯再走一步到达,走到第 i 个楼梯的方法数为走到第 i-1 和第 i-2 个楼梯的方法数之和。
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] 只与 dp[i - 1] 和 dp[i - 2] 有关,因此可以只用两个变量来存储 dp[i - 1] 和 dp[i - 2],使得原来的 O(N) 空间复杂度优化为 O(1) 复杂度。
class Solution {
public:
int climbStairs(int n) {
if(n < 2) return n;
vector<int> dp(n);
dp[0] = 1; dp[1] = 2;
for(int i=2; i<n; i++)
dp[i] = dp[i-1] + dp[i-2];
return dp.back();
}
};
198. House Robber (Easy)
Leetcode / 力扣
题目描述:抢劫一排住户,但是不能抢邻近的住户,求最大抢劫量。
定义 dp 数组用来存储最大的抢劫量,其中 dp[i] 表示抢到第 i 个住户时的最大抢劫量。
由于不能抢劫邻近住户,如果抢劫了第 i -1 个住户,那么就不能再抢劫第 i 个住户,所以
d p [ i ] = m a x ( d p [ i − 2 ] + n u m s [ i ] , d p [ i − 1 ] ) dp[i] = max(dp[i-2] + nums[i], dp[i-1]) 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.empty() ? 0 : nums[0];
vector<int> dp(nums.size(), 0);
dp[0] = nums[0]; dp[1] = max(nums[0], nums[1]);
for(int i=2; i<nums.size(); i++)
dp[i] = max(nums[i]+dp[i-2], dp[i-1]);
return dp[nums.size()-1];
}
};
213. House Robber II (Medium)
Leetcode / 力扣
环状排列意味着第一个房子和最后一个房子中只能选择一个偷窃,因此可以把此环状排列房间问题约化为两个单排排列房间子问题:
1在不偷窃第一个房子的情况下(即 nums[1:]nums[1:]),最大金额是 p1
2在不偷窃最后一个房子的情况下(即 nums[:n-1]nums[:n−1]),最大金额是 p2
综合偷窃最大金额: 为以上两种情况的较大值,即 max(p1,p2)
class Solution {
public:
int rob(vector<int>& nums) {
if(nums.size() <= 1) return nums.empty() ? 0 : nums[0];
return max(robRange(nums, 0, nums.size()-1), robRange(nums, 1, nums.size()));
}
int robRange(vector<int>& nums, int left, int right){
if(right - left <= 1) return nums[left];
vector<int> dp(right, 0);
dp[left] = nums[left]; dp[left+1] = max(nums[left], nums[left+1]);
for(int i=left+2; i<right; i++)
dp[i] = max(nums[i]+dp[i-2], dp[i-1]);
return dp.back();
}
};
题目描述:有 N 个 信 和 信封,它们被打乱,求错误装信方式的数量。
定义一个数组 dp 存储错误方式数量,dp[i] 表示前 i 个信和信封的错误方式数量。假设第 i 个信装到第 j 个信封里面,而第 j 个信装到第 k 个信封里面。根据 i 和 k 是否相等,有两种情况:
综上所述,错误装信数量方式数量为:
程序员代码面试指南-P181
题目描述:假设农场中成熟的母牛每年都会生 1 头小母牛,并且永远不会死。第一年有 1 只小母牛,从第二年开始,母牛开始生小母牛。每只小母牛 3 年之后成熟又可以生小母牛。给定整数 N,求 N 年后牛的数量。
第 i 年成熟的牛的数量为:
64. Minimum Path Sum (Medium)
Leetcode / 力扣
[[1,3,1],
[1,5,1],
[4,2,1]]
Given the above grid map, return 7. Because the path 1→3→1→1→1 minimizes the sum.
题目描述:求从矩阵的左上角到右下角的最小路径和,每次只能向右和向下移动。
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
int m = grid.size(), n = grid[0].size();
for(int i=0; i<m; i++){
for(int j=0; j<n; j++){
if(i==0 && j>0)
grid[i][j] = grid[i][j] + grid[i][j-1];
if(j==0 && i>0)
grid[i][j] = grid[i][j] + grid[i-1][j];
if(i>0 && j>0)
grid[i][j] = grid[i][j] + min(grid[i-1][j], grid[i][j-1]);
}
}
return grid[m-1][n-1];
}
};
62. Unique Paths (Medium)
Leetcode / 力扣
题目描述:统计从矩阵左上角到右下角的路径总数,每次只能向右或者向下移动。
class Solution {
public:
int uniquePaths(int m, int n) {
int path[m][n];
for(int i=0; i<m; i++){
for(int j=0; j<n; j++){
if(i==0 || j==0) path[i][j] = 1;
else path[i][j] = path[i-1][j] + path[i][j-1];
}
}
return path[m-1][n-1];
}
};
也可以直接用数学公式求解,这是一个组合问题。机器人总共移动的次数 S=m+n-2,向下移动的次数 D=m-1,那么问题可以看成从 S 中取出 D 个位置的组合数量,这个问题的解为 C(S, D)。
public int uniquePaths(int m, int n) {
int S = m + n - 2; // 总共的移动次数
int D = m - 1; // 向下的移动次数
long ret = 1;
for (int i = 1; i <= D; i++) {
ret = ret * (S - D + i) / i;
}
return (int) ret;
}
303. Range Sum Query - Immutable (Easy)
Leetcode / 力扣
Given nums = [-2, 0, 3, -5, 2, -1]
sumRange(0, 2) -> 1
sumRange(2, 5) -> -1
sumRange(0, 5) -> -3
求区间 i ~ j 的和,可以转换为 sum[j + 1] - sum[i],其中 sum[i] 为 0 ~ i - 1 的和。
class NumArray {
public:
vector<int> dp;
NumArray(vector<int>& nums) {
dp = nums;
for(int i=1; i<nums.size(); i++){
dp[i] += dp[i-1];
}
}
int sumRange(int i, int j) {
return i==0 ? dp[j] : dp[j]-dp[i-1];
}
};
413. Arithmetic Slices (Medium)
Leetcode / 力扣
A = [0, 1, 2, 3, 4]
return: 6, for 3 arithmetic slices in A:
[0, 1, 2],
[1, 2, 3],
[0, 1, 2, 3],
[0, 1, 2, 3, 4],
[ 1, 2, 3, 4],
[2, 3, 4]
dp[i] 表示以 A[i] 为结尾的等差递增子区间的个数。
当 A[i] - A[i-1] == A[i-1] - A[i-2],那么 [A[i-2], A[i-1], A[i]] 构成一个等差递增子区间。而且在以 A[i-1] 为结尾的递增子区间的后面再加上一个 A[i],一样可以构成新的递增子区间。
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 数组累加的结果。
class Solution {
public:
int numberOfArithmeticSlices(vector<int>& A) {
int res = 0, n = A.size();
vector<int> dp(n, 0);
for(int i=2; i<n; i++){
if(A[i]-A[i-1]==A[i-1]-A[i-2]){
dp[i] = dp[i-1] + 1;
}
res +=dp[i];
}
return res;
}
};
343. Integer Break (Medim)
Leetcode / 力扣
题目描述:For example, given n = 2, return 1 (2 = 1 + 1); given n = 10, return 36 (10 = 3 + 3 + 4).
class Solution {
public:
int integerBreak(int n) {
vector<int> dp(n+1, 1);
for(int i=3; i<n+1; i++){
for(int j=0; j<i; j++){
dp[i] = max(dp[i], max(j*(i-j), j*dp[i-j]));
}
}
return dp.back();
}
};
279. Perfect Squares(Medium)
Leetcode / 力扣
题目描述:For example, given n = 12, return 3 because 12 = 4 + 4 + 4; given n = 13, return 2 because 13 = 4 + 9.
class Solution {
public:
int numSquares(int n) {
vector<int> dp(1,0);
while(dp.size()<=n){
int m = dp.size(), val = m;
for(int i=1; i*i<=m; i++){
val = min(val, dp[m-i*i]+1);
}
dp.push_back(val);
}
return dp.back();
}
};
91. Decode Ways (Medium)
Leetcode / 力扣
题目描述:Given encoded message “12”, it could be decoded as “AB” (1 2) or “L” (12).
分析:本题利用动态规划,需要注意分情况讨论
1.dp[i]为str[0...i]的译码方法总数
2.分情况讨论:(建立最优子结构)
(a).若s[i]='0',那么若s[i-1]='1'or'2',则dp[i]=dp[i-2];否则,return 0
解释:s[i-1]+s[i]被唯一译码,不增加情况
(b).若s[i-1]='1',则dp[i]=dp[i-2]+dp[i-1]
解释:s[i-1]与s[i]分开译码,为dp[i-1];合并译码,为dp[i-2]
(c).若s[i-1]='2'and'1'<=s[i]<='6',则dp[i]=dp[i-1]+dp[i-2]
解释:同上
class Solution {
public:
int numDecodings(string s) {
if(s.empty() || s[0]=='0') return 0;
//dp.size=s.size+1
vector<int> dp(s.size()+1, 0);
dp[0] = 1;
for(int i=1; i<dp.size(); i++){
dp[i] = (s[i - 1] == '0') ? 0 : dp[i - 1];
if(i>1 && (s[i-2]=='1' || (s[i-2]=='2' && s[i-1]<='6'))){
dp[i] = dp[i] + dp[i-2];
}
}
return dp.back();
}
};
已知一个序列 {S1, S2,…,Sn},取出若干数组成新的序列 {Si1, Si2,…, Sim},其中 i1、i2 … im 保持递增,即新序列中各个数仍然保持原数列中的先后顺序,称新序列为原序列的一个 子序列 。
如果在子序列中,当下标 ix > iy 时,Six > Siy,称子序列为原序列的一个 递增子序列 。
定义一个数组 dp 存储最长递增子序列的长度,dp[n] 表示以 Sn 结尾的序列的最长递增子序列长度。对于一个递增子序列 {Si1, Si2,…,Sim},如果 im < n 并且 Sim < Sn,此时 {Si1, Si2,…, Sim, Sn} 为一个递增子序列,递增子序列的长度增加 1。满足上述条件的递增子序列中,长度最长的那个递增子序列就是要找的,在长度最长的递增子序列上加上 Sn 就构成了以 Sn 为结尾的最长递增子序列。因此 dp[n] = max{ dp[i]+1 | Si < Sn && i < n} 。
因为在求 dp[n] 时可能无法找到一个满足条件的递增子序列,此时 {Sn} 就构成了递增子序列,需要对前面的求解方程做修改,令 dp[n] 最小为 1,即:
对于一个长度为 N 的序列,最长递增子序列并不一定会以 SN 为结尾,因此 dp[N] 不是序列的最长递增子序列的长度,需要遍历 dp 数组找出最大值才是所要的结果,max{ dp[i] | 1 <= i <= N} 即为所求。
300. Longest Increasing Subsequence (Medium)
Leetcode / 力扣
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
if(nums.empty()) return 0;
vector<int> dp(nums.size(), 1);
int maxlength = 1;
for(int i=1; i<nums.size(); i++){
for(int j=0; j<i; j++){
if(nums[i] > nums[j]){
dp[i] = max(dp[i], dp[j]+1);
maxlength = max(dp[i], maxlength);
}
}
}
return maxlength;
}
};
使用 Stream 求最大值会导致运行时间过长,可以改成以下形式:
int ret = 0;
for (int i = 0; i < n; i++) {
ret = Math.max(ret, dp[i]);
}
return ret;
以上解法的时间复杂度为 O(N2),可以使用二分查找将时间复杂度降低为 O(NlogN)。
定义一个 tails 数组,其中 tails[i] 存储长度为 i + 1 的最长递增子序列的最后一个元素。对于一个元素 x,
例如对于数组 [4,3,6,5],有:
tails len num
[] 0 4
[4] 1 3
[3] 1 6
[3,6] 2 5
[3,5] 2 null
可以看出 tails 数组保持有序,因此在查找 Si 位于 tails 数组的位置时就可以使用二分查找。
class Solution {
public:
//复杂度O(nlogn)
int lengthOfLIS(vector<int>& nums) {
if(nums.empty()) return 0;
vector<int> dp(nums.size()+1, 0);
int len=1;
dp[len] = nums[0];
for(int i=1; i<nums.size(); i++){
if(nums[i] > dp[len]) dp[++len] = nums[i];
else{
int left=1, right=len, pos=0;
while(left <= right){
int mid = left + (right-left)/2;
if(dp[mid] < nums[i]){
left = mid + 1;
pos = mid;
}else{
right = mid -1;
}
}
dp[pos+1] = nums[i];
}
}
return len;
}
// 复杂度 O(n^2)
// int lengthOfLIS(vector& nums) {
// if(nums.empty()) return 0;
// vector dp(nums.size(), 1);
// int maxlength = 1;
// for(int i=1; i
// for(int j=0; j
// if(nums[i] > nums[j]){
// dp[i] = max(dp[i], dp[j]+1);
// maxlength = max(dp[i], maxlength);
// }
// }
// }
// return maxlength;
// }
};
646. Maximum Length of Pair Chain (Medium)
Leetcode / 力扣
Input: [[1,2], [2,3], [3,4]]
Output: 2
Explanation: The longest chain is [1,2] -> [3,4]
题目描述:对于 (a, b) 和 (c, d) ,如果 b < c,则它们可以构成一条链。
class Solution {
public:
//动态规划 时间复杂度O(n2) 空间O(N)
int findLongestChain(vector<vector<int>>& pairs) {
sort(pairs.begin(), pairs.end(), [&](const vector<int> &A, const vector<int> &B) {
return (A[0] < B[0]) || (A[0]==B[0]&&A[1]<B[1]);
});
if(pairs.empty()) return 0;
int len=1, max_y = pairs[0][1];
int n = pairs.size();
vector<int> dp(n, 1);
for(int i=1; i<n; i++){
for(int j=0; j<i; j++){
if(pairs[j][1]<pairs[i][0]){
dp[i] = max(dp[i], dp[j]+1);
}
}
}
return dp[n-1];
}
//贪心算法:时间复杂度O(nlogn)
// int findLongestChain(vector>& pairs) {
// sort(pairs.begin(), pairs.end(), [&](const vector &A, const vector &B) {
// return A[1] < B[1];
// });
// if(pairs.empty()) return 0;
// int len=1, max_y=pairs[0][1];
// for(int i=1; i
// if(pairs[i][0] > max_y){
// len++;
// max_y = pairs[i][1];
// }
// }
// return len;
// }
};
376. Wiggle Subsequence (Medium)
Leetcode / 力扣
Input: [1,7,4,9,2,5]
Output: 6
The entire sequence is a wiggle sequence.
Input: [1,17,5,10,13,15,10,5,16,8]
Output: 7
There are several subsequences that achieve this length. One is [1,17,10,13,10,16,8].
Input: [1,2,3,4,5,6,7,8,9]
Output: 2
要求:使用 O(N) 时间复杂度求解。
class Solution {
public:
//O(N^2)
int wiggleMaxLength(vector<int>& nums) {
if(nums.empty()) return 0;
vector<int> p(nums.size(), 1);
vector<int> q(nums.size(), 1);
for(int i=1; i<nums.size(); i++){
for(int j=0; j<i; j++){
if(nums[i] > nums[j]) p[i] = max(p[i], q[j]+1);
else if(nums[i] < nums[j]) q[i] = max(q[i], p[j]+1);
}
}
return max(p.back(), q.back());
}
// O(N)
// public int wiggleMaxLength(int[] nums) {
// if(nums.length<2) return nums.length;
// int down=1;
// int up=1;
// for(int i=1;i
// if(nums[i]>nums[i-1]){
// up=down+1;
// }else if(nums[i]
// down=up+1;
// }
// }
// return Math.max(up,down);
// }
};
对于两个子序列 S1 和 S2,找出它们最长的公共子序列。
定义一个二维数组 dp 用来存储最长公共子序列的长度,其中 dp[i][j] 表示 S1 的前 i 个字符与 S2 的前 j 个字符最长公共子序列的长度。考虑 S1i 与 S2j 值是否相等,分为两种情况:
综上,最长公共子序列的状态转移方程为:
对于长度为 N 的序列 S1 和长度为 M 的序列 S2,dp[N][M] 就是序列 S1 和序列 S2 的最长公共子序列长度。
与最长递增子序列相比,最长公共子序列有以下不同点:
1143. Longest Common SubsequenceLeetcode / 力扣
1.对于0位置未添加空字符串
2.dp[i][j]dp[i][j]表示的是s1[0...i-1]s1[0...i−1]与s2[0...j-1]s2[0...j−1]的最长公共子序列的长度,要求的是dp[m-1][n-1]dp[m−1][n−1]
3.当s1[i]==s2[j]s1[i]==s2[j],说明这两个字符是公共的字符,只要考察其子问题,dp[i-1][j-1]dp[i−1][j−1]的长度即可,在此基础上+1,
4.当s1[i]!=s2[j]s1[i]!=s2[j],说明这两个字符不是公共的字符,只要考察其两个子问题,dp[i-1][j],dp[i][j-1]dp[i−1][j],dp[i][j−1],取maxmax
动态转移方程:
d p [ i ] [ j ] = { d p [ i − 1 ] [ j − 1 ] + 1 , s 1 [ i ] = = s 2 [ j ] m a x ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − 1 ] ) , s 1 [ i ] ! = s 2 [ j ] dp[i][j]= \begin{cases} dp[i-1][j-1]+1, s1[i]==s2[j]\\ max(dp[i-1][j], dp[i][j-1]), s1[i]!=s2[j]\end{cases} dp[i][j]={ dp[i−1][j−1]+1,s1[i]==s2[j]max(dp[i−1][j],dp[i][j−1]),s1[i]!=s2[j]
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int m = text1.size(), n = text2.size();
vector<vector<int>> dp = vector(m + 1, vector(n + 1, 0));
for(int i=1; i<=m; i++){
for(int j=1; j<=n; j++){
if(text1[i-1]==text2[j-1])
dp[i][j] = dp[i - 1][j - 1] + 1;
else
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
return dp[m][n];
}
};
有一个容量为 N 的背包,要用这个背包装下物品的价值最大,这些物品有两个属性:体积 w 和价值 v。
定义一个二维数组 dp 存储最大价值,其中 dp[i][j] 表示前 i 件物品体积不超过 j 的情况下能达到的最大价值。设第 i 件物品体积为 w,价值为 v,根据第 i 件物品是否添加到背包中,可以分两种情况讨论:
第 i 件物品可添加也可以不添加,取决于哪种情况下最大价值更大。因此,0-1 背包的状态转移方程为:
// W 为背包总体积
// N 为物品数量
// weights 数组存储 N 个物品的重量
// values 数组存储 N 个物品的价值
public int knapsack(int W, int N, int[] weights, int[] values) {
int[][] dp = new int[N + 1][W + 1];
for (int i = 1; i <= N; i++) {
int w = weights[i - 1], v = values[i - 1];
for (int j = 1; j <= W; j++) {
if (j >= w) {
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - w] + v);
} else {
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[N][W];
}
空间优化
在程序实现时可以对 0-1 背包做优化。观察状态转移方程可以知道,前 i 件物品的状态仅与前 i-1 件物品的状态有关,因此可以将 dp 定义为一维数组,其中 dp[j] 既可以表示 dp[i-1][j] 也可以表示 dp[i][j]。此时,
因为 dp[j-w] 表示 dp[i-1][j-w],因此不能先求 dp[i][j-w],防止将 dp[i-1][j-w] 覆盖。也就是说要先计算 dp[i][j] 再计算 dp[i][j-w],在程序实现时需要按倒序来循环求解。
public int knapsack(int W, int N, int[] weights, int[] values) {
int[] dp = new int[W + 1];
for (int i = 1; i <= N; i++) {
int w = weights[i - 1], v = values[i - 1];
for (int j = W; j >= 1; j--) {
if (j >= w) {
dp[j] = Math.max(dp[j], dp[j - w] + v);
}
}
}
return dp[W];
}
无法使用贪心算法的解释
0-1 背包问题无法使用贪心算法来求解,也就是说不能按照先添加性价比最高的物品来达到最优,这是因为这种方式可能造成背包空间的浪费,从而无法达到最优。考虑下面的物品和一个容量为 5 的背包,如果先添加物品 0 再添加物品 1,那么只能存放的价值为 16,浪费了大小为 2 的空间。最优的方式是存放物品 1 和物品 2,价值为 22.
id | w | v | v/w |
---|---|---|---|
0 | 1 | 6 | 6 |
1 | 2 | 10 | 5 |
2 | 3 | 12 | 4 |
变种
完全背包:物品数量为无限个
多重背包:物品数量有限制
多维费用背包:物品不仅有重量,还有体积,同时考虑这两种限制
其它:物品之间相互约束或者依赖
416. Partition Equal Subset Sum (Medium)
Leetcode / 力扣
Input: [1, 5, 11, 5]
Output: true
Explanation: The array can be partitioned as [1, 5, 5] and [11].
定义一个一维的 dp 数组,其中 dp[i] 表示原数组是否可以取出若干个数字,其和为i。那么最后只需要返回 dp[target] 就行了。初始化 dp[0] 为 true,由于题目中限制了所有数字为正数,就不用担心会出现和为0或者负数的情况。关键问题就是要找出状态转移方程了,需要遍历原数组中的数字,对于遍历到的每个数字 nums[i],需要更新 dp 数组,既然最终目标是想知道 dp[target] 的 boolean 值,就要想办法用数组中的数字去凑出 target,因为都是正数,所以只会越加越大,加上 nums[i] 就有可能会组成区间 [nums[i], target] 中的某个值,那么对于这个区间中的任意一个数字j,如果 dp[j - nums[i]] 为 true 的话,说明现在已经可以组成 j-nums[i] 这个数字了,再加上 nums[i],就可以组成数字j了,那么 dp[j] 就一定为 true。如果之前 dp[j] 已经为 true 了,当然还要保持 true,所以还要 ‘或’ 上自身,于是状态转移方程如下:
dp[j] = dp[j] || dp[j - nums[i]] (nums[i] <= j <= target)
有了状态转移方程,就可以写出代码了,这里需要特别注意的是,第二个 for 循环一定要从 target 遍历到 nums[i],而不能反过来,想想为什么呢?因为如果从 nums[i] 遍历到 target 的话,假如 nums[i]=1 的话,那么 [1, target] 中所有的 dp 值都是 true,因为 dp[0] 是 true,dp[1] 会或上 dp[0],为 true,dp[2] 会或上 dp[1],为 true,依此类推,完全使的 dp 数组失效了,
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = accumulate(nums.begin(), nums.end(), 0), target = sum>>1;
if(sum & 1) return false; //判断sum是否奇数
vector<bool> dp(target+1, false);
dp[0] = true;
for(int num: nums){
for(int i=target; i>=num; i--){
dp[i] = dp[i] || dp[i-num];
}
}
return dp[target];
}
};
494. Target Sum (Medium)Leetcode / 力扣
Input: nums is [1, 1, 1, 1, 1], S is 3.
Output: 5
Explanation:
-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
There are 5 ways to assign symbols to make the sum of nums be target 3.
该问题可以转换为 Subset Sum 问题,从而使用 0-1 背包的方法来求解。
class Solution {
public:
// 动态优化
int findTargetSumWays(vector<int>& nums, int S) {
int n = nums.size();
// 其中 dp[i][j] 表示到第 i-1 个数字且和为j的情况总数
vector<unordered_map<int, int>> dp(n+1);
dp[0][0] = 1;
for(int i=0; i<n; i++){
for(auto &a : dp[i]){
int sum = a.first, cnt = a.second;
dp[i+1][sum + nums[i]] += cnt;
dp[i+1][sum - nums[i]] += cnt;
}
}
return dp[n][S];
}
// 递归dfs
// int findTargetSumWays(vector& nums, int S) {
// int res = 0;
// helper(nums, S, 0, res);
// return res;
// }
// void helper(vector& nums, long S, int start, int& res){
// if(start >= nums.size()){
// if(S==0) res++;
// return;
// }
// helper(nums, S-nums[start], start+1, res);
// helper(nums, S+nums[start], start+1, res);
// }
};
474. Ones and Zeroes (Medium)
Leetcode / 力扣
Input: Array = {"10", "0001", "111001", "1", "0"}, m = 5, n = 3
Output: 4
Explanation: There are totally 4 strings can be formed by the using of 5 0s and 3 1s, which are "10","0001","1","0"
这是一个多维费用的 0-1 背包问题,有两个背包大小,0 的数量和 1 的数量。
public int findMaxForm(String[] strs, int m, int n) {
if (strs == null || strs.length == 0) {
return 0;
}
int[][] dp = new int[m + 1][n + 1];
for (String s : strs) {
// 每个字符串只能用一次
int ones = 0, zeros = 0;
for (char c : s.toCharArray()) {
if (c == '0') {
zeros++;
} else {
ones++;
}
}
for (int i = m; i >= zeros; i--) {
for (int j = n; j >= ones; j--) {
dp[i][j] = Math.max(dp[i][j], dp[i - zeros][j - ones] + 1);
}
}
}
return dp[m][n];
}
322. Coin Change (Medium)
Leetcode / 力扣
Example 1:
coins = [1, 2, 5], amount = 11
return 3 (11 = 5 + 5 + 1)
Example 2:
coins = [2], amount = 3
return -1.
题目描述:给一些面额的硬币,要求用这些硬币来组成给定面额的钱数,并且使得硬币数量最少。硬币可以重复使用。
因为硬币可以重复使用,因此这是一个完全背包问题。完全背包只需要将 0-1 背包的逆序遍历 dp 数组改为正序遍历即可。
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
int MAX = amount + 1;
vector<int> dp(amount+1, MAX);
dp[0] = 0;
for(int i=1; i<amount+1; i++){
for(int j=0; j<coins.size(); j++){
if(coins[j] <= i){
dp[i] = min(dp[i], dp[i-coins[j]]+1);
}
}
}
return dp[amount] >= MAX ? -1 : dp[amount];
}
};
518. Coin Change 2 (Medium)
Leetcode / 力扣
Input: amount = 5, coins = [1, 2, 5]
Output: 4
Explanation: there are four ways to make up the amount:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
完全背包问题,使用 dp 记录可达成目标的组合数目。
正确的子问题定义应该是,problem(k,i) = problem(k-1, i) + problem(k, i-k)
即前k个硬币凑齐金额i的组合数等于前k-1个硬币凑齐金额i的组合数加上在原来i-k的基础上使用硬币的组合数。说的更加直白一点,那就是用前k的硬币凑齐金额i,要分为两种情况开率,一种是没有用前k-1个硬币就凑齐了,一种是前面已经凑到了i-k,现在就差第k个硬币了。
状态数组就是DP[k][i]
, 即前k个硬币凑齐金额i的组合数。
这里不再是一维数组,而是二维数组。第一个维度用于记录当前组合有没有用到硬币k,第二个维度记录现在凑的金额是多少?如果没有第一个维度信息,当我们凑到金额i的时候,我们不知道之前有没有用到硬币k。
因为这是个组合问题,我们不关心硬币使用的顺序,而是硬币有没有被用到。是否使用第k个硬币受到之前情况的影响。
状态转移方程如下
if 金额数大于硬币
DP[k][i] = DP[k-1][i] + DP[k][i-k]
else
DP[k][i] = DP[k-1][i]
为了降低空间复杂度,需要重新定义我们的子问题
此时的子问题是,对于硬币从0到k,我们必须使用第k个硬币的时候,当前金额的组合数。
因此状态数组DP[i]表示的是对于第k个硬币能凑的组合数
状态转移方程如下
DP[i] = DP[i] + DP[i-k]
class Solution1 {
public:
int change(int amount, vector<int>& coins) {
int dp[amount+1];
memset(dp, 0, sizeof(dp)); //初始化数组为0
dp[0] = 1;
for (int coin : coins){
//枚举硬币
for (int j = 1; j <= amount; j++){
//枚举金额
if (j < coin) continue; // coin不能大于amount
dp[j] += dp[j-coin];
}
}
return dp[amount];
}
};
// 错误代码,[1,2,1],[1,1,2]这里算作两次(错误),本题属于组合问题不是排列问题,即[1,2,1],[1,1,2]
//是同一个组合,leetcode70.爬楼梯属于排列问题
class Solution2 {
public:
int change(int amount, vector<int>& coins) {
int dp[amount+1];
memset(dp, 0, sizeof(dp)); //初始化数组为0
dp[0] = 1;
for (int j = 1; j <= amount; j++){
//枚举金额
for (int i = 0; i < coins.size(): i++){
int coin = coins[i]; //枚举硬币
if (j < coin) continue; // coin不能大于amount
dp[j] += dp[j-coin];
}
}
return dp[amount];
}
};
NOTE:好了,继续之前的问题,这里的内外循环能换吗?
显然不能,因为我们这里定义的子问题是,必须选择第k个硬币时,凑成金额i的方案。如果交换了,我们的子问题就变了,那就是对于金额i, 我们选择硬币的方案。
同样的,我们回答之前爬楼梯的留下的问题,原循环结构对应的子问题是,对于楼梯数i, 我们的爬楼梯方案。第1种循环结构则是,固定爬楼梯的顺序,我们爬楼梯的方案。第2种循环下,对于楼梯3,你可以先2再1,或者先1再2,但是对于第1种循环,楼梯3的情况下只能先1再2一种情况000000
139. Word Break (Medium)
Leetcode / 力扣
s = "leetcode",
dict = ["leet", "code"].
Return true because "leetcode" can be segmented as "leet code".
dict 中的单词没有使用次数的限制,因此这是一个完全背包问题。
该问题涉及到字典中单词的使用顺序,也就是说物品必须按一定顺序放入背包中,例如下面的 dict 就不够组成字符串 “leetcode”:
["lee", "tc", "cod"]
求解顺序的完全背包问题时,对物品的迭代应该放在最里层,对背包的迭代放在外层,只有这样才能让物品按一定顺序放入背包中。 定义 dp 数组跟找出状态转移方程,先来看 dp 数组的定义,这里我们就用一个一维的 dp 数组,其中 dp[i] 表示范围 [0, i) 内的子串是否可以拆分,注意这里 dp 数组的长度比s串的长度大1,是因为我们要 handle 空串的情况,我们初始化 dp[0] 为 true,然后开始遍历。注意这里我们需要两个 for 循环来遍历,因为此时已经没有递归函数了,所以我们必须要遍历所有的子串,我们用j把 [0, i) 范围内的子串分为了两部分,[0, j) 和 [j, i),其中范围 [0, j) 就是 dp[j],范围 [j, i) 就是 s.substr(j, i-j),其中 dp[j] 是之前的状态,我们已经算出来了,可以直接取,只需要在字典中查找 s.substr(j, i-j) 是否存在了,如果二者均为 true,将 dp[i] 赋为 true,并且 break 掉,此时就不需要再用j去分 [0, i) 范围了,因为 [0, i) 范围已经可以拆分了。最终我们返回 dp 数组的最后一个值,就是整个数组是否可以拆分的布尔值了
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
unordered_set<string> wordSet(wordDict.begin(), wordDict.end());
int n = s.size();
vector<bool> dp(n+1, false);
dp[0] = true;
for(int i=1; i<n+1; i++){
for(int j=0; j<i; j++){
if(dp[j] && wordSet.count(s.substr(j, i-j))){
dp[i] = true;
break;
}
}
}
return dp.back();
}
};
377. Combination Sum IV (Medium)
Leetcode / 力扣
nums = [1, 2, 3]
target = 4
The possible combination ways are:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
Note that different sequences are counted as different combinations.
Therefore the output is 7.
涉及顺序的完全背包,属于排列问题。
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
vector<unsigned long> dp(target+1, 0);
dp[0] = 1;
for(int i=1; i<target+1; i++){
for(int num : nums){
if(i < num) continue;
dp[i] += dp[i - num];
}
}
return dp[target];
}
};
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
max( 选择 rest , 选择 sell )
解释:今天我没有持有股票,有两种可能:
要么是我昨天就没有持有,然后今天选择 rest,所以我今天还是没有持有;
要么是我昨天持有股票,但是今天我 sell 了,所以我今天没有持有股票了。
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
max( 选择 rest , 选择 buy )
解释:今天我持有着股票,有两种可能:
要么我昨天就持有着股票,然后今天选择 rest,所以我今天还持有着股票;
要么我昨天本没有持有,但今天我选择 buy,所以今天我就持有股票了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CMkDP3GS-1605528288365)(E:\MEGA\leetcode\CS-Notes-master\assets\1.png)]
309. Best Time to Buy and Sell Stock with Cooldown(Medium)
Leetcode / 力扣
题目描述:交易之后需要有一天的冷却时间。
public:
int maxProfit(vector<int>& prices) {
int index = prices.size();
if(index==0)
return 0;
int dp_i_0 = 0, dp_i_1 = INT_MIN;
int dp_pre_0 = 0;
for(int i=0; i<index; i++){
int temp = dp_i_0;
dp_i_0 = max(dp_i_0, dp_i_1+prices[i]);
dp_i_1 = max(dp_i_1, dp_pre_0-prices[i]);
dp_pre_0 = temp;
}
return dp_i_0;
}
714. Best Time to Buy and Sell Stock with Transaction Fee (Medium)
Leetcode / 力扣
Input: prices = [1, 3, 2, 8, 4, 9], fee = 2
Output: 8
Explanation: The maximum profit can be achieved by:
Buying at prices[0] = 1
Selling at prices[3] = 8
Buying at prices[4] = 4
Selling at prices[5] = 9
The total profit is ((8 - 1) - 2) + ((9 - 4) - 2) = 8.
题目描述:每交易一次,都要支付一定的费用。
class Solution {
public:
int maxProfit(vector<int>& prices, int fee) {
int index = prices.size();
if(index==0)
return 0;
int dp_i_0 = 0, dp_i_1 = INT_MIN;
for(int i=0; i<index; i++){
dp_i_0 = max(dp_i_0, dp_i_1+prices[i]);
dp_i_1 = max(dp_i_1, dp_i_0-prices[i]-fee);
}
return dp_i_0;
}
};
123. Best Time to Buy and Sell Stock III (Hard)
Leetcode / 力扣
class Solution {
public:
int maxProfit(vector<int>& prices) {
int max_k = 2, n = prices.size();
if(n<=1)
return 0;
int dp[n][max_k+1][2];
memset(dp, 0, sizeof(dp));
for(int i=0; i<n; i++){
for(int k=max_k; k>=1; k--){
if(i==0){
dp[i][k][0]=0;
dp[i][k][1]=-prices[0];
continue;
}
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]);
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]);
}
}
return dp[n - 1][max_k][0];
}
};
188. Best Time to Buy and Sell Stock IV (Hard)
Leetcode / 力扣
class Solution {
public:
int maxProfit_k_inf(vector<int>& prices) {
int index = prices.size();
if(index==0)
return 0;
int dp_i_0 = 0, dp_i_1 = INT_MIN;
for(int i=0; i<index; i++){
dp_i_0 = max(dp_i_0, dp_i_1+prices[i]);
dp_i_1 = max(dp_i_1, dp_i_0-prices[i]);
}
return dp_i_0;
}
int maxProfit(int k, vector<int>& prices) {
int max_k = k, n = prices.size();
if(n<=1)
return 0;
if (max_k > n / 2)
return maxProfit_k_inf(prices);
int dp[n][max_k+1][2];
memset(dp, 0, sizeof(dp));
for(int i=0; i<n; i++){
for(int k=max_k; k>=1; k--){
if(i==0){
dp[i][k][0]=0;
dp[i][k][1]=-prices[0];
continue;
}
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]);
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]);
}
}
return dp[n - 1][max_k][0];
}
};
583. Delete Operation for Two Strings (Medium)
Leetcode / 力扣
Input: "sea", "eat"
Output: 2
Explanation: You need one step to make "sea" to "ea" and another step to make "eat" to "ea".
方法1.不使用 LCS 的动态规划 [Accepted]:
算法
此方法中,我们不通过求解 LCS 再求解删除次数的方法来求解问题,我们直接使用动态规划来求解删除次数。
我们使用二维数组 dp,现在 dp[i][j] 表示 s1串前 i 个字符和 s2 串前 j 个字符匹配的最少删除次数。同样我们逐行求解 dp 数组。为了求出 dp[i][j] ,我们只考虑 2中情况:
s1[i-1] 和 s2[j-1] 匹配,那么我们只需要让 dp[i][j] 赋值为 dp[i-1][j-1] 即可。这是因为两个字符串能匹配的字符不需要被删除。
字符 s1[i-1] 和 s2[j-1]不匹配。这种情况下,我们需要考虑删除两个字符中的哪一个,同时需要增加一次删除操作。两种可能的选择是 dp[i-1][j] 和 dp[i][j-1]。我们从中选择较小值。
最后,dp[m][n] 就是我们需要的最少删除次数,m 和 n 分别是字符串 s1 和字符串 s2 的长度。
方法2.最长公共子序列 - 动态规划 [Accepted]
算法
另一个获得 lcs 值的办法是动态规划。我们来看看它的实现思想和具体方法。
我们使用一个二维数组 dp, dp[i][j] 表示 s1 前 i 个字符和 s2 前 j 个字符中最长公共子序列。我们逐行填充 dp 数组。
对于每一个 dp[i][j],我们有 2 种选择:
1.字符 s1[i-1] 和 s2[j-1] 匹配,那么 dp[i][j] 会比两个字符串分别考虑到前 i-1i−1 个字符 和 j-1j−1 个字符的公共子序列长度多 1 。所以 dp[i][j]被更新为 dp[i][j] = dp[i-1][j-1] + 1。注意到 dp[i-1][j-1]已经被求解过了,所以可以直接使用。
2.字符 s1[i-1]和 s2[j-1]不匹配,这种情况下我们不能直接增加已匹配子序列的长度,但我们可以将之前已经求解过的最长公共子序列的长度作为当前最长公共子序列的长度。但是我们应该选择哪一个呢?事实上此时我们有 2 种选择。我们可以删除 s1 或者 s2 的最后一个字符然后将对应的 dp 数组的值作比较,也就是取 dp[i−1][j] 和 dp[i][j−1] 的较大值。
最后,与前面方法类似的,我们获得删除次数 m + n - 2*dp[m][n]m+n−2∗dp[m][n] ,其中 m 和 n 分别是 s1 和 s2 的字符串长度,,dp[m][n] 是两个字符串的最长公共子序列。
可以转换为求两个字符串的最长公共子序列问题。
class Solution {
public:
int minDistance(string word1, string word2) {
int m=word1.size(), n=word2.size();
int dp[m+1][n+1];
memset(dp, 0, sizeof(dp));
for(int i=0; i<=m; i++){
for(int j=0; j<=n; j++){
if(i==0 || j==0)
dp[i][j] = i+j;
else if(word1[i-1] == word2[j-1])
dp[i][j] = dp[i-1][j-1];
else
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + 1;
}
}
return dp[m][n];
}
//使用最大公共子字符串 时间复杂度O(m*n), 空间复杂度O(M*N)
// int minDistance(string word1, string word2) {
// int m=word1.size(), n=word2.size();
// int dp[m+1][n+1];
// memset(dp, 0, sizeof(dp));
// for(int i=0; i<=m; i++){
// for(int j=0; j<=n; j++){
// if(i==0||j==0)
// continue;
// if(word1[i-1] == word2[j-1])
// dp[i][j] = dp[i-1][j-1] + 1;
// else
// dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
// }
// }
// return m+n-2*dp[m][n];
// }
};
72. Edit Distance (Hard)
Leetcode / 力扣
给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
插入一个字符
删除一个字符
替换一个字符
题目描述:修改一个字符串成为另一个字符串,使得修改次数最少。一次修改操作包括:插入一个字符、删除一个字符、替换一个字符。
class Solution {
public:
int minDistance(string word1, string word2) {
int m = word1.size(), n = word2.size();
int dp[m+1][n+1];
// base case
for(int i=0; i<=m; i++)
dp[i][0] = i;
for(int j=1; j<=n; j++)
dp[0][j] = j;
// 自底向上求解
for(int i=1; i<=m; i++)
for(int j=1; j<=n; 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][j-1]+1, dp[i-1][j-1]+1)
);
return dp[m][n];
}
};
650. 2 Keys Keyboard (Medium)
Leetcode / 力扣
题目描述:最开始只有一个字符 A,问需要多少次操作能够得到 n 个字符 A,每次操作可以复制当前所有的字符,或者粘贴。
Input: 3
Output: 3
Explanation:
Intitally, we have one character 'A'.
In step 1, we use Copy All operation.
In step 2, we use Paste operation to get 'AA'.
In step 3, we use Paste operation to get 'AAA'.
class Solution {
public:
// 动态规划
int minSteps(int n) {
if(n==1) return 0;
vector<int> dp(n+1, 0);
dp[1] = 1;
for(int i=2; i<=n; i++){
dp[i] = i;
for(int j=1; j<i; j++){
if(i % j ==0)
dp[i] = min(dp[i], dp[i/j]+j);
}
}
return dp[n];
}
// 递归
// int minSteps(int n) {
// if(n == 1) return 0;
// int res = n;
// for(int i=n-1; i>1; i--){
// if(n%i == 0)
// res = min(res, minSteps(n/i)+i);
// }
// return res;
// }
};
参考:
https://github.com/CyC2018/CS-Notes