题目地址: https://www.hackerrank.com/challenges/maximum-subarray-sum/problem?isFullScreen=false
简述:在给定正整数组中,找到任意连续子数组求和后对M取余的最大值。
栗子
当a = [3,2,7,4] 且 m=7 时,返回6。因为子数组[2,7,4]求和后对7取余可取到最大值6。
难度:Hard
解题
先正常搞。直接切片一行搞定。结果肯定是无法通过全部测试的。原因主要是因为切片的执行时间太长,其次是因为有2个for迭代。
def maximumSum(a, m):
return max([sum(a[i:j]) % m for i in range(len(a)) for j in range(i+1,len(a)+1)])
通过分析子数组情况我们可知,例子中子数组 [3,2,7] 和子数组 [3,2] 存在交集或者说重复的部分。若我们已经知道子数组 [3,2] 对m求余的值为R[3,2],则 [3,2,7] 对m取余的值 R[3,2,7] = (R[3,2] + 7) % m。知道这一点以后,我们便可以构建数组a的prefix sum 来解决这个问题。
接上面的例子,数组a的对m求余的prefix sum数组presum_a = [3,5,5,2],其中
presum_a[1] = (presum_a[0] + a[1]) % m = (3+2)%7 = 5
presum_a[2] = (presum_a[1] + a[2]) % m = (5+7)%7 = 5
当我们有了presum_a以后,事情就好办了。由于求余运算的性质,presum_a中每个元素取值范围为 [0, 7) ,并且通过观察我们可知presum_a[3] < presum_a[0],可知a的子数组[2,7,4]是一个值得考虑的对象。因为当j > i时,presum_a中任意两个元素差值的绝对值abs(presum_a[i] - presum_a[j])就代表a中从i到j的子数组求和并对m取余的值,与m之间的差值,即
abs(m - (a[i] +... + a[j])%m)
若这个差值越小,则代表我们我们求和取余的值越大。因此,由于abs(presum_a[3] - presum_a[0]) = 1,这代表a[1]到a[3]至少加了一个6或者6的倍数,而6是对7求余能够得到的最大值。
那么问题就简单啦,根据上面描述的例子,当我们只需要找到presum_a中任意两元素差值绝对值的最小值d = min(abs(presum_a[i] - presum_a[j])),并且拿m - d与presum_a中的最大值作比较max(max(presum_a), m-d),选择两者中大的那一个即可。
def maximumSum(a, m):
# 原始数组a的值在做完prefix sum后没有价值了,不需要保存,因此可直接在原数组上滚。
a[0] = a[0]%m
for i in range(1,len(a)):
a[i] = (a[i - 1] + a[i])%m
max_d = max(a) # 拿到滚出来的最大值
a = list(zip(a,[i for i in range(len(a))])) # 把元素index贴上,用来判断j > i
a.sort() # 来一手排序,减少找任意两元素差值最小值的计算量
d = min([abs(a[i][0]-a[i+1][0]) for i in range(len(a)-1) if a[i][1] > a[i+1][1]])
return max(max_d,m-d)
程序整体除了排序O(nlogn)时间,其余O(n)时间,因此程序整体O(nlogn)时间。