总和小于或等于k的最长子阵列的长度

时间:2016-11-02 23:29:53

标签: python algorithm dynamic-programming

在一次采访中,我被问到这个问题:给定一些正整数数组,找到最长子阵列的长度,使其所有值的总和小于或等于某个正整数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

7 个答案:

答案 0 :(得分:21)

您可以在线性(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,我们记录长度。

嗯......我撒了谎,弗朗西斯科·库佐在评论中抓住了我。上面的代码实际上不是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:]?根据{{​​3}},从列表中取一个切片需要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

根据你在第一次传球中遇到的值,你可以做的更快的检查可以更快地突破第二次传球。

答案 1 :(得分:7)

原始答案

最初的问题是要找到总和为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

答案 2 :(得分:3)

我认为这有效......(递归并从问题中取出连续的要求,因为这似乎与问题中提供的样本输出不匹配)并且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:如果这不是你想要的(我提到我从要求中取出了连续的要求)请告诉我,我会删除这个答案。

答案 3 :(得分:2)

这是一个适用于任何可迭代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]

答案 4 :(得分:1)

比特缩短并假设非圆形s:滑动窗口的尺寸越小s。

Question.route('answers', {
method:'post',
detail: true,
handler: function(req, res, next) {
    // req.params.id holds the resource's id
    res.send("I'm at /questions/:id/answers")
}

答案 5 :(得分:1)

解决此问题的方法

O(n) - 两个指针逼近

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

初始化
  

现在,如果subArray_sum&lt; k增加e而subArray_sum&lt; = k

     

一旦subArray_sum变为&gt; = k增量s,直到变为&lt; = k

O(nlog n) - 二进制搜索

考虑所有可能的子阵列长度i。(1&lt; = i&lt; = n)。在长度i的所有子阵列中找到具有最小总和的子阵列。对于给定的i值,这可以在O(n)中完成。 )。现在对于任何长度为i的子阵列,如果长度为i但具有最小和的子阵列的总和<= k,我们可以找到长度为&lt; = k的i长度子阵列。现在找到最长的i,使得存在子阵列总和&lt; = k的那个长度的子阵列。 使用start = 1和end = n;

对i的范围进行二进制搜索

O(n * n) - 暴力

考虑所有可能的子阵列(数量为n * n)并找到最长的总和&lt; = k

上述问题的变体

最长子阵列的长度,平均值小于或等于k

此处适用的所有方法也适用

答案 6 :(得分:0)

本文可以为您提供很多帮助。

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

可以使用 Sum Array +二进制搜索来解决。

你得到的第一个观察是,如果我们考虑i th 元素,那么我们必须继续(i + 1) th 等等。那就是我们需要在订单中添加所有元素,直到最后一个元素或者我们达到总和。所以订单很重要。

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

所以蛮力算法会给你一个O(n 2 ) 解。我们怎样才能改进呢?显然这不是预期的解决方案。对于每个我

  

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

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

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

表示所有i> 0。和

sum[i]=array[i]

表示i = 0。

可以在O(n)时间内创建Sum数组。找到i th 和j th 之间的总和变得容易。这是不同的,

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

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

希望这有帮助。