具有重复的区间数组的并集中的第n个最小元素

时间:2012-11-22 07:43:29

标签: algorithm methods

我想知道是否有比我提出的更有效的解决方案(尚未编码,但在底部描述了它的要点)。

写一个函数calcNthSmallest(n,interval),它将非负int n作为输入,以及一个区间列表[[a_1; B_1]; ::: ;; [上午; b_m]]并在重复所有区间的并集时计算第n个最小数(0索引)。例如,如果间隔是[1; 5]; [2; 4]; [7; 9],他们与重复的结合将是[1; 2; 2; 3; 3; 4; 4; 5; 7; 8; 9](注2; 3; 4每次出现两次,因为它们都在[1; 5]和[2; 4]的区间内)。对于此间隔列表,第0个最小数字将为1,第3个和第4个最小数字都为3.即使a_i,您的实现也应该快速运行。 b_i可能非常大(例如,一万亿),并且有几个间隔

我想到的方法就是制作联合数组并遍历它的简单解决方案。

5 个答案:

答案 0 :(得分:4)

这个问题可以在O(N log N)中解决,其中N是列表中的间隔数,而不管间隔端点的实际值。

有效解决这个问题的关键是将可能重叠的区间列表转换为不相交或相同的区间列表。在给定的示例中,只需要拆分第一个间隔:

{       [1,5],        [2,4], [7,9]} =>
 +-----------------+  +---+  +---+
{[1,1], [2,4], [5,5], [2,4], [7,9]}

(但这不必明确地完成:见下文。)现在,我们可以对新的时间间隔进行排序,用计数替换重复项。由此,我们可以计算每个(可能重复的)间隔所代表的值的数量。现在,我们只需要累积值来确定解决方案所处的区间:

interval  count  size    values     cumulative
                       in interval    values
  [1,1]     1      1        1         [0, 1)
  [2,4]     2      3        6         [1, 7)  (eg. from n=1 to n=6 will be here)
  [5,5]     1      1        1         [7, 8)
  [7,9]     1      3        3         [8, 11)

我将累积值写成半开区间列表,但显然我们只需要终点。然后我们可以通过二进制搜索累积值列表来找到哪个区间保持值n,并且我们可以通过从n减去区间的开始然后整数除以得出我们想要的区间中的哪个值。计数。

应该清楚的是,上表的最大大小是原始区间数的两倍,因为每一行必须在原始列表中某个区间的开始或结束处开始和结束。如果我们把间隔写成半开而不是闭合,这将更加清晰;在这种情况下,我们可以断言表的精确大小将是端点集合中唯一值的数量。从这种洞察力来看,我们可以看到我们根本不需要这张桌子;我们只需要排序的端点列表(尽管我们需要知道每个值代表哪个端点)。我们可以简单地遍历该列表,保持活动间隔数的计数,直到达到我们正在寻找的值。

这是一个快速的python实现。它可以改进。

def combineIntervals(intervals):
  # endpoints will map each endpoint to a count
  endpoints = {}
  # These two lists represent the start and (1+end) of each interval
  # Each start adds 1 to the count, and each limit subtracts 1
  for start in (i[0] for i in intervals):
    endpoints[start] = endpoints.setdefault(start, 0) + 1
  for limit in (i[1]+1 for i in intervals):
    endpoints[limit] = endpoints.setdefault(limit, 0) - 1
  # Filtering is a possibly premature optimization but it was easy
  return sorted(filter(lambda kv: kv[1] != 0,
                       endpoints.iteritems()))

def nthSmallestInIntervalList(n, intervals):
  limits = combineIntervals(intervals)
  cumulative = 0
  count = 0
  index = 0
  here = limits[0][0]
  while index < len(limits):
    size = limits[index][0] - here
    if n < cumulative + count * size:
      # [here, next) contains the value we're searching for
      return here + (n - cumulative) / count
    # advance
    cumulative += count * size
    count += limits[index][1]
    here += size
    index += 1
  # We didn't find it. We could throw an error

因此,正如我所说,该算法的运行时间与间隔的实际值无关;它只取决于间隔列表的长度。由于排序成本(O(N log N)),此特定解决方案为combineIntervals;如果我们使用优先级队列而不是完整排序,我们可以在O(N)中构建堆,但为每个扫描的端点进行扫描O(log N)。除非N非常大并且参数n的预期值相对较小,否则这会适得其反。但是,可能还有其他方法可以降低复杂性。

答案 1 :(得分:2)

EDIT2:

这是您的另一个问题。

让我们以图形方式考虑间隔:

             1  1   1 2  2  2  3
   0-2-4--7--0--3---7-0--4--7--0
     [-------]
       [-----------------]
          [---------]
                [--------------]
                      [-----]

当在下限按升序排序时,我们可以得到类似于上面的区间列表([2;10];[4;24];[7;17];[13;30];[20;27])的内容。每个下限表示新间隔的开始,并且还标记另一个&#34;级别的开始&#34;重复的数字。相反,上限标记该级别的结束,并减少一级的重复级别。 因此,我们可以将上述内容转换为以下列表:

   [2;+];[4;+];[7;+][10;-];[13;+];[17;-][20;+];[24;-];[27;-];[30;-]

