问题
已知一个长度为n的数组(允许负数), 和一个整数k, 求:和小于k的最长连续子串?
例如:
k=184, A=[431, -15, 639, 342, -14, 565, -924, 635, 167, -70],
和小于184的最长子串为A[3..6]
写个简单测试先
require_relative '../longest_subarray_with_sum_less_than_k'
describe LongestSub do
let(:array) {[431, -15, 639, 342, -14, 565, -924, 635, 167, -70]}
let(:bound) {184}
let(:result) {subject.longest(array,bound)}
let(:ans) {array[3..6]}
it "should compute the longest subarray whose sum <= bound" do
expect(result).to eq ans
end
end
分析
如果暴力枚举起点和终点,复杂度为O(n^2)。能不能找到一些规律优化呢?
Make hands dirty
单纯考虑原数组好像找不到规律。
考虑前缀和,此时问题转换为已知pre数组,求pre[j]-pre[i]<=k的距离最大(i,j)对.
由于有负数,pre数组没有递增规律。
第一个元素的右端点,就已经难以确定了,必须从右到左扫描,直到差值<=k,有可能一个都不成功;而且,算好i后,算i+1时好像还是要扫描所有之后的右端点。
于是考虑对pre排序, 然后可以用“双指针”,不断移动右指针直到>k,并一直保存当前最右下标。这样需要复杂度O(nlogn)排序+O(n) == O(nlogn)
Make it better
还有更好的方法吗?有没有什么规律,来排除一些值呢?
可以发现右端点中,如果j1
而且发现,这样过滤后的右端点在pre上是递增的,就可以用“双指针”求了。
复杂度 = O(n)过滤 + O(n)扫描 = O(n)
代码:
class LongestSub
def longest(arr,bound)
reset(arr,bound)
l,r = [(0...size),poss_r].map(&:to_enum)
loop { update(l,r) }
arr[@longest_range]
end
def update(l, r)
i,j = [l,r].map(&:peek)
if i<=j && presum(j,i)<=bound
@longest_range = [@longest_range, (i..j)].max_by(&:size)
r.next
else
l.next
end
end
private
attr_reader :arr, :bound
def reset(arr,bound)
@arr,@bound=[arr,bound]
@poss_r = @pre = nil
@longest_range = (0...0)
end
def size; arr.size end
def pre
@pre ||= arr.reduce([0]){|res,v| res << res.last+v}
end
def presum(j,i=0)
pre[j+1] - pre[i]
end
def poss_r
@poss_r ||= (0...size).reverse_each.reduce([]) do |res,i|
res.unshift(i) unless res.first && presum(i) >= presum(res.first)
res
end
end
end