如何在整数数组中查找所有有序元素对,其总和位于给定的值范围内

Posted

技术标签:

【中文标题】如何在整数数组中查找所有有序元素对,其总和位于给定的值范围内【英文标题】:How to find all ordered pairs of elements in array of integers whose sum lies in a given range of value 【发布时间】:2015-02-26 14:42:19 【问题描述】:

给定一个整数数组,找出数组中所有有序元素对的数量,其和位于给定范围 [a,b]

这是相同的 O(n^2) 解决方案

'''
counts all pairs in array such that the 
sum of pair lies in the range a and b
'''
def countpairs(array, a, b):
    num_of_pairs = 0
    for i in range(len(array)):
        for j in range(i+1,len(array)):
            total = array[i] + array[j]
            if total >= a and total <= b:
                num_of_pairs += 1
    return num_of_pairs

我知道我的解决方案不是最优的 有什么更好的算法来做到这一点。

【问题讨论】:

你需要找到所有的配对还是只计算它们? 只计算所有对 你的算法不是每对计算两次吗?你不应该...for j in range(i+1, len(array)):(这也意味着你不需要if i != j @jcfollower 是的,你是对的,我已经编辑了问题。 您已将问题中的示例解决方案更改为不包含 (i, i)。您可以更改标题和问题定位的措辞给定一个整数数组,找出数组中所有有序元素对的数量…… 【参考方案1】:
    对数组进行排序(比如按升序排列)。 对于数组中的每个元素 x: 考虑数组切片元素之后。 在此数组切片上进行二分搜索,查找 [a - x],将其命名为 y0。如果未找到精确匹配,则将比 [a - x] 最接近的匹配 更大 视为 y0。 从 y0 向前输出所有元素 (x, y),只要 x + y

时间复杂度当然是输出敏感的,但这仍然优于现有算法:

O(nlogn) + O(k)

其中 k 是满足条件的对数。

注意:如果您只需要计算对的数量,您可以在O(nlogn) 中完成。修改上述算法,以便也搜索 [b - x](或下一个较小的元素)。这样,您可以简单地从第一个和最后一个匹配的索引中计算每个元素在O(logn) 中的“匹配”数。那么这只是一个将这些相加得到最终计数的问题。这样,初始的O(nlogn) 排序步骤占主导地位。

【讨论】:

我写的是完全相同的解决方案 :) 这不是最优的。此解决方案中的最后一条评论具有误导性:即使列表已经排序,它仍然是一个 nlogn 算法。您必须对每个索引进行两次二进制搜索,每次二进制搜索花费 logn 并且有 n 个索引。我给出了一个 sort + O(N) 的解决方案。当然,这两种解决方案都是 nlogn,但对于足够大的 N,我的解决方案绝对更快。 @wwii 我恭敬但真诚地问这个问题:你的意思是什么? @NirFriedman 当时我只是想确认时间复杂度,我天真地猜想,我只是对结果发表了评论。最初我认为实际时间值 (T),而不是 logT,应该与 logN 相关。 @wwii 绘制日志-日志图是危险的,需要小心。首先,当你有权力关系时,对数绘图更适合,例如t = aN^2。那么 logt = loga + 2logN。但是,即使在这里,拟合一条直线也会给您系统性地不正确的结果,因为 log 函数不会对称地分布误差。其次,在这种特殊情况下,您正在绘制 t = NlogN log-log,即 logt = logN + loglogN。 LoglogN 的增长速度非常缓慢,这就是为什么你的线看起来“几乎”笔直。【参考方案2】:

首先对数组进行排序,然后按两个索引对对进行计数。这两个索引方法类似于2-sum problem 中的方法,它避免了对N 的二分搜索。该算法的耗时为Sort Complexity + O(N),通常排序为O(NlnN),因此这种方法为O(NlnN)。该算法的思想是,对于一个索引i,找到一个下界和一个上界使得a &lt;= arr[i]+arr[low] &lt;= arr[i]+arr[high] &lt;= b和当i增加时,我们应该做的是减少lowhigh来保持条件。为了避免重复计算同一对,我们保留low &gt; i,也保留low &lt;= high。下面的计数方法的复杂度是O(N),因为在while loop中,我们能做的就是++i或者--low或者--high,最多有N这样的操作。