其中第一个值表示边界的等级,第二个值表示边界是低(+)还是上(-)。第n个元素的计算是通过遵循列表,在遇到下限或上限时提高或降低复制级别,并使用复制级别作为计数因子来完成的。

让我们再次以图形方式考虑列表,但作为直方图:

          3333  44444 5555
       2222222333333344444555
     111111111222222222222444444
             1  1   1 2  2  2  3
   0-2-4--7--0--3---7-0--4--7--0

上面的视图与第一个视图相同,所有间隔都是垂直打包的。 1是第一个的元素,2是第二个,等等。事实上,重要的是这里 是每个索引的高度,对应于每个索引在所有间隔的并集中重复的时间。

          3333  55555 7777
       2223333445555567777888
     112223333445555567777888999
             1  1   1 2  2  2  3
   0-2-4--7--0--3---7-0--4--7--0
   | | |  |   | |    ||   |  |

我们可以看到直方图块从间隔的下限开始,并且在上限或者下限之前的一个单位结束,因此必须相应地修改新的表示法。

使用包含 n 区间的列表,作为第一步,我们将列表转换为上面的符号( O(n)),并在增加范围内对其进行排序订单( O(nlog(n)))。计算数字的第二步是在 O(n)中, O(nlog(n))中的总平均时间。

这是OCaml中的一个简单实现,使用1-1代替&#39; +&#39;和&#39; - &#39;。

(* transform the list in the correct notation *)
let rec convert = function
      [] -> []
    | (l,u)::xs -> (l,1)::(u+1,-1)::convert xs;;

(* the counting function *)
let rec count r f = function
      [] -> raise Not_found
    | [a,x] -> (match f + x with 
          0 -> if r = 0 then a else raise Not_found
                    | _ -> a + (r / f))
    | (a,x)::(b,y)::l ->
         if a = b
         then count r f ((b,x+y)::l)
         else
             let f = f + x in
             if f > 0 then
                 let range = (b - a) * f in
                 if range > r
                 then a + (r / f)
                 else count (r - range) f ((b,y)::l)
             else count r f ((b,y)::l);;

(* the compute function *)
let compute l = 
    let compare (x,_) (y,_) = compare x y in
    let l = List.sort compare (convert l) in
    fun m -> count m 0 l;;

注意: - 如果所寻求的数字高于间隔,则上述函数将引发异常。下面的其他方法不考虑这个角落的情况。 - OCaml中使用的列表排序功能是合并排序,有效地在 O(nlog(n))中执行。


编辑:

看到你可能有很大的间隔,我最初给出的解决方案(见下文)远非最佳。 相反,我们可以通过转换列表来加快速度: 我们尝试通过搜索重叠的间隔列表来压缩间隔列表,并通过前缀间隔,重叠间隔的几倍和后缀间隔来替换它们。然后,我们可以直接计算列表中每个元素所涵盖的条目数。 看看上面的分裂(前缀,中缀,后缀),我们看到进行处理的最佳结构是二叉树。该树的节点可以可选地具有前缀和后缀。因此节点必须包含:

  • 节点
  • 中的间隔i
  • 一个整数,给出列表中i的重复次数
  • i
  • 以下所有区间的左子树
  • i
  • 以上所有区间的右子树

使用此结构,树将自动排序。 这是一个体现该树的ocaml类型的例子。

type tree = Empty | Node of int * interval * tree * tree

现在转换算法归结为构建树。

此函数从其组件中创建一个树:

let cons k r lt rt = 
   the tree made of count k, interval r, left tree lt and right tree rt

此函数以递归方式在树中插入间隔。

let rec insert i it =
   let r = root of it
   let lt = the left subtree of it
   let rt = the right subtree of it
   let k = the count of r
   let prf, inf, suf = the prefix, infix and suffix of i according to r
   return cons (k+1) inf (insert prf lt) (insert suf rt)

构建树后,我们对树进行预先遍历,使用节点的计数来加速第n个元素的计算。


以下是我之前的回答。

以下是我的解决方案的步骤:

  • 您需要在每个间隔的下限按升序排序间隔列表
  • 你需要一个双端队列dq(或在某个时候可以反转的列表)来存储间隔

这里是代码:

let lower i = lower bound of interval i
let upper i = upper bound of i

let il = sort of interval list
i <- 0
j <- lower (head of il)
loop on il:
  i <- i + 1
  let h = the head of il
  let il = the tail of il
  if upper h > j then push h to dq
  if lower h > j then
            il <- concat dq and il
            j <- j + 1
            dq <- empty
            loop
  if i = k then return j
  loop

该算法通过简单地迭代间隔,仅考虑相关间隔,并计算联合中元素的等级i和该元素的值j来工作。达到目标排名k后,将返回该值。

复杂性大致为O(k)+ O(sort(l))。

答案 2 :(得分:1)

