寻找第K小的配对距离 - 分析

17

问题:

这是来自 LeetCode 的一个问题:

给定一个整数数组,返回其中所有元素中第 k 小的差值。一对数字 (A, B) 的差值定义为 A 和 B 之间的绝对差。

示例:

Input:
nums = [1,3,1]
k = 1
Output: 0 
Explanation:
Here are all the pairs:
(1,3) -> 2
(1,1) -> 0
(3,1) -> 2
Then the 1st smallest distance pair is (1,1), and its distance is 0.

我的问题

我用一个朴素的方法O(n^2)解决了它,基本上我找到了所有的距离并对其进行排序,然后找到第k小的距离。现在有一个更好的解决方案。这不是我的代码,我在leetcode的讨论论坛上发现的。但我在理解代码的一个关键部分时遇到了麻烦。

下面的代码基本上执行二分搜索。 low 是最小距离,high 是最大距离。像通常的二分搜索一样计算 mid。然后执行 countPairs(a, mid) 来查找绝对差小于或等于 mid 的配对数。然后相应地调整 lowhigh

但为什么二分搜索的结果必须是其中一个距离?起初,lowhigh 是从数组中获取的,但是 mid 是由它们计算的,它可能不是距离。最后,我们返回了 low,而随着 mid + 1 的变化,它的值在二分搜索中发生了变化。为什么 mid + 1 保证是其中一个距离?

class Solution {
    // Returns index of first index of element which is greater than key
    private int upperBound(int[] a, int low, int high, int key) {
        if (a[high] <= key) return high + 1;
        while (low < high) {
            int mid = low + (high - low) / 2;
            if (key >= a[mid]) {
                low = mid + 1;
            } else {
                high = mid;
            }
        }
        return low;
    }

    // Returns number of pairs with absolute difference less than or equal to mid.
    private int countPairs(int[] a, int mid) {
        int n = a.length, res = 0;
        for (int i = 0; i < n; i++) {
            res += upperBound(a, i, n - 1, a[i] + mid) - i - 1;
        }
        return res;
    }

    public int smallestDistancePair(int a[], int k) {
        int n = a.length;
        Arrays.sort(a);

        // Minimum absolute difference
        int low = a[1] - a[0];
        for (int i = 1; i < n - 1; i++)
            low = Math.min(low, a[i + 1] - a[i]);

        // Maximum absolute difference
        int high = a[n - 1] - a[0];

        // Do binary search for k-th absolute difference
        while (low < high) {
            countPairs(a, mid)
            if (countPairs(a, mid) < k)
                low = mid + 1;
            else
                high = mid;
        }

        return low;
    }
}
2个回答

5
这种二分查找将找到第一个值x,使得countPairs(a,x) >= k。 (topcoder教程 很好地解释了这一点。)
因此,当函数以最终值low终止时,我们知道在从low-1到low的距离变化时,成对数目发生了变化,因此必须存在距离为low的一对。
例如,假设我们的目标是100,并且知道:
countPairs(a,9) = 99
countPairs(a,10) = 100

必须存在一对距离正好为10的数字,因为如果没有这样的一对数字,则距离小于或等于10的数字对的数量将与距离小于或等于9的数字对的数量相同。
请注意,这仅适用于循环运行直到完全耗尽正在测试的间隔的情况。 如果代码使用了早期终止条件,在找到精确目标值时退出循环,那么它可能会返回不正确的答案。

我知道这种二分查找会找到第一个满足 countPairs(a,x) >= k 的值 X。(只有在 countPairs(a, mid) < k 时才更新 low)。但是,为什么函数终止时 low 的值必须是距离 low 最近的一对?我不理解你在第二段中的逻辑,请再详细解释一下。 - OLIVER.KOO
我添加了一个例子,它可能有助于尝试构造反例。 - Peter de Rivaz
你的例子很有帮助,我理解了它,因为countPairs(a,x)和countPairs(a,x+1)之间相差1,这在二分查找中似乎不是这种情况,我们一次跳过了多个元素。按照你的例子,假设k=100lo=0hi=18,并且我们有mid=9。由于countPairs(a,9)=99<100,我们将更新lo=mid+1。现在我们有k=100lo=10hi=18。下一次搜索时,我们将查看的是countPairs(a,14)而不是countPairs(a,10) - OLIVER.KOO
我同意你所说的一切,但你需要再进行几次迭代。我们将查看countPairs(a,14)并将high设置为14。然后我们将查看countPairs(a,12)并将high设置为12。然后我们将查看countPairs(a,11)并将high设置为11。最后,我们将查看countPairs(a,10)并将high设置为10。关键是二进制搜索找到了countPairs(a,x)第一次达到目标的位置,在你的例子中当x等于10时。 - Peter de Rivaz
啊,我明白了,所以lo和hi最终会相差1。此时mid将是lo。因此,我们将检查countPairs(a,lo)= x。如果x小于k,我们将返回mid + 1,这是最后一次迭代中的hi。由于我们知道从上一次迭代开始,countPairs(a,lo)<k且countPairs(a,hi)> = k?我觉得我差不多懂了?但是我们如何知道lo和hi相遇的地方,即保证最终mid + 1值是由一对创建的距离?(当我打这段话时,我感到有些困惑,我曾经认为我理解了它) - OLIVER.KOO
惊人的教程。 - maddy

0

仅出于兴趣,我们可以使用快速傅里叶变换在O(n log n + m log m)时间内解决此问题,其中m是范围。

首先对输入进行排序。现在考虑到每个可达距离都可以通过从另一个差分前缀和中减去一个差异前缀和来实现。例如:

input:            1 3 7
diff-prefix-sums:  2 6
difference between 7 and 3 is 6 - 2

现在让我们将总和(最右侧的前缀和)加到等式的每一边:

ps[r] - ps[l]       = D
ps[r] + (T - ps[l]) = D + T

让我们列出它们的不同之处:

1 1 3
 0 2

还有前缀和:

p     => 0 0 2
T - p => 2 2 0  // 2-0, 2-0, 2-2

我们需要高效地确定和排序所有不同可达差异的计数。这类似于将系数为[1, 0, 2]的多项式与系数为[2, 0, 0]的多项式相乘(由于第二组中的零系数仅生成小于或等于T的度数,因此我们不需要它),我们可以在m log m时间内完成,其中m是度数,使用快速傅里叶变换。
结果系数将是:
  1 0 2
* 
  2 0 0
=> 
  x^2 + 2
*
  2x^2

= 2x^4 + 4x^2

=> 2 0 4 0 0

我们舍弃低于T的度数计数,并展示我们排序后的结果:

2 * 4 = 2 * (T + 2) => 2 diffs of 2
4 * 2 = 4 * (T + 0) => 4 diffs of 0

我们对0的差异进行了过多计数。也许有一种方便的方法可以计算出零的过度计数,希望有人能提供建议。我花了一些时间,但还没有找到一个。

无论如何,使用不相交的重复计数可以轻松地获得零差异的计数,这使我们仍然可以在O(n log n + m log m)的总时间内返回第k个差异。


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