问题描述
小包非常喜欢吃甜点,他收到了一次性送来的 N 个甜点,每个甜点都有一个对应的喜爱值。
但是这还不够!小包让小哥连续送来了 M 次相同的 N 个甜点,并将这些甜点首尾相接排成一排。
现在,小包面前有 (N×M) 个排成一排的甜点,小包希望从中选择一段连续的甜点,使得这段甜点的总喜爱值最大化。
注意:尽管小包喜欢甜食,但有些甜点可能不合口味,导致其喜爱值为负数。小包至少要选择一个甜点来满足他对甜点的贪心。
问题思路解析
首先,我们可以将问题看成是一个求解连续子数组和最大化的问题。为了找到最优的解决方案,我们可以采用“前缀和”技术来高效计算子数组的和,并结合一些优化方法提高求解速度。
1. 问题转化
给定N个甜点的喜爱值,并且这些甜点被连续复制了M次。我们需要从这些(N * M)个甜点中选出一段连续子数组,使得这段子数组的总喜爱值最大。
我们可以将这个问题简化为求一个长度为N * M的数组中,任意连续子数组的最大和。通过前缀和数组的引入,原问题可以转化为求解arr[j] - arr[i]的最大值,其中i < j。
2. 前缀和的引入
- 前缀和:我们定义一个前缀和数组
arr,其中arr[i]表示从第1个甜点到第i个甜点的累积和。这样,任意一个子数组的和就可以通过arr[j] - arr[i]来表示,表示的是从第i+1个甜点到第j个甜点的和。 - 具体的计算方法是:
arr[i+1] = data[i % N] + arr[i],这里data[i % N]表示对于第i个甜点,如果i大于N,则通过取余i % N来重复使用第i个甜点的喜爱值。
3. 最小前缀和的优化
为了高效计算最大子数组和,我们引入一个辅助数组brr,用于存储当前前缀和数组中最小的前缀和。我们可以通过比较每个新的前缀和arr[i]与之前的最小前缀和brr[i-1]来更新最大子数组和。
- 对于每一个
i,我们计算arr[i] - brr[i-1],这个值表示从brr[i-1]到arr[i]这段区间的和。如果这个值大于之前计算的最大值,我们就更新最大值。 brr[i] = min(brr[i-1], arr[i]),通过这个更新方式,我们始终能够追踪到当前arr数组中最小的前缀和,进而帮助我们找到当前i为结尾时最大子数组和。
4. 最终的计算
在每一次迭代中,我们都会更新最大子数组和,并且不断追踪最小前缀和。最终,我们返回计算得到的最大值。
5. 时间复杂度
- 计算前缀和的时间复杂度是O(N * M),因为我们需要遍历所有甜点。
- 更新最小前缀和的过程也需要O(N * M)的时间。
完整代码
#include <vector>
#include <limits.h>
using namespace std;
int solution(int N, int M, std::vector<int> data) {
// Edit your code here
vector<long long> arr(N*M+1);
vector<long long> brr(N*M+1);
for(int i=0;i<N*M;i++){
arr[i+1]=data[i%N]+arr[i];
}
for(int i=1;i<=N*M;i++)
{
brr[i]=min(brr[i-1],arr[i]);
}
long long res=INT_MIN;
for(int i=1;i<=N*M;i++){
long long a=arr[i]-brr[i-1];
res=max(res,a);
}
return res;
}
int main() {
// Add your test cases here
std::cout << (solution(5, 1, {1, 3, -9, 2, 4}) == 6) << std::endl;
std::cout << (solution(5, 3, {1, 3, -9, 2, 4}) == 11) << std::endl;
return 0;
}