//count pair whose sum is in [a, b]
//arr is a sorted array with size integers.
int countPair(int arr[], int size, int a, int b) 
    int cnt = 0;
    int i = 0, low = size-1, high = size-1;
    while (i < high) 
        //find the lower bound such that arr[i] + arr[low] < a, 
        //meanwhile arr[i]+arr[low+1] >= a
         low = max(i, low);
         while (low > i && arr[i] + arr[low] >= a) --low;

        //find an upper bound such that arr[i] + arr[high] <= b 
        //meanwhile, arr[i]+arr[high+1] > b
        while (high > low && arr[i] + arr[high] > b) --high; 
        //all pairs: arr[i]+arr[low+1], arr[i]+arr[low+2],...,arr[i]+arr[high]
        //are in the rage[a, b], and we count it as follows.
        cnt += (high-low);
        ++i;
    
    return cnt;

【讨论】:

这在我看来是 N^2。 N 次 for 循环迭代,每个循环在两个 while 循环之间最多可以有 N 次迭代。考虑所有对都有效的情况。 @Nir Friedman:st 的初始值不会在外循环的每次迭代中从零或 i 开始,它会在每次迭代中增加。如果在第一次迭代中找到end(通过例如二分搜索),并在内部循环中增加,事情可能看起来更好...... @NirFriedman 这不是 N^2,但我的最后一个实现有点误导。我重新实现了它,它看起来更像是一个线性算法:) @NirFriedman 考虑所有对都有效的情况,st 将在每个循环中增加 1,但 end 将始终为 size-1。所以它是线性的:) @zhiwenf 是的,我写完后才意识到。我相信以前的实现现在是正确的,但是写得非常混乱。新的实现很棒。我给出的解决方案是类似的,但是 zhiwenf 更有效,因为他跳过了中间步骤。我将保留我的解决方案,因为它可能会生成一个查找表,因此您不仅可以计数,还可以读取这些对。但是这个答案应该得到赏金,干得好zhiwenf。【参考方案3】:

我有一个解决方案(实际上是 2 个解决方案 ;-))。用python写:

def find_count(input_list, min, max):
    count = 0
    range_diff = max - min
    for i in range(len(input_list)):
        if input_list[i]*2 >= min and input_list[i]*2 <= max:
            count += 1
        for j in range(i+1, len(input_list)):
            input_sum = input_list[i] + input_list[j]
            if input_sum >= min and input_sum <= max:
                count += 2

这将运行 nCr(n 个组合) 次到最大值,并为您提供所需的计数。这比对列表进行排序然后在一个范围内查找对要好。如果组合失败的元素数量更多并且所有数字都是正整数,我们可以通过添加一个检查元素的条件来更好地改进结果,

即使加上最大值也不在范围内的数字 大于范围最大数量的数字。

类似这样的:

# list_maximum is the maximum number of the list (i.e) max(input_list), if already known
def find_count(input_list, min, max, list_maximum):
    count = 0
    range_diff = max - min
    for i in range(len(input_list)):
        if input_list[i] > max or input_list[i] + list_maximum < min:
            continue
        if input_list[i]*2 >= min and input_list[i]*2 <= max:
            count += 1
        for j in range(i+1, len(input_list)):
            input_sum = input_list[i] + input_list[j]
            if input_sum >= min and input_sum <= max:
                count += 2

我也很乐意学习比这更好的解决方案 :-) 如果我遇到一个,我会更新这个答案。

【讨论】:

【参考方案4】:

计算工作对的问题可以在排序时间 + O(N) 内完成。这比 Ani 给出的解决方案更快,即排序时间 + O(N log N)。这个想法是这样的。首先你排序。然后,您运行几乎相同的单遍算法两次。然后,您可以使用这两个单遍算法的结果来计算答案。

第一次运行单遍算法时,我们将创建一个新数组,其中列出了可以与该索引合作的最小索引以给出大于 a 的总和。示例:

a = 6
array = [-20, 1, 3, 4, 8, 11]
output = [6, 4, 2, 2, 1, 1]

因此,数组索引 1 处的数字为 1(基于 0 的索引)。它可以配对以超过 6 的最小数字是 8,它在索引 4 处。因此 output[1] = 4。-20 不能与任何东西配对,所以 output[0] = 6(超出范围) .另一个例子:output[4] = 1,因为 8(索引 4)可以与 1(索引 1)或它后面的任何数字配对,总和大于 6。

你现在需要做的是说服自己这是 O(N)。它是。代码是:

i, j = 0, 5
while i - j <= 0:
  if array[i] + array[j] >= a:
    output[j] = i
    j -= 1
  else:
    output[i] = j + 1
    i += 1

想象一下从边缘开始向内工作的两个指针。是 O(N)。你现在做同样的事情,只是条件 b

while i-j <= 0:
  if array[i] + array[j] <= b:
    output2[i] = j
    i += 1
  else:
    output2[j] = i-1
    j-=1

