贪心的小包 | 豆包MarsCode AI刷题

152 阅读6分钟

1.问题描述

小包是一名很喜欢吃甜点的小朋友,他在工作时特别爱吃下午茶里的甜食。
这天,送下午茶的小哥送来了N个甜点,放成一排给小包挑选,但身为强迫症患者的小包一定要选择连续的一段。并且每个甜点有一个喜爱值,他希望他最后选择的甜点的总喜爱值最大。
等等,就这好像还不足以显示小包的贪心
小包让小哥给自己送了M次甜点,每次同样的N个甜点。最后他把这N*M个甜点排成一排,从中选择连续的一段甜点食用。他想知道,这种情况下他最多能获得多少喜爱值呢。

注意,虽然小包爱吃甜食,但是总有不合口味的,所以会有喜爱值为负数的甜点。另外,小包必须至少选择1个甜点进行食用,为下午的工作补足能量

输入格式

输入第一行,包含2个整数N,M。代表甜点的数量,以及送甜点的次数。
接下来一行,包含N个整数,第i个整数代表第i个甜点的喜爱值S[i]。

输出格式

输出仅包含一个整数,代表小包能获得的最大喜爱值。

输入样例1
5 1  
1 3 -9 2 4
输出样例1

6

输入样例2
5 3  
1 3 -9 2 4
输出样例2

11

注解

在样例1中,选择[4, 5]区间,可获得6点喜爱度。
在样例2中,小包拿到的甜点序列为[1, 3, -9, 2, 4, 1, 3, -9, 2, 4, 1, 3, -9, 2, 4],选择区间[4, 12] 可获得11点喜爱值。

数据范围

10%的数据保证,1 <= NM <= 1000
20%的数据保证,1 <= N
M <= 10^5
40%的数据保证,1 <= N <= 1000,1 <= M <= 10^6
100%的数据保证,1 <= N, M <= 10^5, -10^6 <= S[i] <= 10^6

这一问题可以归结为最大子数组和问题的循环扩展版本,也是经典的动态规划贪心结合的应用。


2. 基础知识点

2.1 最大子数组和问题

最大子数组和问题是一个求解连续数组子段的和最大值的经典问题。例如,给定数组 [1,−2,3,5,−3,2][1, -2, 3, 5, -3, 2][1,−2,3,5,−3,2],其最大子数组为 [3,5][3, 5][3,5],和为 8。

其解法主要有以下几种:

  1. 暴力法: 遍历所有可能的子数组,时间复杂度为 O(N^2)。
  2. 动态规划(Kadane's算法) : 利用局部最优解递推全局最优解,将时间复杂度降至 O(N)。这是最优解法之一。
  3. 分治法: 利用分治策略将问题划分为左右子问题,再合并结果,时间复杂度为 O(Nlog⁡N)。
2.2 扩展问题

本题要求在重复 M 次的甜点序列中找到最大子数组和,这就引入了以下复杂情况:

  • 跨越边界的子数组:例如,最大和可能出现在第 M−1 次与第 M 次的序列之间。
  • 总和影响:如果单次序列的总和为正,则多次重复会显著增加整体和。

3. 问题分析与解决思路

3.1 分解问题

我们可以将问题拆解为以下几部分:

  1. 单次序列的最大子数组和:通过 Kadane's 算法快速求解。

  2. 前缀和与后缀和

    • 前缀和是从序列开头到某一位置的最大和。
    • 后缀和是从序列末尾到某一位置的最大和。
  3. 完全重复的序列贡献:如果序列的总和为正,则中间的完整序列会贡献额外的最大值。

3.2 关键公式

最终,我们需要比较以下三种情况的最大值:

  1. 单次序列的最大子数组和。
  2. 跨越多次序列的最大值:前缀和 + 后缀和 + 中间序列的总和。
  3. 若 M=1,直接返回 Kadane 结果。
3.3 时间复杂度分析

单次 Kadane 算法的复杂度为 O(N)。本题仅需对单次序列进行前缀和、后缀和及 Kadane 的计算,因此整体时间复杂度为 O(N),空间复杂度为 O(1)。


4. 代码实现与详解

代码实现如下:

public static int solution(int N, int M, int[] data) {
    int maxSubarraySum = kadane(data); // 单次最大子数组和
    long totalSum = 0;                // 单次序列的总和
    int prefixMax = Integer.MIN_VALUE;
    int suffixMax = Integer.MIN_VALUE;

    int prefixSum = 0;
    int suffixSum = 0;

    // 计算前缀和、后缀和和总和
    for (int i = 0; i < N; i++) {
        totalSum += data[i];
        prefixSum += data[i];
        prefixMax = Math.max(prefixMax, prefixSum);
    }

    for (int i = N - 1; i >= 0; i--) {
        suffixSum += data[i];
        suffixMax = Math.max(suffixMax, suffixSum);
    }

    // 如果 M = 1,直接返回 Kadane 的结果
    if (M == 1) {
        return maxSubarraySum;
    } else {
        // 计算跨越多次序列的最大值
        long maxWithMultipleArrays = Math.max(
            maxSubarraySum,
            prefixMax + suffixMax + Math.max(0, (M - 2) * totalSum)
        );
        return (int) maxWithMultipleArrays;
    }
}

// Kadane's算法
private static int kadane(int[] arr) {
    int maxEndingHere = 0;
    int maxSoFar = Integer.MIN_VALUE;

    for (int value : arr) {
        maxEndingHere = Math.max(value, maxEndingHere + value);
        maxSoFar = Math.max(maxSoFar, maxEndingHere);
    }

    return maxSoFar;
}

5. 思考与分析

5.1 动态规划与贪心结合的威力

通过 Kadane 算法,我们可以看到动态规划与贪心策略的结合能够高效地解决最大子数组和问题。动态规划记录状态转移,而贪心则保证局部最优选择。

5.2 数据规模与内存优化

本题的关键在于避免构造 N×M 的完整数组。通过数学技巧,仅用 O(N) 的时间复杂度完成了最大子数组和的求解。这种优化在大规模数据处理时尤为重要。

5.3 实际应用场景

在实际中,类似的场景广泛存在。例如:

  • 股票交易中求连续最大收益。
  • 动态规划优化路径规划问题。
  • 数据流处理中的窗口最大值计算。

6. 总结与启示

本题作为经典算法问题的扩展,既要求我们掌握基础知识,又需要灵活应用优化技巧。以下几点是学习中的关键:

  1. 掌握动态规划和贪心算法的核心思想。
  2. 学会对问题进行分解,并找到关键性质。
  3. 面对大规模数据时,善于通过数学方法减少计算量。

通过本次学习,不仅巩固了基础知识,还对高效算法的设计与优化有了更深刻的理解。这种学习过程有助于提升实际问题的解决能力,也为进一步研究算法优化奠定了基础。