【问题标题】:Is there a way to quickly find in a range contains a number that is in a given range?有没有办法在一个范围内快速找到包含给定范围内的数字?
【发布时间】:2022-01-20 19:45:47
【问题描述】:

所以这里有一个问题,给我一个整数数组,它的数字都是不同的,假设它是

int[] data = {21, 34, 12, 88, 54, 73};

现在我想看看一个子数组或一个范围是否包含一个范围内的数字(这也是给定的)。换句话说,我想看看数组的一个范围是否包含一个范围内的数字。例如,如果我有一个函数check(int a, int b, int l, int r),其中ab 是数组的范围,lr 是数字的范围。

所以对于上面的数组,check(0, 2, 20, 50) 应该返回true,因为从index = 0 to 2,有21, 34, 12 并且有两个数字,21, 34,在20 to 50 的范围内。

所以另一个例子是check(2, 3, 20, 80)应该返回false,因为12, 88在20、80的范围内没有数字。

我正在考虑使用 Segment Tree,因为据我所知,RMQ(范围最小查询)可以通过使用 Segment Tree 来解决,因此我认为 Segment Tree 也可以解决这个问题;但是,Segment Tree 的所有"get" function 都是"single"(也许不是最好的词),所以,我想知道 Segment Tree 应该包含哪些节点。有没有什么算法可以回答O(log(n)) 中的每个查询,而"build" time 不是O(n^2),其中n 是数组的大小?

注意:使用 Segment Tree 只是我自己的想法,任何其他方法都值得赞赏。

【问题讨论】:

  • 如果你要anther way而不是segment tree,我可以展示如何找到ans。
  • array[i]的范围是多少?
  • @nice_dev,我现在明白了,这是一个问题,实际上,如果我想为此使用线段树,那就是我遇到的问题。这也是我要问的问题。我会更新问题以使其更清楚,谢谢。
  • 你能同时处理所有的查询吗?它使这个问题变得容易得多。
  • ...这就是您在 SO 上提问的原因 :) 我在已接受的答案中添加了评论。

标签: java arrays algorithm segment-tree


【解决方案1】:

这有点异国情调,但持久的红黑树,或任何其他自平衡树的持久变体都可以。

persistent data structure 允许人们(时间和空间)有效地在不同时间拍摄结构的“快照”,然后稍后查询这些快照,并根据截至快照时间的结构状态接收结果。对于这个用例,我们想要做的特定查询是计算给定范围内所有包含的元素(如果每个节点都使用其后代的数量进行注释,则可以在 O(log n) 中执行)。

在这种情况下,您将从一个空结构开始,在时间 i 插入 data[i],然后将快照存储为 snapshot[i]。然后,check(a,b,l,r) 将被实现为return snapshot[b].countInRange(l,r) > snapshot[a].countInRange(l,r)。也就是说,如果截至时间b 的目标范围内的元素多于截至时间a 的元素,则必须在ab 之间添加目标范围内的某些元素,从而满足你的约束。

如果实现最佳,预计算需要时间O(n log n) 和空间O(n),查询需要时间O(log n)


如果您愿意放宽对查询的O(log n) 要求,则更简单且可能更实用的方法是二维k-D tree。只需插入每个data[i] 作为点(i, data[i]),然后对a<=x<b, l<=y<r 进行范围搜索。这为您提供了O(sqrt(n)) 的查询时间,虽然效率不高,但更容易编写代码(或查找现有代码)。

【讨论】:

  • 如果使用持久性数据结构(我知道它会保留其历史/版本)并且随着时间的推移不断制作快照,那么空间复杂度怎么会只有O(n)。如果我误解了任何内容,请告诉我?
  • @miiiiii 快照存储为结构本身的一部分(这就是我所说的它们的空间效率)。每个只占用 O(1) 摊销的额外空间。
  • 感谢您的回复。但是那如何实现countInRange呢?对于每个 a,b,l,r 组合,它会预先计算/存储数据吗?
  • @miiiiii 它的实现就像您为常规红黑树实现它一样,通过递归访问与范围重叠的节点。它不是为任何特定的输入集预先计算的。
  • 由于 OP 在 cmets 中表示您提前拥有所有查询,因此您不需要使用持久树。制作两个按lr 排序的查询列表。然后将点按顺序插入到普通的顺序统计树中。当您交叉查询l 时,计算该范围内的值的数量。文你交叉查询r 计算范围内的值并减去先前的计数。如果答案>0,则查询满足。
【解决方案2】:

O(N) 很简单:

public static boolean check(int[] data, int a, int b, int l, int r) {
    return Arrays.stream(data, a, b + 1).anyMatch(n -> n >= l && n <= r);
}

我怀疑任何更有效的 big-O 方法都会花费足够的时间来构建所需的数据结构,除非您在巨大的数据集上进行 大量 查找,否则不值得付出努力。即使这样,也许上述的并行版本可能就足够了。

