标题中提到了两道题目,它们的题解中用到的核心算法是一致的,这一点光是从题目本身也能窥见一二,所以将它们放在了一起。接下来我将首先介绍用到的核心算法————Kadane算法。
Kadane算法
Kadane 算法是一种用于解决最大子数组和问题的动态规划算法。最大子数组和问题是指在一个给定的整数数组中,找到一个连续的子数组,使得该子数组的和最大(数组含有负数)。算法的核心思想是通过迭代数组的每个元素,维护两个变量来跟踪局部最优解和全局最优解,具体代码实现如下:
def kadane(array):
# Kadane算法实现
max_ending_here = max_so_far = array[0]
for x in array[1:]:
max_ending_here = max(x, max_ending_here + x)
max_so_far = max(max_so_far, max_ending_here)
return max_so_far
算法中维护的两个变量 max_ending_here 和 max_so_far,其中 max_ending_here 表示以当前元素为结尾的最大子数组和,max_so_far 表示截至目前的遍历中出现的最大子数组和。在每次迭代中,首先会将当前元素 x 与 max_ending_here 相加,然后将得到的值与 x 作比较,取其中更大的一个。max_so_far 则在每次迭代过程中都与 max_ending_here 比较取更大值。
在上述过程中,比较难以理解的是 max_ending_here 的维护过程,为何是与 x 本身作比较而不是与 max_ending_here 比较?以下为我个人的理解:首先,不与 max_ending_here 比较是因为这样做,如果 max_ending_here + x 更小,则最终结果取的是 max_ending_here,x 元素被抛弃了,而随着迭代的进行, max_ending_here 会与下一个 x 进行运算,这显然不符合“子数组”的要求。而如果是与 x 进行比较,那么当最终结果取 x,则意味着之后的迭代将以 x 为起始重新开始计算子数组。其次,通过比较 max_ending_here + x 和 x,可以选择局部最优的策略。如果 max_ending_here + x 大于 x,说明将 x 加入到之前的子数组中会得到更大的和,因此选择加入max_ending_here。反之,如果 x 大于 max_ending_here + x,说明开始一个新的子数组会得到更大的和,因此选择重新开始新的子数组。由于算法是对整个数组的遍历,所以最终的结果实际上就是整个数组的最优解。
接下来将用标题中的两个算法题作为应用实例。
例题1:
问题描述:
小C面对一个由整数构成的数组,他考虑通过一次操作提升数组的潜力。这个操作允许他选择数组中的任一子数组并将其翻转,目的是在翻转后的数组中找到具有最大和的子数组。小C对这个可能性很感兴趣,并希望知道翻转后的数组中可能得到的最大子数组和是多少。
例如,数组是 1,2,3,-1,4。小C可以选择翻转了数组-1,4得到 1,2,3,4,-1或者翻转 1,2,3,-1得到 -1,3,2,1,4,在这两种情况下,最大的子数组和都是 10。
测试样例:
输入:N = 5, data_array = [1, 2, 3, -1, 4] 输出:10
输入:N = 4, data_array = [-3, -1, -2, 3] 输出:3
输入:N = 3, data_array = [-1, -2, -3] 输出:-1
输入:N = 6, data_array = [-5, -9, 6, 7, -6, 2] 输出:15
输入:N = 7, data_array = [-8, -1, -2, -3, 4, -5, 6] 输出:10
输入:N = 100, data_array = [-85, -11, 92, 6, 49, -76, 28, -16, 3, -29, 26, 37, 86, 3, 25, -43, -36, -27, 45, 87, 91, 58, -15, 91, 5, 99, 40, 68, 54, -95, 66, 49, 74, 9, 24, -84, 12, -23, -92, -47, 5, 91, -79, 94, 61, -54, -71, -36, 31, 97, 64, -14, -16, 48, -79, -70, 54, -94, 48, 37, 47, -58, 6, 62, 19, 8, 32, 65, -81, -27, 14, -18, -34, -64, -97, -21, -76, 51, 0, -79, -22, -78, -95, -90, 4, 82, -79, -85, -64, -79, 63, 49, 21, 97, 47, 16, 61, -46, 54, 44] 输出:1348
解题过程:
这道题在最大子数组和的基础上增加了翻转的操作,则在实现中增加翻转子数组的遍历,给出所有可能的翻转操作,然后将操作后的数组交由Kadane算法解出最大子数组和,最后记录下其中的最大值即可。具体实现如下:
def solution(N, data_array):
# 计算不翻转的最大子数组和
max_sum_no_flip = kadane(data_array)
# 计算翻转后的最大子数组和
max_sum_with_flip = float('-inf')
# 两层循环遍历所有可能的子数组
for i in range(N):
for j in range(i, N):
# 翻转子数组 data_array[i:j+1]
flipped_array = data_array[:i] + data_array[i:j+1][::-1] + data_array[j+1:]
# 计算翻转后的最大子数组和
max_sum_with_flip = max(max_sum_with_flip, kadane(flipped_array))
# 返回最大值
return max(max_sum_no_flip, max_sum_with_flip)
例题2:
问题描述:
小R手上有一个长度为n的数组(n > 0),数组中的元素分别来自集合 [0,1,2,4,8,16,32,64,128,256,512,1024]。小R想从这个数组中选取一段连续的区间,得到可能的最大乘积。
你需要帮助小R找到最大乘积的区间,并输出这个区间的起始位置x和结束位置y(x ≤ y)。如果存在多个区间乘积相同的情况,优先选择x更小的区间;如果x相同,选择y更小的区间。
注意:数组的起始位置为 1,结束位置为 n。
测试样例:
输入:n = 5, arr = [1, 2, 4, 0, 8] 输出:[1, 3]
输入:n = 7, arr = [1, 2, 4, 8, 0, 256, 0] 输出:[6, 6]
输入:n = 8, arr = [1, 2, 4, 8, 0, 256, 512, 0] 输出:[6, 7]
解题过程:
这道题是最大子数组和的变体,求和变成了求乘积,并且还要求输出区间索引。在实现中,我对应原Kadane算法维护的两个变量,分别增加了相应的区间索引变量,随着迭代一起维护。因为要兼顾索引的维护,代码结构不如Kadane那么简洁,但核心思想是一样的,求和改为求乘积即可。
def solution(n: int, arr: list[int]) -> list[int]:
# Edit your code here
product = arr[0] # 记录以当前元素为结尾的的最大乘积
start = 0 # 记录以当前元素为结尾最大乘积区间的起始位置
end = 0 # 记录以当前元素为结尾最大乘积区间的结束位置
max_product = product # 记录最大乘积值
max_start = 0 # 记录最大乘积区间的起始位置
max_end = 0 # 记录最大乘积区间的结束位置
for i in range(1, n):
temp = arr[i] * product
if temp >= arr[i]:
product = temp
end = i
else:
product = arr[i]
start = end = i
if product > max_product:
max_product = product
max_start = start
max_end = end
return [max_start + 1, max_end + 1]