在我们的示例中,此代码为您提供(数组和 b 供参考):

b = 9
array = [-20, 1, 3, 4, 8, 11]
output2 = [5, 4, 3, 3, 1, 0]

但是现在, output 和 output2 包含了我们需要的所有信息,因为它们包含了配对的有效索引范围。 output 是它可以配对的最小索引, output2 是它可以配对的最大索引。差 + 1 是该位置的配对数。所以对于第一个位置(对应于 -20),有 5 - 6 + 1 = 0 对。对于 1,有 4-4 + 1 对,索引 4 处的数字是 8。另一个微妙之处,这个算法计算自我配对,所以如果你不想要它,你必须减去。例如。 3 似乎包含 3-2 + 1 = 2 对,一个在索引 2,一个在索引 3。当然,3 本身在索引 2,所以其中一个是自配对,另一个是与 4 的配对。只要 output 和 output2 的索引范围包含您正在查看的索引本身,您只需减去一个。在代码中,你可以这样写:

answer = [o2 - o + 1 - (o <= i <= o2) for i, (o, o2) in enumerate(zip(output, output2))]

产量:

answer = [0, 1, 1, 1, 1, 0]

总和为4,对应于(1,8), (3,4), (4,3), (8, 1)

无论如何,如您所见,这是 sort + O(N),这是最优的。

编辑:要求全面实施。假如。供参考,完整代码:

def count_ranged_pairs(x, a, b):
    x.sort()

    output = [0] * len(x)
    output2 = [0] * len(x)

    i, j = 0, len(x)-1
    while i - j <= 0:
      if x[i] + x[j] >= a:
        output[j] = i
        j -= 1
      else:
        output[i] = j + 1
        i += 1

    i, j = 0, len(x) - 1
    while i-j <= 0:
      if x[i] + x[j] <= b:
        output2[i] = j
        i += 1
      else:
        output2[j] = i-1
        j -=1

    answer = [o2 - o + 1 - (o <= i <= o2) for i, (o, o2) in enumerate(zip(output, output2))]
    return sum(answer)/2

【讨论】:

你有没有机会组合一个完整的实现? 你能解释一下i - j &lt;= 1背后的逻辑吗?为什么允许前向指针i 超过后向指针j 1? 这只是一个错误,出乎意料地在我运行代码时并没有影响结果。 更常用的符号似乎是while i &lt;= j。然后是loop jamming,变量名比ij更具暗示性,…… 我认为我写不等式的方式是吹毛求疵,而 i 和 j 是循环索引的常用名称。也许不是最好的,但他们很好。这是 SO 而不是代码审查,这仍然是非常可读的代码。至于循环干扰,我不知道如何干扰这两个循环,如果您知道如何请务必编辑我的答案。【参考方案5】:

我相信这是一个简单的数学问题,可以使用numpy 解决,无需循环,也无需我们进行排序。我不完全确定,但我相信在更坏的情况下复杂度为 O(N^2)(希望有更多了解 numpy 时间复杂性的人对此进行确认)。

无论如何,这是我的解决方案:

import numpy as np

def count_pairs(input_array, min, max):
    A = np.array(input_array)
    A_ones = np.ones((len(A),len(A)))
    A_matrix = A*A_ones
    result = np.transpose(A_matrix) + A_matrix
    result = np.triu(result,0)
    np.fill_diagonal(result,0)
    count = ((result > min) & (result < max)).sum()
    return count

现在让我们来看看它 - 首先我只是创建一个矩阵,其中的列代表我们的数字:

A = np.array(input_array)
A_ones = np.ones((len(A),len(A)))
A_matrix = A*A_ones

假设我们的输入数组看起来像:[1,1,2,2,3,-1],因此,此时这应该是 A_matrix 的值。

[[ 1.  1.  2.  2.  3. -1.]
 [ 1.  1.  2.  2.  3. -1.]
 [ 1.  1.  2.  2.  3. -1.]
 [ 1.  1.  2.  2.  3. -1.]
 [ 1.  1.  2.  2.  3. -1.]
 [ 1.  1.  2.  2.  3. -1.]]

如果我将它添加到自身的转置中......

result = np.transpose(A_matrix) + A_matrix

...我应该得到一个代表对和的所有组合的矩阵:

[[ 2.  2.  3.  3.  4.  0.]
 [ 2.  2.  3.  3.  4.  0.]
 [ 3.  3.  4.  4.  5.  1.]
 [ 3.  3.  4.  4.  5.  1.]
 [ 4.  4.  5.  5.  6.  2.]
 [ 0.  0.  1.  1.  2. -2.]]