【讨论】:

  • 谢谢,但我需要进行大量查找...因此,如果需要数据结构来回答查询(在log(n) 中),我想在一段时间内构建它复杂度低于O(n^2)
【解决方案3】:

更新:

public static void main(String[] args) {
    int[] data = {21, 34, 12, 88, 54, 73, 99, 100};
    List<Integer> dataList = Arrays.stream(data).boxed().collect(Collectors.toList());
    System.out.println(searchRange(0, 2, 20, 50, data));
    System.out.println(searchRange(2, 3, 20, 80, data));
    System.out.println(searchRange(0, 2, 20, 22, data));    

public static boolean searchRange(int from, int to, int min, int max, int[] data) {
    // slice array
    data = Arrays.copyOfRange(data, from, to + 1);
    Arrays.sort(data);
    // System.out.println(Arrays.toString(data));
    int index = findInBoundaries(data, min, max);
    // System.out.println(index);
    return index != -1;
}

// return -1: no elements found.
static int findInBoundaries(int[] data, int min, int max) {
    int start = 0;
    int end = data.length - 1;
    int ans = -1;
    while (start <= end) {
        int mid = (start + end) / 2;
        // Break if found 
        if (data[mid] >= min && data[mid] <= max) {
            ans = mid;
            break;
        } 
        // Right move if element <= max
        else if (data[mid] <= max) {
            start = mid + 1;
        }
        // Left move
        else {
            end = mid - 1;
        }
    }
    return ans;
}

输出

true
false
true

此代码已经过多次测试。与我第一个独立达到最小和最大边界的答案不同,这是查找目标元素的范围以确定子数组是否包含合格的数字。

说明:

为了简化问题,我将其定义为任意数量的子数组都在给定范围内,并且该方法的时间复杂度应小于 O(n^2)。

一旦对数组进行了排序,就很容易在二分查找中进行。解决方案从中间元素 (int mid = (start + end) / 2) 开始搜索给定范围内的数字。当元素满足范围要求时,循环终止。如果小于(或小于等于)最大值,则搜索右侧(较大)元素,否则,将搜索左侧(较小)元素。在这种情况下,最大循环时间将为 O(log n),其中 n 是数组的大小。

示例:

我修改为通过添加计数器将解决方案与正常循环进行比较。在某些情况下,正常循环需要遍历整个数组。 正常解决方案的排序不是很重要,所以我不这样做。

// return -1: no elements found.
static void findBoundaryCompareMethods(int[] data, int min, int max) {
    int start = 0;
    int end = data.length - 1;
    int ans = -1;
    int count = 0;
    while (start <= end) {
        int mid = (start + end) / 2;
        count++;
        // Right move to find element > max 
        if (data[mid] >= min && data[mid] <= max) {
            ans = mid;
            break;
        } 
        else if (data[mid] <= max) {
            start = mid + 1;
        }
        // Left move
        else {
            end = mid - 1;
        }
    }
    System.out.println("Method 1 Find: " + ans);
    System.out.println("Method 1 Count: " + count);
    ans = -1;
    count = 0;
    for (int i = 0; i < data.length; i++) {
        count++;
        if (data[i] >= min && data[i] <= max) {
            ans = i;
            break;
        }
    }
    System.out.println("Method 2 Find: " + ans);
    System.out.println("Method 2 Count: " + count);
}

测试输出如下。方法一为答案解法,方法二为正解法。

输出

Array: [12, 21, 34]
Min: 20 Max: 50
Method 1 Find: 1
Method 1 Count: 1
Method 2 Find: 1
Method 2 Count: 2

Array: [12, 88]
Min: 20 Max: 80
Method 1 Find: -1
Method 1 Count: 2
Method 2 Find: -1
Method 2 Count: 2

Array: [12, 21, 34]
Min: 20 Max: 22
Method 1 Find: 1
Method 1 Count: 1
Method 2 Find: 1
Method 2 Count: 2

Array: [12, 21, 34, 54, 73, 88, 99, 100]
Min: 70 Max: 73
Method 1 Find: 4
Method 1 Count: 3
Method 2 Find: 4
Method 2 Count: 5

【讨论】:

  • System.out.println(searchRange(0, 2, 20, 22, data)); 返回false,但预期为trueminmax 的决定并不总是有帮助。
  • 请检查我的更新答案,谢谢。
  • 您的代码对每个查询进行排序,从而导致O(n log n) 查询时间。没有理由这样做。线性扫描会更快 (O(n))。
  • @WingKuiTsoi 你能补充一些解释吗?应避免仅代码答案。只有在那之后我才能进一步测试。
  • @WingKuiTsoi 对于每个查询,运行排序例程需要O(n log n) 时间。因此,如果有N 查询,则总复杂度为O(N * n log n)。如果您只是线性搜索每个查询的数组,则只需 O(N * n) 时间。
猜你喜欢
  • 2010-11-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-01-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-07-28
相关资源
最近更新 更多