如何在整数数组中查找所有有序元素对,其总和位于给定的值范围内
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 <= arr[i]+arr[low] <= arr[i]+arr[high] <= b
和当i
增加时,我们应该做的是减少low
和high
来保持条件。为了避免重复计算同一对,我们保留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;
【讨论】:
这在我看来是 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 <= 1
背后的逻辑吗?为什么允许前向指针i
超过后向指针j
1?
这只是一个错误,出乎意料地在我运行代码时并没有影响结果。
更常用的符号似乎是while i <= j
。然后是loop jamming,变量名比i
和j
更具暗示性,……
我认为我写不等式的方式是吹毛求疵,而 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]
条件为>=a
和<=b
。即使有了这个更正,count = ((result >= _min) & (result <= _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 <= 2*el && 2*el <= 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))
【讨论】:
以上是关于如何在整数数组中查找所有有序元素对,其总和位于给定的值范围内的主要内容,如果未能解决你的问题,请参考以下文章
谷歌面试:在给定的整数数组中找到所有连续的子序列,其总和在给定范围内。我们能比 O(n^2) 做得更好吗?
【C语言】查找:给定有10个元素的整数数组,输入一个数,在数组中查找是该数