如何找到整数数组中所有有序元素对,其总和在给定范围内

11

给定一个整数数组,找到数组中所有有序元素对的数量,其和在给定范围[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

我知道我的解决方案不是最优的,你有更好的算法吗?


你需要找到所有的配对还是只需计数它们? - kraskevich
1
只需计算所有的配对。 - akashchandrakar
1
你的算法不是会把每一对都计算两次吗?难道你不应该写成... for j in range(i+1, len(array)): (这也意味着你不需要 if i != j - jcfollower
@jcfollower 没错,你说得对。我已经编辑了这个问题。 - akashchandrakar
9个回答

14
  1. 对数组进行排序(假设按递增顺序)。
  2. 对于数组中的每个元素 x:
    • 考虑元素后面的数组片段。
    • 在该数组片段上执行二分查找来查找 [a - x],记为 y0。如果没有找到完全匹配的值,则将最接近 [a - x] 并且比其大的值作为 y0。
    • 输出所有满足 x + y <= b 的元素 (x, y),其中 y0 到末尾均可。

时间复杂度显然与输出数量有关,但仍然优于现有算法:

O(nlogn) + O(k)

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

注意:如果你只需要计算配对数,那么可以在 O(nlogn) 的时间复杂度内完成。修改上述算法,使其也搜索小于 [b - x](或下一个更小的元素)。这样,你就可以通过第一次和最后一次匹配的索引来简单地计算每个元素具有的“匹配”数量,时间复杂度为 O(logn)。然后只需将它们相加即可获得最终计数。这样,初始的 O(nlogn) 排序步骤就是主导因素。


我正在编写完全相同的解决方案 :) - Ali Akber
3
这不是最优的。这个解决方案中的最后一个评论是误导性的:即使列表已经排序,它仍然是一个nlogn算法。你需要为每个索引执行两次二分查找,每次二分查找的成本为logn,并且有n个索引。我提供了一种排序+O(N)的解决方案。虽然当然两种解决方案都是nlogn,但是我的解决方案对于足够大的N来说严格更快。 - Nir Friedman
1
@wwii 我以恭敬而真诚的态度提问:你的观点是什么? - Nir Friedman
@NirFriedman,我只是想确认时间复杂度,然后我很天真地对结果进行了评论。最初,我认为实际时间值(T),而不是logT,应该与logN相关联。 - wwii
@wwii 绘制对数-对数图表是危险的,需要小心谨慎。首先,当您有一个幂关系时,例如t = aN^2时,对数-对数绘图更加适用。然后logt = loga + 2logN。但是,在这种情况下,即使在这里,拟合一条直线也可能会给您带来系统性的不正确结果,因为对数函数不对称地分布误差。 其次,在这种特殊情况下,您正在绘制t = NlogN对数-对数图,即logt = logN + loglogN。LoglogN增长非常缓慢,这就是为什么您的线看起来“几乎”是直的原因。 - Nir Friedman
显示剩余5条评论

11

首先对数组进行排序,然后通过两个索引计数对。这种基于两个索引的方法类似于2-sum问题中的方法,避免了二分查找需要进行N次的情况。算法的时间复杂度为排序复杂度 + O(N),通常排序的复杂度为O(NlnN),因此这种方法的时间复杂度为O(NlnN)。该算法的思想是,对于一个索引i,寻找一个下限和一个上限,使得a <= arr[i]+arr[low] <= arr[i]+arr[high] <= b,当i增大时,我们需要减小lowhigh来保持这个条件。为了避免重复统计同一对数,我们保证low > i,同时保持low <= 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;
}

1
这对我来说看起来是N^2。N代表循环迭代次数,每个迭代之间的两个while循环之间最多可以有N次迭代。考虑所有配对都有效的情况。 - Nir Friedman
2
@Nir Friedman:st的初始值并不是在每次外部循环中从零或i开始,而是在每次迭代中递增。如果在第一次迭代中找到了end(例如通过二分查找),并且在内部循环中递增,则情况可能会更好... - greybeard
1
@NirFriedman 这不是N^2,但我的上一个实现有点误导人。我已经重新实现了它,现在看起来更像是一个线性算法 :) - notbad
1
@zhiwenf 是的,写完后我意识到了。我现在相信之前的实现是正确的,但它写得非常令人困惑。新的实现很棒。我给出的解决方案类似,但 zhiwenf 更高效,因为他跳过了中间步骤。我会保留我的解决方案,因为它本质上生成了一个查找表,所以你不仅可以计数,还可以读取成对的值。但这个答案应该获得悬赏,做得好 zhiwenf。 - Nir Friedman
1
这确实是O(N)。我尝试了Python的实现,并相信我已经正确地实现了它 - 然而,这并没有提供与OP的函数相同的答案,那么OP的函数是否有误? - wwii
显示剩余5条评论

7
计算有效配对的问题可以在排序时间+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个数字,该数字在索引4处为8。因此,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 <= a,就可以做同样的事情了:

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

