【问题标题】:Length of longest subarray of sum less than or equal to k和小于或等于 k ​​的最长子数组的长度
【发布时间】:2017-03-16 10:44:18
【问题描述】:

在一次采访中,有人问我这个问题:给定一些正整数数组 s,求最长子数组的长度,使其所有值的总和小于或等于某个正整数 k。每个输入总是至少有一个解决方案。数组不是圆形的。

我开始编写一个动态编程解决方案,该解决方案通过在从 0 到 k 越来越大的值处找到最大长度来工作。

这是我在 python 中的代码,里面有一个我似乎找不到的错误,我的答案总是相差几位数:

def maxLength(s, k):
    lengths = [0 for x in range(k)]
    for i in range(1,k+1):
        for j in range(len(s)):
            if s[j] <= i and lengths[i - s[j]] + 1 > lengths[i]:
                lengths[i] = lengths[i - s[j]] + 1
        if i + 1 == len(s):
            break
    return lengths[-1]

输入1:s = [1,2,3], k = 4

输出1:2

输入2:s=[3,1,2,1], k = 4

输出2:3

【问题讨论】:

  • 第一个示例是如何工作的? [1, 2, 3] 没有连续子数组,其中整数加起来为 4。
  • 我们只需要找到最长的长度而不是具体的子数组
  • 当然,但是第一种情况仍然没有意义。那里有 2 个长度为 2 的子数组:[1, 2][2, 3]。仅当您认为数组是循环的时,此示例才有意义。 I.a.数组中的最后一个元素与第一个元素相邻。那么[3, 1]也是一个连续子数组,加起来是4个。
  • 你的意思是“和 k 的最长连续子数组的长度”吗?

标签: python algorithm dynamic-programming


【解决方案1】:

我认为这可行...(递归并从问题中取出 contiguous 要求,因为这似乎与问题中提供的示例输出不匹配)并且 OP 提到问题是:

给定一些正整数数组s,求最长子数组的长度,使得所有值的总和等于某个正整数k。

def longest_sum(input_list, index, num_used, target_number):
    if target_number == 0:
        return num_used
    if index >= len(input_list):
        return 0

    # Taken
    used_1 = longest_sum(input_list, index + 1, num_used + 1, target_number - input_list[index])
    # Not taken
    used_2 = longest_sum(input_list, index + 1, num_used, target_number)
    return max(used_1, used_2)


if __name__ == "__main__":
    print(longest_sum([2, 1, 8, 3, 4], 0, 0, 6))
    print(longest_sum([1, 2, 3], 0, 0, 4))
    print(longest_sum([3, 1, 2, 1], 0, 0, 4))
    print(longest_sum([1, 2, 7, 8, 11, 12, 14, 15], 0, 0, 10))
    print(longest_sum([1, 2, 3], 0, 0, 999))
    print(longest_sum([1, 1, 1, 1, 1, 1, 4], 0, 0, 6))

输出:

3
# BorrajaX's note: 2 + 1 + 3
2
# BorrajaX's note: 3 + 1
3
# BorrajaX's note: 1 + 2 + 1
3
# BorrajaX's note: 1 + 2 + 7
0
# BorrajaX's note: No possible sum
6
# BorrajaX's note: 1 + 1 + 1 + 1 + 1 + 1

编辑 01:

如果你想获取给你最长总和的列表,你总是可以这样做:

import copy


def longest_sum(input_list, used_list, target_number):
    if target_number == 0:
        return used_list

    if not input_list:
        return []

    # Taken
    used_list_taken = copy.copy(used_list)
    used_list_taken.append(input_list[0])
    used_1 = longest_sum(input_list[1:], used_list_taken, target_number - input_list[0])

    # Not taken
    used_list_not_taken = copy.copy(used_list)
    used_2 = longest_sum(input_list[1:], used_list_not_taken, target_number)
    if len(used_1) > len(used_2):
        return used_1
    else:
        return used_2


if __name__ == "__main__":
    print(longest_sum([2, 1, 8, 3, 4], [], 6))
    print(longest_sum([1, 2, 3], [], 4))
    print(longest_sum([3, 1, 2, 1], [], 4))
    print(longest_sum([1, 2, 7, 8, 11, 12, 14, 15], [], 10))
    print(longest_sum([1, 2, 3], [], 999))
    print(longest_sum([1, 1, 1, 1, 1, 1, 4], [], 6))

你会看到:

[2, 1, 3]
[1, 3]
[1, 2, 1]
[1, 2, 7]
[]
[1, 1, 1, 1, 1, 1]

