问题描述
小包收到 𝑁 个甜点,每个甜点都有一个对应的喜爱值。
小包让小哥连续送来了 𝑀 次相同的 𝑁个甜点,并将这些甜点首尾相接排成一排。
现在,有 (𝑁×𝑀) 个排成一排的甜点,从中选择一段连续的甜点,使得这段甜点的总喜爱值最大化。
注意:小包至少要选择一个甜点。
-
输入
-
N:每次收到的甜点数量 => 数组data的长度 -
M:送来的次数 => 有多少个数组首尾相连 -
data:表示每个甜点的喜爱值的数组
-
-
输出
𝑁×𝑀个甜点中可以选择的连续甜点段的最大总喜爱值
问题分析
阅读题目,可以把问题转化为:
M 个数组 data 首位相连,求这个大数组的连续子序列元素之和最大值。
算法分析
很明显,这是 Kadane 算法 的典型问题。
Kadane算法 是一种用于解决最大子数组和问题的动态规划算法。这类问题的目标是在给定整数数组(数组含有负数)中找到一个 连续 的子数组,使其元素之和最大。
-
算法核心思想
-
维护两个变量来跟踪局部最优解和全局最优解。
-
max_ending_here:表示包含当前元素的子数组的最大和(当前子数组和)。 -
max_so_far:记录迄今为止遇到的最大子数组和。
-
-
逐步迭代数组的元素,更新当前子数组的最大和
max_ending_here。 -
将其与之前的最大值
max_so_far进行比较,更新max_ending_here为两者中的较大值。
局部最优:在遍历到每一个元素时,我们只关心当前元素加上前面子数组的最大和是否比当前元素本身更大。
这决定了 现在的子序列 是 原来的子序列加上当前元素 还是变成 只有当前元素
由于数组内部有负数存在,有时从当前元素开始一个新的子数组会更好。
-
-
复杂度
时间复杂度是 O(n)
只需要遍历一次数组(即一次线性扫描)
空间复杂度是 O(1)
只需要常数空间来存储两个变量(
max_ending_here和max_so_far) -
算法代码实现
def kadane(arr): # 初始化为第一个元素 max_ending_here = max_so_far = arr[0] # 从第二个元素开始遍历 for x in arr[1:]: # 更新当前子数组和 max_ending_here = max(x, max_ending_here + x) # 更新全局最大子数组和 max_so_far = max(max_so_far, max_ending_here) return max_so_far
跨越边界分析
对于 M == 1 的情况,我们只需要返回 kadane(data) 即可。
但在 M > 1 时 ,我们需要考虑由于 M 个数组 data 首位相连,最大和子序列可能会跨越多个数组。返回的值是 max_single(单个数组的最大子数组和)和 max_crossing(跨越边界的最大子数组和)中的较大值。
先以 M == 2 为例:
求跨越两个数组的最大和子序列,由于数组都是 data,可以把这个序列看成是数组 data 的最大后缀和序列和最大前缀和序列相连。
跨越边界的最大和为最大前缀和加上最大后缀和
max_crossing = prefix_sum + suffix_sum
当 M>2 时,考虑到最大和子序列会跨越多个数组,比如说多个数组 data 整体都被包含在里面。所以要计算 data 的总和,如果总和大于0,max_crossing 还要加上 M-2 个数组总和(除去首尾的数组)。
total_sum = sum(data)
if M > 2 and total_sum > 0:
max_crossing = max_crossing + (M - 2) * total_sum
完整代码
def solution(N, M, data):
def kadane(arr):
max_ending_here = max_so_far = arr[0]
for x in arr[1:]:
max_ending_here = max(x, max_ending_here + x)
max_so_far = max(max_so_far, max_ending_here)
return max_so_far
# the maximum subarray sum for a single `data` array
max_single = kadane(data)
# the maximum subarray sum that may span the boundary
if M > 1:
# the maximum prefix sum
prefix_sum = float('-inf')
current_sum = 0
for i in range(N):
current_sum += data[i]
prefix_sum = max(prefix_sum, current_sum)
# the maximum suffix sum
suffix_sum = float('-inf')
current_sum = 0
for i in range(N-1, -1, -1):
current_sum += data[i]
suffix_sum = max(suffix_sum, current_sum)
max_crossing = prefix_sum + suffix_sum
total_sum = sum(data)
if M > 2 and total_sum > 0:
max_crossing = max_crossing + (M - 2) * total_sum
return max(max_single, max_crossing)
return max_single
# test
if __name__ == "__main__":
print(solution(5, 1, [1, 3, -9, 2, 4]) == 6)
print(solution(5, 3, [1, 3, -9, 2, 4]) == 11)
print(solution(24, 11, [-5, -12, 7, -4, 22, -1, -2, 2, -22, 1, -9, -21, -16, -23, -13, 18, -16, 8, 16, 14, -5, 1, 2, 14]) == 60)