在我们的示例中,这段代码会给出(array 和 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个配对,对应索引为8。另一个细节是,这个算法会计算自身配对,如果不想要,就需要减去自身配对的数量。例如,3似乎包含了3-2+1=2个配对,分别在索引2和3处。当然,3本身在索引2处,所以其中一个是自身配对,另一个是与4的配对。只要output和output2的索引范围包含正在查找的索引本身,就需要减去1。在代码中,可以这样写:
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

2
你有没有可能把一个完整的实现搞出来? - Eric
你能解释一下 i - j <= 1 的逻辑吗?为什么允许前向指针 i 超过后向指针 j 1 个单位? - Eric
这只是一个错误,令人惊讶的是当我运行代码时它并没有影响结果。 - Nir Friedman
1
更常用的符号似乎是 while i <= j。然后,还有循环卡顿,变量名比 ij 更具启示性,... - greybeard
你应该返回答案的一半,以保持与 OP 发布的代码一致吗? - wwii
显示剩余2条评论

5
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。

这个答案在时间复杂度上并没有比原始答案更好,尽管可以承认它更清晰。 - Eric
“例如,您将(1,3)和(3,1)视为两个不同的组合”,这不是“有序对”的意思。 - Eric

3

在一些数据的限制下,我们可以在线性时间内解决问题(抱歉使用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!!!");
        }
    }
}

对于数组中的每个元素,我们知道配对中第二个元素可以落在哪个范围内。该算法的核心是以O(1)时间计算[a; b]范围内的元素数量。
结果复杂度为O(max(N, D)),其中D是数组中最大和最小元素之间的差值。如果这个值与N相同,则复杂度为O(N)。
注意事项:
- 没有涉及排序! - 构建查找表是必需的,以使算法适用于负数,并使第二个数组尽可能小(正面影响内存和时间) - 丑陋的条件if (a <= 2*el && 2*el <= b)是必需的,因为算法总是计算成对(a[i], a[i]) - 算法需要额外的O(d)内存,这可能很多。
另一个线性算法是基数排序+线性配对计数。
编辑。如果D比N小得多,并且不允许修改输入数组,则此算法可能非常好。这种情况的替代选择是略微修改的计数排序,其中分配计数数组(额外的O(D)内存),但不将排序后的元素填回到输入数组中。可以调整配对计数以使用计数数组而不是完整排序数组。

问题实际上是问题陈述没有提及D。因此,你的答案的算法复杂度在N方面没有上限。特别地,你将无法在不详细查看整个输入的情况下对运行时间进行上限设置,而其他算法只需要数组的大小即可设置上限。因此,总的来说,这是一个好主意,但由于我所述的原因,它并不完全奏效。只是想提供这个评论,以防你想知道为什么尽管这个想法很棒(确实如此!),但你却没有得到赞。 - Nir Friedman
1
@NirFriedman 谢谢!我指出复杂度将是O(max(N, D)),在一般情况下N没有保证。事实上,JVM不允许您创建比Integer.MAX_VALUE-5更大的数组(如果我没有记错的话)。对于其他平台,限制也类似 - .NET的int.MaxValue,Python的PY_SSIZE_T_MAX/sizeof(PyObject*)等。但是,如果您了解数据的其他信息,算法仍然可能很有用。例如 - 元素可以表示货币金额、物品重量或年龄。 - Jarlax
1
我同意领域知识可以将此算法提升到这里最好的算法之一。这也是我点赞的原因之一 :-) 我并不是说这个算法比其他任何算法都差,但它确实不够通用,而且由于用户的问题是通用的,因此它不是对手头特定问题的最佳答案。 - Nir Friedman

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

如果我发现更好的解决方案,我也会很高兴学习它 :-)。如果我找到了,我会更新这个答案。


1

我认为这是一个简单的数学问题,可以使用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)

Our result now looks like:

[[ 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转换为其他无意义的数字将是微不足道的....


8
您生成了一个N×N的矩阵,这使得您的算法无论多么简单都是O(N^2)。这个解决方案比朴素的双重for循环解决方案更糟糕,因为它使用了更多的空间。 - Nir Friedman
2
你可以将以下代码: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] - Eric
标准是 >=a<=b。即使进行了这个更正,count = ((result >= _min) & (result <= _max)).sum(),你的函数返回的结果比使用arr = [random.randint(-10, 10) for _ in xrange(1000)]; a = 6; b = 16的 OP 的函数少一个。 - wwii
嗯,现在我无法重现“one less”错误,是我的错。您的解决方案已重构 - wwii
各位提供了很好的建议;出于某种原因,我确实喜欢探索线性代数解作为典型循环解的替代方案。也许这是我的MatLab :)。@wwii感谢您的重构! - Quentin Donnellan

1
与其使用关系运算符,我们可以简单地检查数组元素 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 的数组元素求和。 - akashchandrakar
不够优化。这仍然是O(n^2)的时间复杂度。 - SinByCos

0
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))

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接