如果我正确理解了你的问题,你想找到间隔列表联合的第k个最大元素。 如果我们假设列表中没有= 2则问题是: 找到两个排序数组联合的第k个最小元素(其中区间[2,5]只是2到5个元素{2,3,4,5})这个溶剂可以用来解决(n + m)log(n + m)时间(n和m是列表的大小)。其中i和j是列表迭代器。

Maintaining the invariant
    i + j = k – 1,
If Bj-1 < Ai < Bj, then Ai must be the k-th smallest,
or else if Ai-1 < Bj < Ai, then Bj must be the k-th smallest.

详细信息click here

现在的问题是,如果你没有列表= 3列表,那么

 Maintaining the invariant
        i + j+ x = k – 1,
         i + j=k-x-1
     The value k-x-1 can take y (size of third list, because x iterates from start point of list to end point) .
    problem of 3 lists size can be reduced to y*(problem of size 2 list). So complexity is `y*((n+m)log(n+m))`

    If Bj-1 < Ai < Bj, then Ai must be the k-th smallest,
    or else if Ai-1 < Bj < Ai, then Bj must be the k-th smallest.

因此对于大小n列表的问题,复杂性是NP。

但是,如果我们知道k

答案 3 :(得分:0)

让我用一个例子来解释一下: 假设给出这些区间[5,12],[3,9],[8,13]。 这些区间的结合是:

number : 3 4 5 5 6 6 7 7 8  8  8  9  9  9 10 10 11 11 12 12 13.
indices: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21

当9传递输入时,lowest将返回11 当9传递输入时,highest将返回14。

最低和最高功能只是检查x是否存在于该间隔中,如果存在,则将x-a(间隔的较低索引)添加到该特定间隔的返回值。如果间隔完全小于x,则将该间隔中的元素总数添加到返回值。

当13通过时,find函数将返回9 find函数将使用二进制搜索的概念来查找第k个最小元素。在给定范围[0,N]中(如果未给出范围,我们可以在O(n)中找到高范围)找到mid并计算中间的最低和最高。如果给定的k落在最低和最高回报中间,如果k小于或等于下半部分中的最低搜索(0,mid-1),则在上半部分搜索(mid + 1,high)。
如果间隔数为n且范围为N,则该算法的运行时间为n * log(N)。我们将找到最低和最高(在O(n)中运行)log(N)次。

//Function call will be `find(0,N,k,in)`

//Retrieves the no.of smaller elements than first x(excluding) in union
public static int lowest(List<List<Integer>> in, int x){
    int sum = 0;
    for(List<Integer> lst: in){
        if(x > lst.get(1)) 
            sum += lst.get(1) - lst.get(0)+1;
        else if((x >= lst.get(0) && x<lst.get(1)) || (x > lst.get(0) && x<=lst.get(1))){
                sum += x - lst.get(0);

         }
        }

    return sum;
}
//Retrieve the no.of smaller elements than last x(including) in union.
public static int highest(List<List<Integer>> in, int x){
    int sum = 0;
    for(List<Integer> lst: in){
        if(x > lst.get(1)) 
            sum += lst.get(1) - lst.get(0)+1;
        else if((x >= lst.get(0) && x<lst.get(1)) || (x > lst.get(0) && x<=lst.get(1))){
                sum += x - lst.get(0)+1;

        }
        }
    return sum;
}

//Do binary search on the range.
public static int find(int low, int high, int k,List<List<Integer>> in){
    if(low > high)
        return -1;
    int mid = low + (high-low)/2;
    int lowIdx = lowest(in,mid);
    int highIdx = highest(in,mid);
    //k lies between the current numbers high and low indices
    if(k > lowIdx && k <= highIdx) return mid;
    //k less than lower index. go on to left side
    if(k <= lowIdx) return find(low,mid-1,k,in);
    // k greater than higher index go to right
    if(k > highIdx) return find(mid+1,high,k,in);
    else
        return -1; // catch statement
}

答案 4 :(得分:0)

可以计算列表中的数量小于某个选定的数字X(通过遍历所有间隔)。现在,如果此数字大于n,则解决方案肯定小于X.同样,如果此数字小于或等于n,则解决方案大于或等于X.基于这些观察,我们可以使用二进制搜索。

以下是Java实现:

public int nthElement( int[] lowerBound, int[] upperBound, int n )
   {
      int lo = Integer.MIN_VALUE, hi = Integer.MAX_VALUE;
      while ( lo < hi ) {
         int X = (int)( ((long)lo+hi+1)/2 );
         long count = 0;
         for ( int i=0; i<lowerBound.length; ++i ) {
            if ( X >= lowerBound[i] && X <= upperBound[i] ) {
               // part of interval i is less than X
               count += (long)X - lowerBound[i];
            }
            if ( X >= lowerBound[i] && X > upperBound[i] ) {
               // all numbers in interval i are less than X
               count += (long)upperBound[i] - lowerBound[i] + 1;
            }
         }

         if ( count <= n ) lo = X;
         else hi = X-1;
      }

      return lo;
   }