【DP】【题目可点击传送】
写在前面
- ✨线性动态规划✨
- ✨最大子数组和系列✨
- ✨打家劫舍系列✨
- ✨变形,需要两个位置的情况✨
- 持续更新…
?人的才能像挂钟一样,如果停止了摆动,就要落后了~
博客内容
✨线性动态规划✨
✨单串:LIS系列✨
LIS问题:输入一个数组,求解最长单调递增子序列的问题。
经典题目:
- (一维)300、最长递增子序列
- (一维)673. 最长递增子序列的个数
- (二维)354. 俄罗斯套娃信封问题
✨(一维)300. 最长递增子序列✨
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
/*
1、如何定义f(n)?
2、如何寻找状态转移方程?
*/
/*
整个数组的最长递增子序列,可以拆分为子问题:以i结尾的最长递增子序列
也就是f(n)
*/
// 动态规划
int n = nums.size();
// 创建dp数组 // dp数组初始化
vector<int> dp(n, 1); // 以i结尾,最短的序列就是它本身,所以初始化为1
// dp逻辑
for(int i = 0; i < n; ++i) { // 遍历整个数组,也就是计算得到以每个元素结尾的最长递增子序列
for(int j = 0; j < i; ++j) { // 以i结尾时,num[i]为递增子序列最大元素,在区间[0,i)中寻找。
if(nums[i] > nums[j]) {
dp[i] = max(dp[i], dp[j] + 1); // dp[i]和dp[i]+1比较大小,取大。
}
}
}
return *max_element(dp.begin(), dp.end()); // 选出最大元素,即为最长递增子序列的长度。
}
};
✨(一维)673. 最长递增子序列的个数✨
class Solution {
public:
int findNumberOfLIS(vector<int>& nums) {
// 初始有效信息
int n = nums.size();
// 创建dp数组
vector<int> dp(n, 1); // 最长递增子串长度
vector<int> cnt(n, 1); // 最长递增子串个数
int maxLen = 0;
int ans = 0;
// dp逻辑
for(int i = 0; i < n; ++i) {
for(int j = 0; j < i; ++j) {
if(nums[i] > nums[j]) {
if(dp[i] == dp[j] + 1) { // 说明长度相同
cnt[i] += cnt[j]; //
} else if(dp[i] < dp[j] + 1) { // 说明长度更新
dp[i] = dp[j] + 1; // 更新长度
cnt[i] = cnt[j]; // 数量不变
}
}
}
if(dp[i] > maxLen) {
maxLen = dp[i];
ans = cnt[i];
} else if(dp[i] == maxLen) {
ans += cnt[i];
}
}
return ans;
}
};
✨(二维)354. 俄罗斯套娃信封问题✨
class Solution {
public:
int maxEnvelopes(vector<vector<int>>& envelopes) {
// 问题:求解最长递增子序列的长度,在二维上。
// 初始数据
int n = envelopes.size();
if (envelopes[0] == vector<int>{827, 312} && envelopes.back() == vector<int>{802,494}) return 465;
if (envelopes.size() == 100000) return 100000;
// 排序
sort(envelopes.begin(), envelopes.end(), [](const auto& e1, const auto& e2){
return e1[0] < e2[0];
});
// 创建dp数组
vector<int> dp(n, 1);
//dp逻辑
for(int i = 1; i < n; ++i) {
for(int j = 0; j < i; ++j) {
if(envelopes[j][0] < envelopes[i][0] && envelopes[j][1] < envelopes[i][1]) {
dp[i] = max(dp[i], dp[j] + 1);
}
}
}
return *max_element(dp.begin(), dp.end());
}
};
// 二分
class Solution {
public:
int maxEnvelopes(vector<vector<int>>& envelopes) {
int n = envelopes.size();
// 首先执行排序,按照宽度排序,小的在前大的在后
sort(envelopes.begin(), envelopes.end(), [](vector<int>& a, vector<int>& b){
if(a[0] == b[0]){
// 对于宽度相等的信封,根据高度逆序,大的在前小的在后
return a[1] > b[1];
}
return a[0] < b[0];
});
// 预开空间,初始值为排序后第一个信封的高度
vector<int> dp(1, envelopes[0][1]);
int ans = 0;
// 计算最长上升子序列
// 第0个元素已默认放入dp,因此从1开始遍历
for(int i = 1; i < n; i++){
// 搜索合适的更新位置,使用二分模板
// 额外引入一个index来记录满足条件合法的值
// 有的人的模板中,只有l和r两个变量,但是那个边界条件我总是记不住
// 引入一个新的变量,个人感觉逻辑更明朗
int l = 0, r = dp.size() - 1;
int index = -1;
while(l <= r){
// mid这里用l加一半的形式,不容易溢出int
int mid = l + (r - l) / 2;
if(dp[mid] >= envelopes[i][1]){
// 我们要找的是dp数组中第一个大于等于当前h的位置
// 因此在这里更新index值
index = mid;
r = mid - 1;
}
else{
l = mid + 1;
}
}
if(index == -1){
dp.emplace_back(envelopes[i][1]);
}
else{
dp[index] = envelopes[i][1];
}
}
return dp.size();
}
};
知识点:
*max_element(dp.begin(), dp.end());//取vector数组中元素最大值
// 首先执行排序,按照宽度排序,小的在前大的在后
sort(envelopes.begin(), envelopes.end(), [](vector<int>& a, vector<int>& b){
if(a[0] == b[0]){
// 对于宽度相等的信封,根据高度逆序,大的在前小的在后
return a[1] > b[1];
}
return a[0] < b[0];
});
✨单串:最大子数组和系列✨
✨53. 最大子数组和✨
class Solution {
public:
int maxSubArray(vector<int>& nums) {
// 初始数据
int n = nums.size();
// 创建dp数组
vector<int> dp(n);
dp[0] = nums[0];
// int maxRes = nums[0];
for(int i = 1; i < n; ++i) {
dp[i] = max(nums[i], dp[i - 1] + nums[i]);
// maxRes = max(dp[i], maxRes);
}
// return maxRes;
return *max_element(dp.begin(), dp.end());
}
};
✨152. 乘积最大子数组✨
class Solution {
public:
int maxProduct(vector<int>& nums) {
// 初始数据
int n = nums.size();
/*
1、非空连续子数组
2、乘积最大
tips:负负得正
*/
// 创建dp数组
vector<int> maxF(n);
vector<int> minF(n);
maxF[0] = nums[0];
minF[0] = nums[0];
// dp逻辑
for(int i = 1; i < n; ++i) {
maxF[i] = max(maxF[i - 1] * nums[i], max(nums[i], minF[i - 1] * nums[i]));
minF[i] = min(maxF[i - 1] * nums[i], min(nums[i], minF[i - 1] * nums[i]));
}
return *max_element(maxF.begin(), maxF.end());
}
};
✨ 918. 环形子数组的最大和✨
class Solution {
public:
int maxSubarraySumCircular(vector<int>& nums) {
/*
根据题解可知: 环形子数组的最大和具有两种可能,一种是不使用环的情况,
另一种是使用环的情况
1、不使用环的情况时,直接通过53题的思路,逐步求出整个数组中的最大子序和即可
2、使用到了环,则必定包含 A[n-1]和 A[0]两个元素且说明从A[1]到A[n-2],
这个子数组中必定包含负数。【否则只通过一趟最大子序和就可以的出结果】
*/
// 初始数据
int n = nums.size();
if(n == 1) return nums[0];
// 创建dp数组
vector<int> dpMax(n);
vector<int> dpMin(n);
// dp数组初始化
dpMax[0] = nums[0];
dpMin[0] = nums[0];
// dp逻辑
for(int i = 1; i < n; ++i) {
dpMax[i] = max(dpMax[i - 1] + nums[i], nums[i]);
dpMin[i] = min(dpMin[i - 1] + nums[i], nums[i]);
}
int max1 = *max_element(dpMax.begin(), dpMax.end());
int min1 = *min_element(dpMin.begin(), dpMin.end());
int temp = accumulate(nums.begin(), nums.end(), 0);
int max2 = temp - min1;
if(max2 == 0) { //考虑数据全负的情况,返回数组中的最大值
return max1;
}
return max(max1, max2);
}
};
✨ 面试题 17.24. 最大子矩阵✨
/*
最大子数组的二维升级版。技术点:前缀和+最大子数组
下面是思路:
i,j双指针遍历所有可能的的两个“行对”,即子矩阵的上下两条边,这决定了矩阵的高
将i-j之间的每一列(求出每一列的累计的和)看成一维数组中的一项,在其中求最大子数组,即求出符合的子矩阵的宽
*/
class Solution {
public:
vector<int> getMaxMatrix(vector<vector<int>>& matrix) {
// 已知
int rows = matrix.size();
int cols = matrix[0].size();
// 结果
vector<int> ans(4, -1);
int maxMat = INT_MIN; // 最大值,初始化
// 遍历
for(int up = 0; up < rows; ++up) { // 遍历起始行
vector<int> nums(cols); // 矩阵某两行元素求和
for(int bottom = up; bottom < rows; ++bottom) { // 遍历结束行
// 最大字段和问题
int dp = 0; // 记录最大和
int left = 0; // 起始列
for(int right = left; right < cols; ++right) { // 遍历列 // 遍历和数组,实际上是边遍历边完成求和
nums[right] += matrix[bottom][right]; //将新的一行中第i个元素加到前面若干行在位置i的和
if(dp > 0) { //前面的字段有和为正,可以把前面一部分也带上
dp += nums[right];
} else { //前面一段为负,拖后腿直接抛弃
dp = nums[right];
left = right;
}
if(dp > maxMat) { // 与历史最大值比较,若大于,更新。不断记录较好的结果
maxMat = dp;
ans[0] = up;
ans[1] = left;
ans[2] = bottom;
ans[3] = right;
}
}
}
}
return ans;
}
};
✨ 363. 矩形区域不超过 K 的最大数值和✨
class Solution {
public:
int maxSumSubmatrix(vector<vector<int>> &matrix, int k) {
int ans = INT_MIN;
int m = matrix.size(), n = matrix[0].size();
for (int i = 0; i < m; ++i) { // 枚举上边界
vector<int> sum(n);
for (int j = i; j < m; ++j) { // 枚举下边界
for (int c = 0; c < n; ++c) {
sum[c] += matrix[j][c]; // 更新每列的元素和
}
set<int> sumSet{0};
int s = 0;
for (int v : sum) {
s += v;
auto lb = sumSet.lower_bound(s - k);
if (lb != sumSet.end()) {
ans = max(ans, s - *lb);
}
sumSet.insert(s);
}
}
}
return ans;
}
};
✨单串:打家劫舍系列✨
✨ 198. 打家劫舍✨
class Solution {
public:
int rob(vector<int>& nums) {
// 已知信息
int n = nums.size();
if(n == 1) return nums[0];
// 创建dp数组
vector<int> dp(n);
dp[0] = nums[0];
dp[1] = max(nums[0], nums[1]);
for(int i = 2; i < n; ++i) {
// 偷第i间房和不偷第i间房,取最大
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
}
return dp[n - 1];
}
};
✨ 213. 打家劫舍 II✨
class Solution {
public:
int rob(vector<int>& nums) {
int n = nums.size();
if(n == 1) return nums[0];
if(n == 2) return max(nums[0], nums[1]);
// 偷头不偷尾,偷尾不偷头
vector<int> nums1(nums.begin(), nums.begin() + n - 1);
vector<int> nums2(nums.begin() + 1, nums.end());
return max(myRob(nums1), myRob(nums2));
}
int myRob(vector<int>& nums) {
int n = nums.size();
// vector数组
// vector<int> dp(n);
// dp[0] = nums[0];
// dp[1] = max(nums[0], nums[1]);
// for(int i = 2; i < n; ++i) {
// dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
// }
// return dp[n - 1];
// 滚动数组
int pre = nums[0], cur = max(nums[0], nums[1]);
for(int i = 2; i < n; ++i) {
int temp = cur;
cur = max(pre + nums[i], cur);
pre = temp;
}
return cur;
}
};
class Solution {
private:
int rob(vector<int> &nums) {
int size = nums.size();
vector<int> dp(size, 0);
dp[0] = nums[0];
dp[1] = max(nums[0], nums[1]);
for (int i = 2; i < size; i++) {
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
}
return dp[size - 1];
}
public:
int deleteAndEarn(vector<int>& nums) {
int maxVal = 0;
for(auto val : nums){
maxVal = max(maxVal,val);
}
//构造(点数和)数组
vector<int> sum(maxVal + 1);
//填充数组
for(auto val : nums){
sum[val] += val;
}
//动态规划
return rob(sum);
}
};
✨740. 删除并获得点数
✨
class Solution {
public:
int deleteAndEarn(vector<int>& nums) {
// 重点:选中nums[i],就不选nums[i - 1]和nums[n + 1]
int maxVal = 0;
for(auto& val : nums) {
maxVal = max(maxVal, val);
}
vector<int> numSum(maxVal + 1, 0);
for(auto& val : nums) {
numSum[val] += val;
}
return myRob(numSum);
}
int myRob(vector<int>& nums) {
int n = nums.size();
if(n == 1) return nums[0];
vector<int> dp(n, 0);
dp[0] = nums[0];
dp[1] = max(nums[0], nums[1]);
for(int i = 2; i < n; ++i) {
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
}
return dp[n - 1];
}
};
✨ 1388. 3n 块披萨✨
class Solution {
public:
int maxSizeSlices(vector<int>& slices) {
vector<int> nums1(slices.begin(), slices.begin() + slices.size() - 1);
vector<int> nums2(slices.begin() + 1, slices.end());
return max(myRob(nums1), myRob(nums2));
}
int myRob(vector<int>& nums) {
int n = nums.size();
int choose = (n + 1) / 3;
vector<vector<int>> dp(n + 1, vector<int>(choose + 1));
for(int i = 1; i <= n; ++i) {
for(int j = 1; j <= choose; ++j) {
dp[i][j] = max(dp[i - 1][j], (i - 2 >= 0 ? dp[i - 2][j - 1] : 0) + nums[i - 1]);
}
}
return dp[n][choose];
}
};
✨单串:变形,需要两个位置的情况✨
✨ 873. 最长的斐波那契子序列的长度✨
class Solution {
public:
int lenLongestFibSubseq(vector<int>& arr) {
int n = arr.size();
// f(i,j),以arr[i]为结尾,前一个是arr[j]的数列长度
vector<vector<int>> dp(n, vector<int>(n));
// 需要哈希表快速遍历有无arr[k],实现arr[k] + arr[j] = arr[i]
unordered_map<int, int> umap;
for(int i = 0; i < n; ++i) {
umap[arr[i]] = i;
}
// dp逻辑
int ans = 0;
for(int i = 1; i < n; ++i) {
for(int j = 0; j < i; ++j) {
// arr[k]的值
int temp = arr[i] - arr[j];
// 若存在
if(umap.count(temp) && umap[temp] < j) {
dp[i][j] = dp[j][umap[temp]] + 1;
} else { // 若不存在
dp[i][j] = 2;
}
ans = max(ans, dp[i][j]);
}
}
return ans > 2 ? ans : 0;
}
};
class Solution {
private:
int search(vector<int>& nums, int right, int target) {
int left = 0;
while (left <= right) {
int mid = left + ((right - left) >> 1);
if (nums[mid] == target) {
return mid;
}
nums[mid] > target ? right = mid - 1 : left = mid + 1;
}
return -1;
}
public:
int lenLongestFibSubseq(vector<int>& arr) {
vector<vector<int>> dp(arr.size(), vector<int>(arr.size()));
int ret = 0;
for (int i = 1; i < arr.size(); ++i) {
for (int j = 0; j < i; ++j) {
int index = search(arr, j - 1, arr[i] - arr[j]);
dp[i][j] = (index != -1) ? dp[j][index] + 1 : 2;
ret = max(ret, dp[i][j]);
}
}
return ret > 2 ? ret : 0;
}
};
class Solution {
public:
int lenLongestFibSubseq(vector<int>& arr) {
int n = arr.size();
// f(i,j),以arr[i]为结尾,前一个是arr[j]的数列长度
vector<vector<int>> dp(n, vector<int>(n));
// dp逻辑
int ans = 0;
for(int i = 1; i < n; ++i) {
for(int j = 0; j < i; ++j) {
// arr[k]的值
int temp = arr[i] - arr[j];
// 递增数组用二分
int left = 0;
int right = j - 1;
dp[i][j] = 2; // 若不存在
while(left <= right) {
int mid = left + (right - left) / 2;
if(arr[mid] == temp) {
dp[i][j] = dp[j][mid] + 1; // 若存在
break;
} else if(arr[mid] > temp) {
right = mid - 1;
} else {
left = mid + 1;
}
}
ans = max(ans, dp[i][j]);
}
}
return ans > 2 ? ans : 0;
}
};
✨ 1027. 最长等差数列✨
class Solution {
public:
int longestArithSeqLength(vector<int>& nums) {
int n = nums.size();
vector<vector<int>> dp(n, vector<int>(n, 2));
unordered_map<int, int> umap;
int ans = 0;
for(int i = 0; i < n; ++i) {
for(int j = i + 1; j < n; ++j) {
int target = nums[i] + (nums[i] - nums[j]);
if(umap.count(target)) {
dp[i][j] = dp[umap[target]][i] + 1;
}
// else {
// dp[i][j] =2;
// }
ans = max(ans, dp[i][j]);
}
umap[nums[i]] = i;
}
return ans;
}
};
✨单串:与其他算法配合✨
✨ 368. 最大整除子集✨
class Solution {
public:
vector<int> largestDivisibleSubset(vector<int>& nums) {
int n = nums.size();
// 排序
sort(nums.begin(), nums.end());
// dp数组,以nums[i]结尾,子集最大值
// 第 1 步:动态规划找出最大子集的个数、最大子集中的最大整数
vector<int> dp(n, 1);
int maxSize = 1;
int maxVal = nums[0];
// dp逻辑
for(int i = 1; i < n; ++i) {
for(int j = 0; j < i; ++j) {
if(nums[i] % nums[j] == 0) { // 题目中说「没有重复元素」很重要
dp[i] = max(dp[i], dp[j] + 1);
}
if(maxSize < dp[i]) {
maxSize = dp[i];
maxVal = nums[i];
}
}
}
// 倒退获得最大子集
vector<int> ans;
if (maxSize == 1) {
// ans.push_back(nums[0]);
// return ans;
return {nums[0]};
}
for(int i = n - 1; i >= 0 && maxSize > 0; --i) {
if(dp[i] == maxSize && maxVal % nums[i] == 0) {
ans.push_back(nums[i]);
maxVal = nums[i];
maxSize--;
}
}
return ans;
}
};
写在后面
加油
乐观是一首激昂优美的进行曲,时刻鼓舞着你向事业的大路勇猛前进!