每日算法刷题Day85:11.12:leetcode 动态规划6道题,用时1h40min
三、最大子数组和(最大子段和)
1.套路
1.有两种做法:
- 定义状态
f[i]表示以a[i]结尾的最大子数组和,不和 i 左边拼起来就是f[i]=a[i],和 i 左边拼起来就是f[i]=f[i−1]+a[i],取最大值就得到了状态转移方程f[i]=max(f[i−1],0)+a[i],答案为max(f)(不是f[n-1],因为f[n-1]表明一定选n-1)。这个做法也叫做 Kadane 算法。 - 用前缀和,转化成 121. 买卖股票的最佳时机。
问:为什么不能用「选或不选 nums[i]」的思路做?
答:选或不选无法保证子数组是连续的。选或不选适用于子序列问题(例如 0-1 背包问题),对于子数组问题,更适合用「拼接」的思路,即如果 nums[i] 左边的子数组元素和是负的,就不用和左边的子数组拼在一起了。
2.题目描述
3.学习经验
1. 53. 最大子数组和(中等)
53. 最大子数组和 - 力扣(LeetCode)
思想
1.给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组是数组中的一个连续部分。
2.f[i]表示以nums[i]结尾的子数组的最大和,答案为max(f)
代码
class Solution {
public:int n;vector<int> f; // f[i]表示以a[i]结尾的最大子数组和int maxSubArray(vector<int>& nums) {n = nums.size();// [0,n)f.resize(n);f[0] = nums[0];int res = f[0];for (int i = 1; i < n; ++i) {// 不与i-1拼接:nums[i],与i-1拼接:f[i-1]+nums[i]f[i] = max(0, f[i - 1]) + nums[i];res = max(res, f[i]);}return res;}
};
空间优化:
class Solution {
public:int n;int maxSubArray(vector<int>& nums) {n = nums.size();// [0,n)int f = nums[0], res = nums[0];for (int i = 1; i < n; ++i) {// 不与i-1拼接:nums[i],与i-1拼接:f[i-1]+nums[i]f = max(0, f) + nums[i];res = max(res, f);}return res;}
};
2. 2606. 找到最大开销的子字符串(中等)
2606. 找到最大开销的子字符串 - 力扣(LeetCode)
思想
1.给你一个字符串 s ,一个字符 互不相同 的字符串 chars 和一个长度与 chars 相同的整数数组 vals 。
子字符串的开销 是一个子字符串中所有字符对应价值之和。空字符串的开销是 0 。
字符的价值 定义如下:
- 如果字符不在字符串
chars中,那么它的价值是它在字母表中的位置(下标从 1 开始)。- 比方说,
'a'的价值为1,'b'的价值为2,以此类推,'z'的价值为26。
- 比方说,
- 否则,如果这个字符在
chars中的位置为i,那么它的价值就是vals[i]。
请你返回字符串s的所有子字符串中的最大开销。
代码
class Solution {
public:int n;vector<int> f; // f[i]表示以s[i]结尾的子字符串最大开销vector<int> charToId;int maximumCostSubstring(string s, string chars, vector<int>& vals) {n = s.size();// [0,n)f.resize(n);charToId.resize(26, -1);for (int i = 0; i < chars.size(); ++i) {int id = chars[i] - 'a';charToId[id] = i;}int idChar = s[0] - 'a';int id = charToId[idChar];if (id != -1)f[0] = vals[id];elsef[0] = idChar + 1;int res = max(0, f[0]); // 空串for (int i = 1; i < n; ++i) {idChar = s[i] - 'a';id = charToId[idChar];int val;if (id != -1)val = vals[id];elseval = idChar + 1;f[i] = max(0, f[i - 1]) + val;res = max(res, f[i]);}return res;}
};
空间优化:
class Solution {
public:int n;vector<int> charToId;int maximumCostSubstring(string s, string chars, vector<int>& vals) {n = s.size();charToId.resize(26, -1);for (int i = 0; i < chars.size(); ++i) {int id = chars[i] - 'a';charToId[id] = i;}int idChar = s[0] - 'a';int id = charToId[idChar];int f;if (id != -1)f = vals[id];elsef = idChar + 1;int res = max(0, f); // 空串for (int i = 1; i < n; ++i) {idChar = s[i] - 'a';id = charToId[idChar];int val;if (id != -1)val = vals[id];elseval = idChar + 1;f = max(0, f) + val;res = max(res, f);}return res;}
};
直接用数组记录价值,而不是先记录下标再在遍历时求价值:
class Solution {
public:int n;vector<int> costs;int maximumCostSubstring(string s, string chars, vector<int>& vals) {n = s.size();costs.resize(26);// 先初始化for (int i = 0; i < 26; ++i) {costs[i] = i + 1;}// 再覆盖更新for (int i = 0; i < chars.size(); ++i) {int id = chars[i] - 'a';costs[id] = vals[i];}int id = s[0] - 'a';int f = costs[id];int res = max(0, f); // 空串for (int i = 1; i < n; ++i) {id = s[i] - 'a';f = max(0, f) + costs[id];res = max(res, f);}return res;}
};
3. 1749. 任意子数组和的绝对值的最大值(中等)
1749. 任意子数组和的绝对值的最大值 - 力扣(LeetCode)
思想
1.给你一个整数数组 nums 。一个子数组 [numsl, numsl+1, ..., numsr-1, numsr] 的 和的绝对值 为 abs(numsl + numsl+1 + ... + numsr-1 + numsr) 。
请你找出 nums 中 和的绝对值 最大的任意子数组(可能为空),并返回该 最大值 。
abs(x) 定义如下:
- 如果
x是负整数,那么abs(x) = -x。 - 如果
x是非负整数,那么abs(x) = x。
2.题目求子数组和绝对值最大,所以求子数组最大和和最小和,两个动态规划数组,可以空间优化
代码
class Solution {
public:int n;vector<int> fMax, fMin; // fMax[i]表示以nums[i]结尾的子数组的和的最大值,fMin[i]同理int maxAbsoluteSum(vector<int>& nums) {n = nums.size();fMax.resize(n), fMin.resize(n);fMax[0] = nums[0], fMin[0] = nums[0];int res = abs(nums[0]);for (int i = 1; i < n; ++i) {fMax[i] = max(fMax[i - 1], 0) + nums[i];fMin[i] = min(fMin[i - 1], 0) + nums[i];res = max({res, abs(fMax[i]), abs(fMin[i])});}return res;}
};
空间优化:
class Solution {
public:int n;int maxAbsoluteSum(vector<int>& nums) {n = nums.size();int fMax = nums[0], fMin = nums[0];int res = abs(nums[0]);for (int i = 1; i < n; ++i) {fMax = max(fMax, 0) + nums[i];fMin = min(fMin, 0) + nums[i];res = max({res, abs(fMax), abs(fMin)});}return res;}
};
4. 1191. K次串联后最大子数组之和(中等,学习)
1191. K 次串联后最大子数组之和 - 力扣(LeetCode)
思想
1.给定一个整数数组 arr 和一个整数 k ,通过重复 k 次来修改数组。
例如,如果 arr = [1, 2] , k = 3 ,那么修改后的数组将是 [1, 2, 1, 2, 1, 2] 。
返回修改后的数组中的最大的子数组之和。注意,子数组长度可以是 0,在这种情况下它的总和也是 0。
由于 结果可能会很大,需要返回的 109 + 7 的 模 。
2.[[十八.动态规划-一.入门DP#1. 53. 最大子数组和(中等)]]的变种,但不能显示复制k次算,而是要分类讨论k=1,k=2,k>=3来找规律
- k=1,无区别
- k=2,令arr数组和为sum,有以下结论
- (1)sum>0,则最大子数字和必然横跨两个arr,证明:
- (1.1)如图所示 ![[1191. K次串联后最大子数组之和.png]]
- (1.2)反证法,若最大子数字和在第一个arr的
[l,r]区间,则加上第一个arr的[r+1,n)和第二个arr的[0,r)构成一个sum,且sum>0,则必然大于[l,r]区间,得证。
- (2)sum<0,还是用两个arr求,是否横跨不一定
- (1)sum>0,则最大子数字和必然横跨两个arr,证明:
- k>=3,共
[0,k)个arr- (1)sum>0,第
[1,k-1)个arr必包括,因为都是整个sum>0,都是正影响,所以就是arr+k-2个arr+arr,其中中间k-2个arr全包含,就是sum - (2)sum<0.最大子数组不可能包含一个完整的arr,反证法:如果包含,去掉这个负影响,和更大,所以变成2个arr
- (1)sum>0,第
代码
超时写法,显式复制k次
class Solution {
public:int n;typedef long long ll;const ll mod = 1e9 + 7;int kConcatenationMaxSum(vector<int>& arr, int k) {n = arr.size();// [0,k*n)ll f = arr[0]; // f[i]表示以arr[i]结尾的最大子数组和ll res = max(f, 1LL * 0);for (ll i = 1; i < 1LL * n * k; ++i) {f = (max(f, 1LL * 0) + arr[i % n]);res = max(res, f);}return res % mod;}
};
优化:
class Solution {
public:int n;typedef long long ll;const ll mod = 1e9 + 7;ll maxSubArray(vector<int>& arr, int k) {n = arr.size();// [0,k*n)ll f = arr[0]; // f[i]表示以arr[i]结尾的最大子数组和ll res = max(f, 1LL * 0);for (int i = 1; i < n * k; ++i) {f = (max(f, 1LL * 0) + arr[i % n]);res = max(res, f);}return res;}int kConcatenationMaxSum(vector<int>& arr, int k) {n = arr.size();if (k == 1)return maxSubArray(arr, 1) % mod;ll res = maxSubArray(arr, 2);ll sum = 0;for (int& x : arr)sum += x;if (sum > 0)res = (res + sum * (k - 2)) % mod;return res % mod;}
};
5. 918. 环形子数组的最大和(中等,学习)
918. 环形子数组的最大和 - 力扣(LeetCode)
思想
1.给定一个长度为 n 的环形整数数组 nums ,返回 nums 的非空 子数组 的最大可能和 。
环形数组 意味着数组的末端将会与开头相连呈环状。形式上, nums[i] 的下一个元素是 nums[(i + 1) % n] , nums[i] 的前一个元素是 nums[(i - 1 + n) % n] 。
子数组 最多只能包含固定缓冲区 nums 中的每个元素一次。形式上,对于子数组 nums[i], nums[i + 1], ..., nums[j] ,不存在 i <= k1, k2 <= j 其中 k1 % n == k2 % n 。
2.此题延长一倍求不好求,要用逆向思维(不连续转化成连续),如下所示
![[918. 环形子数组的最大和.png]]
代码
class Solution {
public:int maxSubarraySumCircular(vector<int>& nums) {int n = nums.size();int sum = nums[0], fMax = nums[0], fMin = nums[0], maxn = nums[0],minn = nums[0]; // fMax,fMin:dp数组,maxn,minn:最大/小子数组和for (int i = 1; i < n; ++i) {sum += nums[i];fMax = max(fMax, 0) + nums[i];fMin = min(fMin, 0) + nums[i];maxn = max(maxn, fMax);minn = min(minn, fMin);}if (maxn < 0)return maxn;return max(maxn, sum - minn);}
};
6. 232. 拼接数组的最大分数(困难)
2321. 拼接数组的最大分数 - 力扣(LeetCode)
思想
1.给你两个下标从 0 开始的整数数组 nums1 和 nums2 ,长度都是 n 。
你可以选择两个整数 left 和 right ,其中 0 <= left <= right < n ,接着 交换 两个子数组 nums1[left...right] 和 nums2[left...right] 。
- 例如,设
nums1 = [1,2,3,4,5]和nums2 = [11,12,13,14,15],整数选择left = 1和right = 2,那么nums1会变为[1,**_12_,_13_**,4,5]而nums2会变为[11,_**2,3**_,14,15]。
你可以选择执行上述操作 一次 或不执行任何操作。
数组的 分数 取sum(nums1)和sum(nums2)中的最大值,其中sum(arr)是数组arr中所有元素之和。
返回 可能的最大分数 。
子数组 是数组中连续的一个元素序列。arr[left...right]表示子数组包含nums中下标left和right之间的元素(含 下标left和right对应元素)。
2.求交换后两个数组和的最大值,即交换给数组带来了最优正向增益,所以求1个子数组,让两个数组的差的和最大,即为1个数组带来的最优正向增益。
但两个数组都要算,不能简单地认为原始和最大的,一定交换完合最大。
代码
错误代码:
错误原因:只算了sum1+增益的最大值,尽管保证原始sum1>sum2,但无法保证sum1+最优增益1就一定大于sum2+最优增益2,所以两个要都算一遍,取最大值
class Solution {
public:int n;typedef long long ll;int maximumsSplicedArray(vector<int>& nums1, vector<int>& nums2) {n = nums1.size();ll sum1 = 0, sum2 = 0;for (int i = 0; i < n; ++i) {sum1 += nums1[i];sum2 += nums2[i];}// 令sum1>=sum2if (sum1 < sum2) {swap(nums1, nums2);swap(sum1, sum2);}ll f = 0, maxn = 0; // f[i]表示以nums2[i]-nums1[i]结尾的子数组的最大和for (int i = 0; i < n; ++i) {f = max(f, 1LL * 0) + nums2[i] - nums1[i];maxn = max(maxn, f);}return sum1 + maxn;}
};
改进代码:
class Solution {
public:int n;typedef long long ll;int maximumsSplicedArray(vector<int>& nums1, vector<int>& nums2) {n = nums1.size();ll sum1 = 0, sum2 = 0;for (int i = 0; i < n; ++i) {sum1 += nums1[i];sum2 += nums2[i];}ll f1 = 0, maxn1 = 0, f2 = 0,maxn2 =0; // f1[i]表示以nums2[i]-nums1[i]结尾的子数组的最大和,f2[i]为nums1[i]-nums2[i]for (int i = 0; i < n; ++i) {f1 = max(f1, 1LL * 0) + nums2[i] - nums1[i];maxn1 = max(maxn1, f1);f2 = max(f2, 1LL * 0) + nums1[i] - nums2[i];maxn2 = max(maxn2, f2);}return max(sum1 + maxn1, sum2 + maxn2);}
};
