343m. 整数拆分
方法一:动态规划
用时:22m52s
思路
dp数组:dp[i]表示拆分i得到的最大乘积
状态转移方程:将i拆分成j和i-j,当固定j时,dp[i] = max(j * (i - j), j * dp[i - j])
,j * (i - j)就相当于将i拆成了两个数i和i-j,j * dp[i - j]就相当于将i拆成了i和i-j后,i-j继续拆分,dp[i - j]就是i-j拆分得到的最大乘积。遍历所有的j,就相当于遍历了所有拆分情况。
- 时间复杂度: O ( n 2 ) O(n^2) O(n2)。
- 空间复杂度: O ( n ) O(n) O(n)。
C++代码
class Solution {
public:
int integerBreak(int n) {
vector<int> dp(n + 1, 0); // dp数组
dp[2] = 1; // dp数组初始化
for (int i = 3; i <= n; ++i) {
for (int j = 1; j <= i / 2; ++j) {
dp[i] = max(dp[i], max(j * dp[i - j], j * (i - j)));
}
}
return dp[n];
}
};
方法二:动态规划优化
思路
方法一对于每个i,j的取值需要从1遍历到i-1,但其实只用讨论j取2或者3的情况即可。
j=1:j取1不可能得到最大乘积。
j=4:4可以拆分成2和2,22=4,所以j取4的情况和j取2的情况是一致的。
j>=5:此时j不拆分一定比拆分的最大乘积小,所以j取大于等于5一定不是最大乘积。(例如:5<23、6<33、7<23*3)
所以,在计算dp[i]时,只用判断j取2和3的情况,对于每个i计算dp[i]的时间复杂度将为常数级。
- 时间复杂度: O ( n ) O(n) O(n)。
- 空间复杂度: O ( n ) O(n) O(n)。
C++代码
class Solution {
public:
int integerBreak(int n) {
if (n <= 3) return n - 1;
vector<int> dp(n + 1, 0);
dp[2] = 1;
for (int i = 3; i <= n; ++i) {
dp[i] = max(max(2 * (i - 2), 2 * dp[i - 2]), max(3 * (i - 3), 3 * dp[i - 3]));
}
return dp[n];
}
};
看完讲解的思考
妙蛙妙蛙。
代码实现遇到的问题
无。
96m. 不同的二叉搜索树
方法一:动态规划
用时:9m47s
思路
dp数组:dp[i]表示i个节点的BST的种数
如何计算dp[i]:对于整数i,BST的节点的值是1到i,假设根节点的值为j,由BST的性质可知,左子树的节点数就是j-1,右子树的节点数就是i-j,左子树的种数就是dp[j-1],右子树的种树就是dp[i-j],那么以j为根节点的种数就为dp[j-1]*dp[i-j],从1到i遍历j,计算不同节点作为根节点的种数并求和,就能得到dp[i]。
- 时间复杂度: O ( n 2 ) O(n^2) O(n2)。
- 空间复杂度: O ( n ) O(n) O(n)。
C++代码
class Solution {
public:
int numTrees(int n) {
vector<int> dp(n + 1, 0);
dp[0] = 1, dp[1] = 1;
for (int i = 2; i <= n; ++i) {
for (int j = 1; j <= i; ++j) {
dp[i] += dp[j - 1] * dp[i - j];
}
}
return dp[n];
}
};
看完讲解的思考
居然自己一枪A了,嘿嘿。
代码实现遇到的问题
无。
01背包
问题描述
有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
示例:
输入:w = 4, weight = [1, 3, 4], value = [15, 20, 30]
输出:35
解释:装入索引为0和1的物品,总重为1+3<=4,物品价值最大,为15+20=35。
方法一:动态规划
思路
dp数组:使用二维数组,dp[i][j]
表示物品0
到i
放置到容量为j
的背包的最大价值
递推公式:对于dp[i][j]
,我们把它看作对于容量为j
的背包,已经从物品0
到i - 1
中选择物品放入了,此时新来了物品i
,有三种情况:
- 物品i的重量大于背包的容量,物品i无法放进背包:
dp[i][j] = dp[i - 1][j]
- 物品i的重量小于背包的容量,物品
i
不放进背包:dp[i][j] = dp[i - 1][j]
- 物品i的重量小于背包的容量,物品
i
放进背包:物品i
放进背包,那么背包的剩余容量为总容量减去物品i
的重量,即j - weight[i]
,剩余容量能放置物品的最大价值为dp[i - 1][j - weight[i]]
,所以dp[i][j]
就等于剩余容量的最大价值加上物品i
的价值,即dp[i][j] = dp[i - 1][j - weight[i]] + value[i]
对于后两种情况,我们取其中的最大值即为最大价值。
- 时间复杂度: O ( n ∗ w ) O(n*w) O(n∗w),n是物品的个数,w是背包的总容量。
- 空间复杂度: O ( n ∗ w ) O(n*w) O(n∗w)。
C++代码
class Solution {
public:
bool zeroOnePack(int w, vector<int>& weight, vector<int>& value) {
int size = weight.size();
vector<vector<int>> dp(size, vector<int>(w + 1, 1));
// 初始化dp数组
for (int j = 0; j <= w; ++j) {
if (weight[0] > j) dp[0][j] = 0;
else break;
}
// dp
for (int i = 1; i < size; ++i) {
for (int j = 0; j <= w; ++j) {
if (weight[i] > j) dp[i][j] = dp[i - 1][j];
else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
return dp[size - 1][w];
}
};
方法二:动态规划+滚动数组
思路
分析方法一的递推公式dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])
,可以发现dp[i]
的状态只依赖于dp[i - 1]
,所以对于i - 1
之前的dp数组其实不用记录下来,空间上还可以优化,我们可以只用一个一维数组来记录上一时刻的dp数组,然后不断更新这个一维数组即可:dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
。
dp数组初始化:使用滚动数组不需要像二维数组一样初始化,直接初始化为0即可。
遍历顺序:从右往左遍历,因为当前位置的值依赖于前边的值,如果先遍历左边的话会改变前边的值。
- 时间复杂度: O ( n ∗ w ) O(n*w) O(n∗w)。
- 空间复杂度: O ( w ) O(w) O(w)。
C++代码
class Solution {
public:
bool zeroOneBag(int w, vector<int>& weight, vector<int>& value) {
int size = weight.size();
vector<vector<int>> dp(w + 1, 0);
for (int i = 0; i < size; ++i) {
for (int j = w; j >= weight[i]; --j) { // 当前背包容量j要大于物品i的重量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
return dp[w];
}
};
看完讲解的思考
方法二好妙啊
代码实现遇到的问题
无。
416m. 分割等和子集
方法一:动态规划-01背包+滚动数组
用时:14m40s
思路
本题关键在于将题目转化,找到两个子集的元素和相等,其实就是:能否找到一个子集,该子集的和等于数组和sum的一半。
再进一步转化为01背包问题:背包容量为sum / 2
,nums数组相当于每个物品的重量。
dp数组:一维滚动数组,dp[j]表示用容量为j的背包装物品0到i,背包能装的最大重量。
状态转移方程:dp[j] = max(dp[j], dp[j - nums[i]] + nums[i])
- 时间复杂度: O ( s u m 2 ∗ n ) O(\frac{sum}{2}*n) O(2sum∗n),n是数组的大小。
- 空间复杂度: O ( s u m 2 ) O(\frac{sum}{2}) O(2sum)。
C++代码
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = 0;
for (int i = 0; i < nums.size(); ++i) sum += nums[i];
if (sum & 1) return false;
vector<int> dp(sum / 2 + 1, 0);
for (int i = 0; i < nums.size(); ++i) {
for (int j = sum / 2; j >= nums[i]; --j) {
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
if (dp[j] == sum / 2) return true;
}
}
return false;
}
};
看完讲解的思考
这题目居然都能转化成01背包,牛。
代码实现遇到的问题
无。
1049m. 最后一块石头的重量II
方法一:动态规划-01背包
用时:15m57s
思路
关键在于将问题转化为01背包问题:找到一个石头的子集A,使得A的总重量w尽可能地接近所有石头总重量sum的一半,这样将石头粉碎后最后一块石头的重量就是sum - w - w,因为用A和剩下的石头粉碎,A肯定全都没了,所以重量是sum - w,剩下的石头因为和A粉碎,自己也会减少w,所以最终重量就是sum - w - w。
故问题就转换成:背包容量为sum / 2,求装石头能装的最大重量。
- 时间复杂度: O ( s u m 2 ∗ n ) O(\frac{sum}{2}*n) O(2sum∗n),n是石头的个数。
- 空间复杂度: O ( s u m 2 ) O(\frac{sum}{2}) O(2sum)。
C++代码
class Solution {
public:
int lastStoneWeightII(vector<int>& stones) {
int sum = accumulate(stones.begin(), stones.end(), 0);
vector<int> dp(sum / 2 + 1, 0);
for (int i = 0; i < stones.size(); ++i) {
for (int j = sum / 2; j >= stones[i]; --j) dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
}
return sum - 2 * dp[sum / 2];
}
};
看完讲解的思考
无。
代码实现遇到的问题
无。
494. 目标和
方法一:动态规划-01背包
用时:23m12s
思路
数组的和为sum
,我们要将一部分数字做加法,另一部分数字做减法,设做加法的数字的和为x
,则做减法的数字的和为sum - x
,我们的目标是找到x
,使得target = x - (sum - x)
,即x = (target + sum) / 2
。
将题目转换为01背包问题:背包的容量为x
,求和为x
的子集的数量。
- dp数组:
dp[j]
表示背包容量为j,用数组0
到i
的元素来装背包,恰好装满背包的子集数量。 - 状态转移:
dp[j] = dp[j] + dp[j - nums[i]]
。如何理解:新增元素i
,组合的数量是在原先的dp[j]
的基础上增加,因为可以不要新增的元素i
,不要元素i
的组合数量就等于原先的dp[j]
;如果要元素i
,那么组合的数量就等于dp[j - nums[i]]
,要了元素i
,相当于背包容量变成了j - nums[i]
,然后在0
到i - 1
的元素中挑选,恰好装满背包的组合数量,即dp[j - nums[i]]
。 - 初始化dp数组:dp数组初始化相当于不选任何元素放到背包,此时元素和为0,也就是只有
dp[0] = 1
,其余都为0。 - 时间复杂度: O ( t a r g e t + s u m 2 ∗ n ) O(\frac{target + sum}{2}*n) O(2target+sum∗n)。
- 空间复杂度: O ( t a r g e t + s u m 2 ) O(\frac{target + sum}{2}) O(2target+sum)。
C++代码
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int sum = accumulate(nums.begin(), nums.end(), 0);
if (abs(target) > sum || (target + sum) & 1) return 0;
int x = (target + sum) / 2;
vector<int> dp(x + 1, 0);
dp[0] = 1;
for (int i = 0; i < nums.size(); ++i) {
for (int j = x; j >= nums[i]; --j) dp[j] += dp[j - nums[i]];
}
return dp[x];
}
};
方法二:回溯
用时:9m27s
思路
回溯搜索所有的组合,判断是否满足条件,时间复杂度高。
- 时间复杂度: O ( 2 n ) O(2^n) O(2n)。
- 空间复杂度: O ( n ) O(n) O(n)。
C++代码
class Solution {
private:
int x;
int curSum;
int res;
void dfs(vector<int>& nums, int begin) {
if (curSum == x) ++res; // 如果当前和等于x,则种数加一
if (begin == nums.size() || curSum > x) return; // 回溯终止条件:遍历完全部元素,或者当前和已经大于x
for (int i = begin; i < nums.size(); ++i) {
curSum += nums[i];
dfs(nums, i + 1); // 递归
curSum -= nums[i]; // 回溯
}
}
public:
int findTargetSumWays(vector<int>& nums, int target) {
int sum = accumulate(nums.begin(), nums.end(), 0);
x = (target + sum) / 2;
if (abs(target) > sum || (target + sum) & 1) return 0;
curSum = 0;
res = 0;
dfs(nums, 0);
return res;
}
};
看完讲解的思考
无。
代码实现遇到的问题
无。
最后的碎碎念
第一天做dp题目,我:就这?
第二天做dp题目,dp题目:就这?
今天的dp题狠狠地拷打我了。