当然,这个矩阵是对角线的镜像,因为 (1,2) 和 (2,1) 对产生相同的结果。我们不想考虑这些重复的条目。我们也不想考虑项目与自身的总和,所以让我们清理我们的数组:

result = np.triu(result,0)
np.fill_diagonal(result,0)

我们的结果现在看起来像:

[[ 0.  2.  3.  3.  4.  0.]
 [ 0.  0.  3.  3.  4.  0.]
 [ 0.  0.  0.  4.  5.  1.]
 [ 0.  0.  0.  0.  5.  1.]
 [ 0.  0.  0.  0.  0.  2.]
 [ 0.  0.  0.  0.  0.  0.]]

剩下的就是计算符合我们标准的项目。

count = ((result > min) & (result < max)).sum()

请注意:

如果0 在可接受的域中,此方法将不起作用,但我确信操纵上面的结果矩阵以将那些 0 转换为其他一些无意义的数字将是微不足道的......

【讨论】:

您生成了 NxN 矩阵,无论生成矩阵多么简单,它都会立即使您的算法 O(N^2)。这个解决方案比简单的 double for 循环解决方案更糟糕,因为它占用了更多空间。 您可以将A_ones = np.ones((len(A),len(A))) A_matrix = A*A_ones result = np.transpose(A_matrix) + A_matrix 替换为result = A + A[:,np.newaxis] 条件为&gt;=a&lt;=b。即使有了这个更正,count = ((result &gt;= _min) &amp; (result &lt;= _max)).sum(),您的函数返回的结果也比使用 arr = [random.randint(-10, 10) for _ in xrange(1000)]; a = 6; b = 16 的 OP 函数少一个@ 嗯,现在我不能重现 one less 错误,我是过失。 Your solution refactored 伟大的建议伙计们;无论出于何种原因,我都喜欢探索线性算法。解决方案作为典型循环解决方案的替代方案。也许这就是我的 MatLab :)。 @wwii 感谢重构!【参考方案6】:
from itertools import ifilter, combinations

def countpairs2(array, a, b):
    pairInRange = lambda x: sum(x) >= a and sum(x) <= b
    filtered = ifilter(pairInRange, combinations(array, 2))
    return sum([2 for x in filtered])

我认为 Itertools 库非常方便。我还注意到您计算了两次配对,例如您将 (1, 3) 和 (3, 1) 计算为两种不同的组合。如果你不想这样,只需将最后一行中的 2 更改为 1。 注意:最后一个可以更改为return len(list(filtered)) * 2。这可以更快,但代价是使用更多 RAM。

【讨论】:

这在时间复杂性方面并不比原来的答案更好,虽然公认更清楚 “例如,您将 (1, 3) 和 (3, 1) 视为两种不同的组合” - 这不是 “有序对” 表示【参考方案7】:

由于对数据的一些限制,我们可以在线性时间内解决问题(对不起,Java,我对 Python 不是很精通):

public class Program 
    public static void main(String[] args) 
        test(new int[]-2, -1, 0, 1, 3, -3, -1, 2);
        test(new int[]100,200,300, 300, 300);
        test(new int[]100, 1, 1000);
        test(new int[]-1, 0, 0, 0, 1, 1, 1000, -1, 2);
    

    public static int countPairs(int[] input, int a, int b) 
        int min = Integer.MAX_VALUE;
        int max = Integer.MIN_VALUE;
        for (int el : input) 
            max = Math.max(max, el);
            min = Math.min(min, el);
        
        int d = max - min + 1; // "Diameter" of the array
        // Build naive hash-map of input: Map all elements to range [0; d]
        int[] lookup = new int[d];
        for (int el : input) 
            lookup[el - min]++;
        
        // a and b also needs to be adjusted
        int a1 = a - min;
        int b1 = b - min;
        int[] counts = lookup; // Just rename
        // i-th element contain count of lookup elements in range [0; i]
        for (int i = 1; i < counts.length; ++i) 
            counts[i] += counts[i - 1];
        
        int res = 0;
        for (int el : input) 
            int lo = a1 - el; // el2 >= lo
            int hi = b1 - el; // el2 <= hi
            lo = Math.max(lo, 0);
            hi = Math.min(hi, d - 1);
            if (lo <= hi) 
                res += counts[hi];
                if (lo > 0) 
                    res -= counts[lo - 1];
                
            
            // Exclude pair with same element
            if (a <= 2*el && 2*el <= b) 
                --res;
            
        
        // Calculated pairs are ordered, divide by 2
        return res / 2;
    

    public static int naive(int[] ar, int a, int b) 
        int res = 0;
        for (int i = 0; i < ar.length; ++i) 
            for (int j = i + 1; j < ar.length; ++j) 
                int sum = ar[i] + ar[j];
                if (a <= sum && sum <= b) 
                    ++res;
                
            
        
        return res;
    

    private static void test(int[] input, int a, int b) 
        int naiveSol = naive(input, a, b);
        int optimizedSol = countPairs(input, a, b);
        if (naiveSol != optimizedSol) 
            System.out.println("Problem!!!");
        
    

