和小于k的最长连续子串

问题

已知一个长度为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[j2],那肯定不会选j1, 因为j2既比j1远,求和时结果又比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
和小于k的最长连续子串_第1张图片

你可能感兴趣的:(和小于k的最长连续子串)