在Perl中确定范围重叠的最快方法

4

我有两组范围。每个范围都是表示某个单独的较大范围的子范围的一对整数(起始和结束)。两组范围的结构类似于这样(当然,...s将被实际数字替换)。

$a_ranges =
{
  a_1 =>
  {
    start => ...,
    end   => ...,
  },
  a_2 =>
  {
    start => ...,
    end   => ...,
  },
  a_3 =>
  {
    start => ...,
    end   => ...,
  },
  # and so on
};

$b_ranges =
{
  b_1 =>
  {
    start => ...,
    end   => ...,
  },
  b_2 =>
  {
    start => ...,
    end   => ...,
  },
  b_3 =>
  {
    start => ...,
    end   => ...,
  },
  # and so on
};

我需要确定集合A中的哪些范围与集合B中的哪些范围重叠。给定两个范围,确定它们是否重叠很容易。我一直在使用双重循环来做到这一点——在外部循环中遍历集合A中的所有元素,在内部循环中遍历集合B中的所有元素,并跟踪哪些元素重叠。
我使用这种方法有两个问题。首先,重叠空间非常稀疏——即使每个集合中都有成千上万个范围,我希望集合A中的每个范围只与集合B中的1或2个范围重叠。我的方法枚举了每一个可能性,这是过度的。这导致我的第二个问题——它的可扩展性非常差。当每个集合中有数千个范围时,代码完成得非常快(不到一分钟),但需要很长时间(+/- 30分钟)。
有没有更好的方法可以索引这些范围,以便我不必进行太多不必要的重叠检查?
更新:我需要的输出是两个哈希表(每个范围集合一个),其中键是范围ID,值是与该集合中给定范围重叠的来自另一个集合的范围的ID。

你能确认一下你需要什么输出吗?“哪些范围与哪些范围重叠”有点模糊,我理解成你想要一个(a_i, b_j)对的列表,但你可能指的是一个区间列表--(start, end)对。 - JB.
@JB 感谢您提出这个问题,我已经更新了问题。 - Daniel Standage
5个回答

10

这似乎是使用区间树的完美案例,它是一种专门设计用于支持此操作的数据结构。如果您有两个大小分别为 m 和 n 的区间集合,则可以在时间 O(m lg m) 内构建其中一个区间树,然后在时间 O(n lg m + k) 内执行 n 次交集查询,其中 k 是您找到的所有交集的总数。这给出了净运行时间O((m + n) lg m + k)。请注意,在最坏情况下,k = O(nm),因此这并不比您现在拥有的O(mn)更好,但对于交集数量稀疏的情况,这可能比现有方法快得多。

我没有太多使用区间树的经验(也没有Perl经验,很抱歉!),但从描述来看,它们应该不难建立。如果还不存在一个这样的东西,我会很惊讶的。

希望这能帮到您!


感谢@templatetypedef提供的想法和@daxim提供的链接。这太完美了。搜索时间从10-30分钟(取决于数据集)降至亚秒级! - Daniel Standage

4
如果您不仅仅使用perl,那么R中的IRanges包可以处理区间算术。它具有非常强大的基元,很容易使用它们编写解决方案。
第二点是,如果区间具有附加结构,则问题可能变得非常简单;例如,如果在每组范围内没有重叠(在这种情况下,可以通过同时筛选两个有序集合的线性方法来解决)。即使缺乏此类结构,最少也要按起始点对一个范围集进行排序,按结束点对另一个集合进行排序,然后在匹配不再可能时跳出内部循环。当然,现有的通用算法和数据结构,如前面提到的区间树,是最强大的。

3
有几个已有的CPAN模块可以解决这个问题,我开发了其中两个:Data::Range::Compare和Data::Range::Compare::Stream。
Data::Range::Compare仅适用于内存中的数组,但支持通用范围类型。
Data::Range::Compare::Stream通过迭代器处理数据流,但需要理解OO Perl才能扩展到通用数据类型。如果您正在处理非常大的数据集,则建议使用Data::Range::Compare::Stream。
以下是Data::Range::Compare::Stream示例文件夹的摘录。
给定这3组数据:
Numeric Range set: A contained in file: source_a.src
+----------+
| 1 - 11   |
| 13 - 44  |
| 17 - 23  |
| 55 - 66  |
+----------+

Numeric Range set: B contained in file: source_b.src
+----------+
| 0  - 1   |
| 2  - 29  |
| 88 - 133 |
+----------+

Numeric Range set: C contained in file: source_c.src
+-----------+
| 17  - 29  |
| 220 - 240 |
| 241 - 250 |
+-----------+