PS 1:如果没有递归提供的快速回溯功能,我真的不知道该怎么做……抱歉:-(

PS 2:如果这不是你想要的(我提到我从要求中取出了 contiguous 要求)告诉我,我会删除这个答案。

【讨论】:

    【解决方案2】:

    短一点并假设非圆形 s:在 s 上减小尺寸的滑动窗口。

    def maxlen(s, k):
        for win in range(k, 0, -1):
            for i in range(0, len(s) - win):
                if sum(s[i:i+win]) == k:
                    return win
        return None
    
    s = [3,1,2,3]
    k = 4
    print(maxlen(s, k))
    

    【讨论】:

      【解决方案3】:

      您可以在线性 (O(n)) 时间内做到这一点:

      def max_length(s, k):
          # These two mark the start and end of the subarray that `current` used to be.
          subarray_start = 0
          subarray_end = 0
      
          subarray_sum = 0
          max_len = -1 # returns -1 if there is no subsequence that adds up to k.
          for i in s:
              subarray_sum += i
              subarray_end += 1
              while subarray_sum > k: # Shrink the array from the left, until the sum is <= k.
                  subarray_sum -= s[subarray_start]
                  subarray_start += 1
      
              # After the previous while loop, subarray_sum is guaranteed to be 
              # smaller than or equal to k.
              max_len = max(max_len, subarray_end - subarray_start)
      
          return max_len
      

      原来的问题有些混乱,我认为我们正在寻找一个总和 ** 等于(但不小于)k* 的子数组。 我的原始答案如下。那里也有关于此解决方案的线性度的信息,如果您有兴趣,请继续阅读。

      原答案

      我会这样做:

      def max_length(s, k):
          current = []
          max_len = -1 # returns -1 if there is no subsequence that adds up to k.
          for i in s:
              current.append(i)
              while sum(current) > k: # Shrink the array from the left, until the sum is <= k.
                 current = current[1:]
              if sum(current) == k:
                  max_len = max(max_len, len(current))
      
          return max_len
      

      这利用了这样一个事实,即我们正在寻找一个连续的子数组来获得具有线性 (O(n)) 时间复杂度的解决方案。 current 是我们当前尝试创建一个加起来为k 的子数组。我们遍历s 并将s 中的每个元素添加到current。如果current 的总和变得太大(大于k),我们从current 的左侧删除元素,直到总和小于或等于k。如果在任何时候,总和等于k,我们记录长度。


      嗯...我撒了谎,Francisco Couzo 抓住了我。上面的代码并不是真正的 O(n) 我正在调用 len(current)sum(current),它们最多需要 n 步骤,使算法以二次时间 (O(n^2)) 运行。我们可以通过自己跟踪current 的大小和总和来解决此问题。

      下面的版本让我们更接近 O(n),但我在编写它时注意到了一个问题。

      def max_length(s, k):
          current = []
          len_current = 0
          sum_current = 0
          max_len = -1 # returns -1 if there is no subsequence that adds up to k.
          for i in s:
              current.append(i)
              sum_current += i
              len_current += 1
              while sum_current > k: # Shrink the array from the left, until the sum is <= k.
                  sum_current -= current[0]
                  current = current[1:]
                  len_current -= 1
              if sum_current == k:
                  max_len = max(max_len, len_current)
      
          return max_len
      

      这段代码可能看起来像 O(n),如果它是用 Go 编写的,那就是。看到current = current[1:]了吗?根据TimeComplexities article in the Python wiki,从列表中获取切片需要 O(n)。

      我找不到从一开始就删除元素的列表操作,直到我突然意识到我不必这样做。 current 总是s 的连续子数组,那么为什么不直接标记它的开始和结束呢?

      所以这是我的最终解决方案:

      def max_length(s, k):
          # These two mark the start and end of the subarray that `current` used to be.
          subarray_start = 0
          subarray_end = 0
      
          subarray_sum = 0
          max_len = -1 # returns -1 if there is no subsequence that adds up to k.
          for i in s:
              subarray_sum += i
              subarray_end += 1
              while subarray_sum > k: # Shrink the array from the left, until the sum is <= k.
                  subarray_sum -= s[subarray_start]
                  subarray_start += 1
              if subarray_sum == k:
                  max_len = max(max_len, subarray_end - subarray_start)
      
          return max_len
      

      如果您认为数组是循环的,问题中的第一个示例似乎表明了这一点,您可以遍历数组两次:

      def max_length(s, k):
          s = s + s
          # These two mark the start and end of the subarray that `current` used to be.
          subarray_start = 0
          subarray_end = 0
      
          subarray_sum = 0
          max_len = -1 # returns -1 if there is no subsequence that adds up to k.
          for i in s:
              subarray_sum += i
              subarray_end += 1
              while subarray_sum > k: # Shrink the array from the left, until the sum is <= k.
                  subarray_sum -= s[subarray_start]
                  subarray_start += 1
              if subarray_sum == k:
                  max_len = max(max_len, subarray_end - subarray_start)
      
          return max_len
      

      根据您在第一遍中遇到的值,您可能可以进行一些检查以更快地摆脱第二遍。

      【讨论】:

      • 这很优雅!
      • 这个解决方案目前是 O(n^2),因为每次迭代都会调用 sum,但无论如何它很容易解决。
      • @FranciscoCouzo 是的。为了完整起见,我将添加 O(n) 版本。
      • 哦,所以他们需要求和到
      • 我的问题是数组是非圆形的并且总和 s = s + s 和条件if subarray_sum == k 的情况下对此进行了测试,效果很好。
      【解决方案4】:

      原答案

      最初的问题是求和为 k 的最长子数组的长度。

      您可以遍历列表索引,将每个索引作为求和窗口的起点。然后,您可以遍历从起始索引到末尾的索引以标记窗口的结束。在每个步骤中,您都需要求和,或者更好的是,将其添加到求和项中。如果总和超过了目标,您将跳出内部循环,继续前进。

      看起来像这样:

      def get_longes(a_list, k):
          longest = 0
          length = len(a_list)
          for i in xrange(length):
              s = 0
              for j in xrange(i,length):
                  s+=a_list[j]
                  if s < k:
                      pass
                  elif s==k:
                      longest = j+1-i
                  else:
                      break
          return longest
      

      这可以进一步加快速度,因为当您在外循环中移动一步时不需要重置窗口大小。实际上,您只需要跟踪窗口大小并在外部循环继续移动时将其减小 1。这样你甚至可以摆脱内循环,在O(n)中写一些东西:

      def get_longest(a_list,k):
          length=len(a_list)
          l_length = 0
          longest = 0
          s = 0
          for i in xrange(length):
              while s<k:  # while the sum is smaller, we increase the window size
                  if i+l_length==length: # this could also be handled with a try, except IndexError on the s+=a_list[... line
                      return longest
                  s+=a_list[i+l_length]
                  l_length+=1
              if s == k:  # if the sum is right, keep its length if it is bigger than the last match.
                  longest = max(l_length, longest)
              l_length-=1  # keep the window end at the same position (as we will move forward one step)
              s-=a_list[i]  # and subtract the element that will leave the window
          return longest
      

      更新问题的答案

      更新后的问题要求求和等于或小于 k 的最长子数组。

      对于这个问题,基本方法是相同的,实际上解决方案变得更加简单,因为现在我们对总和只有两个条件,即:

      1) 总和小于或等于 k。

      2) 总和大于 k。

      解决方案如下:

      def get_longest_smaller_or_equal(a_list,k):
          length=len(a_list)
          l_length = 0
          longest = 0
          s = 0
          for i in xrange(length):
              while s<=k:  # while the sum is smaller, we increase the window size
                  longest = max(l_length, longest)
                  if i+l_length==length: # this could also be handled with a try, except IndexError on the s+=a_list[... line
                      return longest
                  s+=a_list[i+l_length]
                  l_length+=1
              l_length-=1  # keep the window end at the same position (as we will move forward one step)
              s-=a_list[i]  # and subtract the element that will leave the window
          return longest
      

      【讨论】:

      • 我喜欢这个答案的教学方法:首先是基本思想及其实现。然后通过使用子数组的连续性来加速代码来改进这个想法,这也是一个实现。此外,这个答案确实需要通过整个列表运行,@bigblind 的答案就是这种情况。一旦索引加上窗口长度达到列表长度,此解决方案就会停止......非常好!
      • @dendragon 谢谢!您写道,此解决方案不一定需要遍历整个列表,但是,在每次迭代时,它都需要检查窗口是否在列表中(if i+l_length==length 语句),这是您没有的条件如果您将窗口放在索引的左侧,就像@bigblind 所做的那样。所以我认为他的回答可能会更快。
      • 哦,对了。没有意识到这一点。好吧,我猜k 相对于列表中的各个元素来说很大,那么您的答案可能仍然更快。
      • 当我运行这个时,上面提到的第一个输入失败了
      • @nonsequiter 它将第一个输入视为不可解(因此返回 0 作为长度)。这是您提出的原始问题的正确解决方案(等于 k)。我会根据您对问题所做的修改(即等于或小于 k)对其进行调整。
      【解决方案5】:

      这是一个适用于任何可迭代s(甚至是迭代器)的解决方案。它本质上与bigblind's answer 的算法相同,但如果k 相对于s 中的值较大(因此相关子序列的长度很长),它会更有效:

      import itertools
      
      def max_length(s, k):
          head, tail = itertools.tee(s)
          current_length = current_sum = 0
          max_len = -1 # returns -1 if there is no subsequence that adds up to k.
      
          for i in head:
              current_length += 1
              current_sum += i
      
              while current_sum > k:
                 current_length -= 1
                 current_sum -= next(tail)
      
              if current_sum == k:
                  max_len = max(max_len, current_sum)
      
          return max_len
      

      因为我们在迭代时不保留我们正在检查的子序列的列表,所以这种基于迭代器的方法仅在您只需要最长子序列的长度而不是其实际内容时才有用。

      如果您想获得最长子序列的副本,您可以对 bigblind 的答案使用不同的变体,使用 collections.dequeue 而不是列表(这样从左侧弹出很快)并跟踪运行像我的代码一样求和(所以你不需要一遍又一遍地调用sum):

      import collections
      
      def max_subsequence(s, k):
          current = collections.dequeue()
          current_sum = 0
          max_len = -1
          max_seq = None # returns None if there is no valid subsequence.
      
          for i in s:
              current.append(i)
              current_sum += i
      
              while current_sum > k: # Shrink from the left efficiently!
                 current_sum -= current.popleft()
      
              if current_sum == k:
                  if len(current) > max_len:
                      max_len = len_current
                      max_seq = list(current) # save a copy of the subsequence
      
          return max_seq
      

      如果您的问题标题具有误导性,并且您实际上并不关心子序列是否连续,那么我认为您当前的动态编程方法可以满足您的需求。我只是不完全确定我理解你的循环是如何工作的。我认为最自然的做法是对输入项进行外部循环,然后对包含该值的潜在总和进行第二次循环(它们是lengths 列表中的索引)。我还建议使用None 作为除0 之外的所有长度的初始值,否则在没有特殊情况的情况下很难使条件正常工作。

      def max_length(s, k):
          lengths = [None for _ in range(k+1)]
          lengths[0] = 0
      
          for x in s:
              for i in range(k, x-1, -1): # count down to avoid duplication
                  if lengths[i-x] is not None and (lengths[i] is None or
                                                   lengths[i-x] >= lengths[i]):
                      lengths[i] = lengths[i-x] + 1
      
          return lengths[k]
      

      【讨论】:

        【解决方案6】:

        这篇文章对你很有帮助。

        https://e2718281828459045.wordpress.com/2013/08/19/longest-subarray-whose-sum-k/

        可以使用
        求和数组 + 二分查找来解决。

        您得到的第一个观察结果是,如果我们考虑 ith 元素,那么我们必须继续 (i+1)th 等等。也就是说,我们需要按顺序添加所有元素,直到最后一个元素,否则我们达到总和。所以顺序很重要。

        我们如何添加数字。有n种方式存在。第一种方法是我们可以从第一个元素开始并添加直到我们到达 k 或者我们到达最后一个元素。第二种方法是我们可以从第二个元素开始读取,直到我们到达 k 或到达最后一个元素。

        所以蛮力算法会给你一个 O(n2) 解决方案。我们该如何改进呢?显然这不是预期的解决方案。对于每个我

        我们正在计算元素的总和并检查总和是否超过给定的“k”值。为了避免这种情况,我们可以创建一个 sum 数组。

        请记住,每当您遇到与序列求和(或给定数组中连续元素的总和)有关的问题时,很可能可以使用求和数组技术来解决。 Sum 数组是使用给定数组新建的数组。它可以使用以下公式生成,

        sum[i] = sum[i−1] + array[i]
        

        对于所有 i>0。和

        sum[i]=array[i]
        

        对于 i=0。

        求和数组可以在 O(n) 时间内创建。找到 ith 和 jth 之间的和变得容易。这就是区别,

        sum[j]−sum[i], j>i
        

        会给你答案。但它仍然是 O(n2) 解决方案。
        问题是对于i 的每个值,我们需要O(n) 时间来找到j
        那么我们怎样才能减少呢?
        答对了!二进制搜索来了。通过对每个i 的间隔in 使用修改后的二进制搜索,我们可以在O(logn) 时间内找到j。所以它只需要 O(nlogn) 时间。我们需要一个额外的变量和条件来存储子数组的长度,即j−i

        希望这会有所帮助。

        【讨论】:

          【解决方案7】:

          解决这个问题的方法

          O(n) - 两指针方法

          将第一个元素初始化为 s 和 e 和 subArray_sum = arr[0]

          现在如果 subArray_sum

          一旦 subArray_sum 变为 >= k 递增 s 直到变为

          O(nlog n) - 二分查找

          考虑所有可能的子数组长度 i.(1

          O(n*n) - 蛮力

          考虑所有可能的子数组(数量为 n*n)并找到最长的子数组

          上述问题的变体

          平均小于等于k的最长子数组的长度

          以上所有方法也适用于此

          【讨论】:

            猜你喜欢
            • 2016-12-20
            • 2021-12-29
            • 2012-11-08
            • 2019-12-25
            • 2020-11-11
            • 1970-01-01
            • 2019-09-26
            • 1970-01-01
            • 2021-06-04
            相关资源
            最近更新 更多