对于数组的每个元素,我们都知道该对中的第二个元素可以放置的范围。该算法的核心是计算范围 [a; b] 在 O(1) 时间内。

结果复杂度为 O(max(N, D)),其中 D 是数组的最大和最小元素之间的差。如果此值与 N 的阶数相同 - 复杂度为 O(N)。

注意事项:

不涉及排序! 需要构建查找才能使算法与负数一起工作 数字并使第二个数组尽可能小(积极 影响记忆和时间) 丑陋的条件if (a &lt;= 2*el &amp;&amp; 2*el &lt;= b) 是必需的,因为算法总是计算对 (a[i],a[i]) 算法需要 O(d) 额外内存,这可能很多。

另一种线性算法是基数排序 + 线性对计数。

编辑。如果 D 远小于 N 并且不允许修改输入数组,则此算法可能非常好。这种情况的替代选项是稍微修改计数排序,分配计数数组(额外的 O(D) 内存),但不将排序的元素填充回输入数组。可以调整对计数以使用计数数组而不是完全排序的数组。

【讨论】:

坦率地说,问题在于问题陈述中没有提到 D。所以你的答案的算法复杂性现在没有 N 的上限。特别是,你不能把运行时间的上限,而无需详细查看整个输入,而其他算法只需要数组的大小即可设置上限。所以总而言之,这是一个好主意,但由于我所说的原因,它并不完全奏效。只是想我会给出这个评论,以防你想知道为什么你没有得到支持,尽管这个想法很简洁(而且确实如此!)。 @NirFriedman 谢谢!我已经指出复杂性将是 O(max(N, D)) 并且在一般情况下不能保证 N 。实际上,JVM 不允许您创建大于 Integer.MAX_VALUE - 5 的数组(如果我没记错的话)。对于其他平台的限制是相似的 - .NET 的 int.MaxValue,Python 的 PY_SSIZE_T_MAX/sizeof(PyObject*) 等。但是如果您对数据算法有更多的了解,仍然会很有用。例如 - 元素可以表示金额或物品重量或年龄。 我同意领域知识可以将这个算法提升到最好的算法。这就是我 +1 的部分原因:-) 我并不是说这个算法比任何其他算法都差,但是它不那么普遍,并且由于用户的问题是一般性的,因此它不是对特定问题的好答案手头的问题。【参考方案8】:

我们可以简单地检查是否 数组元素 i 和 j 之和在指定范围内。

def get_numOfPairs(array, start, stop):
    num_of_pairs = 0
    array_length = len(array)

    for i in range(array_length):
        for j in range(i+1, array_length):
            if sum([array[i], array[j]]) in range(start, stop):
                num_of_pairs += 1

    return num_of_pairs

【讨论】:

sum([i,j]) 不会将数组形式 i 的元素相加到 j 不是最佳的。这仍然是 O(n^2)【参考方案9】:
n = int(input())
ar = list(map(int, input().rstrip().split()))[:n]
count=0
uniq=[]
for i in range(n):
    if ar[i] not in uniq:
        uniq.append(ar[i])
for j in uniq:
    if ((ar.count(j))%2==0):
        count=count+((ar.count(j))/2)
    if ((ar.count(j))%2!=0) & (((ar.count(j))-1)%2==0):
        count=count+((ar.count(j)-1)/2)
print(int(count))

【讨论】:

以上是关于如何在整数数组中查找所有有序元素对,其总和位于给定的值范围内的主要内容,如果未能解决你的问题,请参考以下文章

如何在javascript或3sum中计算3个元素的总和?

谷歌面试:在给定的整数数组中找到所有连续的子序列,其总和在给定范围内。我们能比 O(n^2) 做得更好吗?

【C语言】查找:给定有10个元素的整数数组,输入一个数,在数组中查找是该数

查找没有溢出的整数数组的总和

【C语言】查找:给定有10个元素的整数数组,输入一个数,在数组中查找是该数

最小唯一数组总和