预期的输出应该是:
+--------------------------------------------------------------------+
| Common Range | Numeric Range A | Numeric Range B | Numeric Range C |
+--------------------------------------------------------------------+
| 0 - 0        |   No Data       |   0 - 1         |   No Data       |
| 1 - 1        |   1 - 11        |   0 - 1         |   No Data       |
| 2 - 11       |   1 - 11        |   2 - 29        |   No Data       |
| 12 - 12      |   No Data       |   2 - 29        |   No Data       |
| 13 - 16      |   13 - 44       |   2 - 29        |   No Data       |
| 17 - 29      |   13 - 44       |   2 - 29        |   17 - 29       |
| 30 - 44      |   13 - 44       |   No Data       |   No Data       |
| 55 - 66      |   55 - 66       |   No Data       |   No Data       |
| 88 - 133     |   No Data       |   88 - 133      |   No Data       |
| 220 - 240    |   No Data       |   No Data       |   220 - 240     |
| 241 - 250    |   No Data       |   No Data       |   241 - 250     |
+--------------------------------------------------------------------+

源代码可以在这里找到。
#!/usr/bin/perl

use strict;
use warnings;
use Data::Dumper;
use lib qw(./ ../lib);

# custom package from FILE_EXAMPLE.pl
use Data::Range::Compare::Stream::Iterator::File;


use Data::Range::Compare::Stream;
use Data::Range::Compare::Stream::Iterator::Consolidate;
use Data::Range::Compare::Stream::Iterator::Compare::Asc;

my $source_a=Data::Range::Compare::Stream::Iterator::File->new(filename=>'source_a.src');
my $source_b=Data::Range::Compare::Stream::Iterator::File->new(filename=>'source_b.src');
my $source_c=Data::Range::Compare::Stream::Iterator::File->new(filename=>'source_c.src');

my $consolidator_a=new Data::Range::Compare::Stream::Iterator::Consolidate($source_a);
my $consolidator_b=new Data::Range::Compare::Stream::Iterator::Consolidate($source_b);
my $consolidator_c=new Data::Range::Compare::Stream::Iterator::Consolidate($source_c);


my $compare=new  Data::Range::Compare::Stream::Iterator::Compare::Asc();

my $src_id_a=$compare->add_consolidator($consolidator_a);
my $src_id_b=$compare->add_consolidator($consolidator_b);
my $src_id_c=$compare->add_consolidator($consolidator_c);



print "  +--------------------------------------------------------------------+
  | Common Range | Numeric Range A | Numeric Range B | Numeric Range C |
  +--------------------------------------------------------------------+\n";

my $format='  | %-12s |   %-13s |   %-13s |   %-13s |'."\n";
while($compare->has_next) {
  my $result=$compare->get_next;
  my $string=$result->to_string;
  my @data=($result->get_common);
  next if $result->is_empty;
  for(0 .. 2) {
     my $column=$result->get_column_by_id($_);
     unless(defined($column)) {
      $column="No Data";
    } else {
      $column=$column->get_common->to_string;
    }
     push @data,$column;


   }
  printf $format,@data;

  } 
 print "  +--------------------------------------------------------------------+\n";

1

尝试使用Tree::RB,但要找到互斥的范围,没有重叠。

性能还不错,如果我有大约10000个段,并且必须为每个离散数字找到段。我的输入有3亿条记录。我需要将它们放入单独的桶中。就像对数据进行分区一样。Tree::RB效果很好。

$var = [
[0,90],
[91,2930],
[2950,8293]
.
.
.
]

我的查找值是10、99、991等...

基本上,我需要给定数字的范围位置。

使用以下比较函数,我的函数使用类似于此:

my $cmp = sub
{
    my ($a1, $b1) = @_;

    if(ref($b1) && ref($a1))
    {
        return ($$a1[1]) <=> ($$b1[0]);
    }

    my $ret = 0;

    if(ref($a1) eq 'ARRAY')
    {
        #
        if($$a1[0] <= $b1 && $b1 >= $$a1[1])
        {
            $ret =  0;
        }
        if($$a1[0] < $b1)
        {
            $ret =  -1;
        }
        if($$a1[1] > $b1)
        {
            $ret =  1;
        }
    }
    else
    {
        if($$b1[0] <= $a1 && $a1 >= $$b1[1])
        {
            $ret =  0;
        }
        if($$b1[0] > $a1)
        {
            $ret =  -1;
        }
        if($$b1[1] < $a1)
        {
            $ret =  1;
        }
    }

    return $ret;
}

1
我应该检查时间以了解是否是最快的方法,但根据您的数据结构,您应该尝试这样做:
use strict;

my $fromA = 12;
my $toA = 15;
my $fromB = 7;
my $toB = 35;

my @common_range = get_common_range($fromA, $toA, $fromB, $toB);
my $common_range = $common_range[0]."-".$common_range[-1];

sub get_common_range {
  my @A = $_[0]..$_[1];
  my %B = map {$_ => 1} $_[2]..$_[3];
  my @common = ();

  foreach my $i (@A) {
    if (defined $B{$i}) {
      push (@common, $i);
    } 
  }
  return sort {$a <=> $b} @common;
}

这段代码在解决一个问题中非常有用,该问题在http://stackoverflow.com/questions/28786941/perl-conditional-range-expansion中提到。 - rajeev

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