如何在Perl中有效地计算覆盖给定范围的范围?

时间:2010-09-24 19:08:03

标签: perl performance range

我有一个大约30k范围的数据库,每个范围都有一对起点和终点:

[12,80],[34,60],[34,9000],[76,743],...

我想编写一个范围(不是来自数据库)的Perl子例程,并返回数据库中完全“包含”给定范围的范围数。

例如,如果数据库中只有这4个范围且查询范围为[38,70],则子例程应返回2,因为第一个和第三个范围都完全包含查询范围。

问题:我希望查询尽可能“便宜”,如果有帮助的话,我不介意做很多预处理。

几点说明:

  1. 我自由地使用了“数据库”这个词,我不是指实际的数据库(例如SQL);这只是一长串的范围。

  2. 我的世界是循环的...有一个给定的max_length(例如9999),像[8541,6]这样的范围是合法的(您可以将其视为单一范围这是[8541,9999][1,6])的结合。

  3. 谢谢, 戴夫

    更新 这是我的原始代码:

    use strict;
    use warnings;
    
    my $max_length = 200;
    my @ranges     = (
        { START => 10,   END => 100 },
        { START => 30,   END => 90 },
        { START => 50, END => 80 },
        { START => 180,  END => 30 }
    );
    
    sub n_covering_ranges($) {
        my ($query_h) = shift;
        my $start     = $query_h->{START};
        my $end       = $query_h->{END};
        my $count     = 0;
        if ( $end >= $start ) {
    
            # query range is normal
            foreach my $range_h (@ranges) {
                if (( $start >= $range_h->{START} and $end <= $range_h->{END} )
                    or (    $range_h->{END} <= $range_h->{START} and  $range_h->{START} <= $end )
                    or ( $range_h->{END} <= $range_h->{START} and  $range_h->{END} >= $end)
                    )
                {
                    $count++;
                }
            }
    
        }
    
        else {
    
            # query range is hanging over edge
            # only other hanging over edges can contain it
            foreach my $range_h (@ranges) {
                if ( $start >= $range_h->{START} and $end <= $range_h->{END} ) {
                    $count++;
                }
            }
    
        }
    
        return $count;
    }
    
    print n_covering_ranges( { START => 1, END => 10 } ), "\n";
    print n_covering_ranges( { START => 30, END => 70 } ), "\n";
    

    并且,是的,我知道if很难看,可以做得更好更有效。

    更新2 - 基准建议的解决方案

    到目前为止,我已经为这两个目标解决方案做了一些基准测试:cjm建议的naive one,与我原来的解决方案类似,而且memory-demanding one,由Aristotle Pagaltzis建议再次感谢对你们俩来说!

    为了比较两者,我创建了以下使用相同接口的包:

    use strict;
    use warnings;
    
    package RangeMap;
    
    sub new {
        my $class      = shift;
        my $max_length = shift;
        my @lookup;
        for (@_) {
            my ( $start, $end ) = @$_;
            my @idx
                = $end >= $start
                ? $start .. $end
                : ( $start .. $max_length, 0 .. $end );
            for my $i (@idx) { $lookup[$i] .= pack 'L', $end }
        }
        bless \@lookup, $class;
    }
    
    sub num_ranges_containing {
        my $self = shift;
        my ( $start, $end ) = @_;
        return 0 unless defined $self->[$start];
        return 0 + grep { $end <= $_ } unpack 'L*', $self->[$start];
    }
    
    1;
    

    use strict;
    use warnings;
    
    package cjm;
    
    sub new {
        my $class      = shift;
        my $max_length = shift;
    
        my $self = {};
        bless $self, $class;
    
        $self->{MAX_LENGTH} = $max_length;
    
        my @normal  = ();
        my @wrapped = ();
    
        foreach my $r (@_) {
            if ( $r->[0] <= $r->[1] ) {
                push @normal, $r;
            }
            else {
                push @wrapped, $r;
            }
        }
    
        $self->{NORMAL}  = \@normal;
        $self->{WRAPPED} = \@wrapped;
        return $self;
    }
    
    sub num_ranges_containing {
        my $self = shift;
        my ( $start, $end ) = @_;
    
        if ( $start <= $end ) {
    
            # This is a normal range
            return ( grep { $_->[0] <= $start and $_->[1] >= $end }
                    @{ $self->{NORMAL} } )
                + ( grep { $end <= $_->[1] or $_->[0] <= $start }
                    @{ $self->{WRAPPED} } );
        }
        else {
    
            # This is a wrapped range
            return ( grep { $_->[0] <= $start and $_->[1] >= $end }
                    @{ $self->{WRAPPED} } )
    
                # This part should probably be calculated only once:
                + ( grep { $_->[0] == 1 and $_->[1] == $self->{MAX_LENGTH} }
                    @{ $self->{NORMAL} } );
        }
    }
    
    1;
    

    然后我使用了一些真实数据:$max_length=3150000,大约17000个范围,平均大小为几千,最后用10000个查询查询对象。我定时创建对象(添加所有范围)和查询。结果:

    cjm creation done in 0.0082 seconds
    cjm querying done in 21.209857 seconds
    RangeMap creation done in 45.840982 seconds
    RangeMap querying done in 0.04941 seconds
    

    祝贺Aristotle Pagaltzis!您的实施速度超快! 但是,要使用此解决方案,我显然希望对对象进行一次预处理(创建)。这个对象在创建后可以存储(nstore)吗?我以前从未这样做过。我该如何retrieve呢?有什么特别的吗?希望检索速度很快,这样就不会影响这个优秀数据结构的整体性能。

    更新3

    我尝试了一个简单的nstore并检索RangeMap对象。这似乎工作正常。唯一的问题是生成的文件大约是1GB,我将有1000个这样的文件。我可以忍受存储TB的存在,但我想知道是否有更高效的存储它而不会显着影响检索性能。另见:http://www.perlmonks.org/?node_id=861961

    更新4 - RangeMap错误

    不幸的是,RangeMap有一个错误。感谢PerlMonks的BrowserUK指出了这一点。例如,创建一个$max_lenght=10和单一范围[6,2]的对象。然后查询[7,8]。答案应该是1,而不是0

    我认为这个更新的包应该做的工作:

    use strict;
    use warnings;
    
    package FastRanges;
    
    sub new($$$) {
        my $class      = shift;
        my $max_length = shift;
        my $ranges_a   = shift;
        my @lookup;
        for ( @{$ranges_a} ) {
            my ( $start, $end ) = @$_;
            my @idx
                = $end >= $start
                ? $start .. $end
                : ( $start .. $max_length, 1 .. $end );
            for my $i (@idx) { $lookup[$i] .= pack 'L', $end }
        }
        bless \@lookup, $class;
    }
    
    sub num_ranges_containing($$$) {
        my $self = shift;
        my ( $start, $end ) = @_;    # query range coordinates
    
        return 0
            unless ( defined $self->[$start] )
            ;    # no ranges overlap the start position of the query
    
        if ( $end >= $start ) {
    
            # query range is simple
            # any inverted range in {LOOKUP}[$start] must contain it,
            # and so does any simple range which ends at or after $end
            return 0 + grep { $_ < $start or $end <= $_ } unpack 'L*',
                $self->[$start];
        }
        else {
    
            # query range is inverted
            # only inverted ranges in {LOOKUP}[$start] which also end
            # at of after $end contain it. simple ranges can't contain
            # the query range
            return 0 + grep { $_ < $start and $end <= $_ } unpack 'L*',
                $self->[$start];
        }
    }
    
    1;
    

    欢迎您的评论。

6 个答案:

答案 0 :(得分:2)

这是蛮力解决方案的一种方法:

use strict;
use warnings;

my @ranges = ([12,80],[34,60],[34,9000],[76,743]);

# Split ranges between normal & wrapped:
my (@normal, @wrapped);

foreach my $r (@ranges) {
  if ($r->[0] <= $r->[1]) {
    push @normal, $r;
  } else {
    push @wrapped, $r;
  }
}

sub count_matches
{
  my ($start, $end, $max_length, $normal, $wrapped) = @_;

  if ($start <= $end) {
    # This is a normal range
    return (grep { $_->[0] <= $start and $_->[1] >= $end } @$normal)
        +  (grep { $end <= $_->[1] or $_->[0] <= $start } @$wrapped);
  } else {
    # This is a wrapped range
    return (grep { $_->[0] <= $start and $_->[1] >= $end } @$wrapped)
        # This part should probably be calculated only once:
        +  (grep { $_->[0] == 1 and $_->[1] == $max_length } @$normal);
  }
} # end count_matches

print count_matches(38,70, 9999, \@normal, \@wrapped)."\n";

答案 1 :(得分:2)

你有很多可用的内存吗?

my $max_length = 9999;
my @range = ( [12,80],[34,60],[34,9000] );

my @lookup;

for ( @range ) {
    my ( $start, $end ) = @$_;
    my @idx = $end >= $start ? $start .. $end : ( $start .. $max_length, 0 .. $end );
    for my $i ( @idx ) { $lookup[$i] .= pack "L", $end }
}

现在,您在@lookup中有一个打包号码列表数组,其中每个索引处的打包列表包含包含该点的所有范围的末尾。因此,要检查有多少范围包含另一个范围,请在数组中查找其起始索引,然后计算该索引处的打包列表中的条目数,这些条目小于或等于结束索引。该算法是 O(n),相对于任何一个点的最大范围数(限制是范围的总数),每次迭代的开销非常小。

sub num_ranges_containing {
    my ( $start, $end ) = @_;

    return 0 unless defined $lookup[$start];

    # simple ranges can be contained in inverted ranges,
    # but inverted ranges can only be contained in inverted ranges
    my $counter = ( $start <= $end )
        ? sub { 0 + grep { $_ < $start or  $end <= $_ } }
        : sub { 0 + grep { $_ < $start and $end <= $_ } };

    return $counter->( unpack 'L*', $lookup[$start] );
}

未测试。

为了更加整洁,

package RangeMap;

sub new {
    my $class = shift;
    my $max_length = shift;
    my @lookup;
    for ( @_ ) {
        my ( $start, $end ) = @$_;
        my @idx = $end >= $start ? $start .. $end : ( $start .. $max_length, 0 .. $end );
        for my $i ( @idx ) { $lookup[$i] .= pack 'L', $end }
    }
    bless \@lookup, $class;
}

sub num_ranges_containing {
    my $self = shift;
    my ( $start, $end ) = @_;

    return 0 unless defined $self->[$start];

    # simple ranges can be contained in inverted ranges,
    # but inverted ranges can only be contained in inverted ranges
    my $counter = ( $start <= $end )
        ? sub { 0 + grep { $_ < $start or  $end <= $_ } }
        : sub { 0 + grep { $_ < $start and $end <= $_ } };

    return $counter->( unpack 'L*', $self->[$start] );
}

package main;
my $rm = RangeMap->new( 9999, [12,80],[34,60],[34,9000] );

这样你可以有任意数量的范围。

也未经测试。

答案 2 :(得分:2)

比滚动自己的范围更简单:使用Number::Interval

my @ranges     = (
    { START => 10,   END => 100 },
    { START => 30,   END => 90 },
    { START => 50, END => 80 },
    { START => 180,  END => 30 }
);
my @intervals;
for my $range ( @ranges ) {
  my $int = new Number::Interval( Min => $range->{START},
                                  Max => $range->{END} );
  push @intervals, $int;
}

然后,您可以使用intersection()方法查明两个范围是否重叠:

my $num_overlap = 0;
my $checkinterval = new Number::Interval( Min => $min, Max => $max );
for my $int ( @intervals ) {
  $num_overlap++ if $checkinterval->intersection( $int );
}

我不太确定它会对你的“循环”范围做什么(它们被Number::Interval归类为“倒置”间隔)所以你必须做一些实验。

但是使用模块真的比你自己的范围比较方法更好。

编辑:实际上,仔细查看文档,intersection()将无法执行您想要的操作(事实上,它会修改其中一个间隔对象)。您可能希望在开始和结束值上使用contains(),如果这两个值都包含在另一个时间间隔内,则第一个时间间隔包含在第二个时间间隔内。

当然,您可以更新Number::Interval以添加此功能......: - )

答案 3 :(得分:1)

你有哪些问题?你都尝试了些什么?这是一项相当简单的任务:

  * Iterate through the ranges
  * Foreach range, check if the test range is in it.
  * Profile and benchmark

这是相当简单的Perl:

 my $test = [ $n, $m ];
 my @contains = map { 
      $test->[0] >= $_->[0] 
         and 
      $test->[1] <= $_->[1]
      } @ranges

对于环绕范围,诀窍是在查看它们之前将它们分解为单独的范围。这是蛮力的工作。

而且,就像社交报告一样,你提出的问题的比率非常高:高于我对真正试图解决自己问题的人的期望。我认为你过快地运行Stackoverflow而不是获得帮助,你真的是在外包你的工作。那不是那么好。我们根本没有得到报酬,特别是没有报酬分配给你的工作。如果您至少尝试过对问题的实施,这可能会有很大的不同,但很多问题似乎表明您甚至没有尝试过。

答案 4 :(得分:1)

非常确定有更好的方法可以做到这一点,但这是一个起点:

预处理:

  • 创建两个列表,一个按范围的起始值排序,一个按结尾排序。

一旦你得到你的范围:

  • 使用二进制搜索来匹配起始排序列表中的开始
  • 使用另一个二进制搜索来匹配最终排序列表中的结尾
  • 查找两个列表中显示的范围(@start [0 .. $ start_index]和@end [$ end_index .. $#end])。

答案 5 :(得分:1)

我认为这样的问题说明了将工作分解成小的,容易掌握的部分的可维护性好处(诚然,一个代价是更多的代码行)。

最简单的想法是普通的非包装范围。

package SimpleRange;

sub new {
    my $class = shift;
    my ($m, $n) = @_;
    bless { start => $m, end => $n }, $class;
}

sub start { shift->{start} }
sub end   { shift->{end}   }

sub covers {
    # Returns true if the range covers some other range.
    my ($self, $other) = @_;
    return 1 if $self->start <= $other->start
            and $self->end   >= $other->end;
    return;
}

使用该构建块,我们可以创建包裹范围类,它包含1或2个简单范围(如果范围环绕宇宙边缘,则为2)。与简单范围的类一样,此类定义covers方法。该方法中的逻辑非常直观,因为我们可以使用covers对象提供的SimpleRange方法。

package WrappingRange;

sub new {
    my $class = shift;
    my ($raw_range, $MIN, $MAX) = @_;
    my ($m, $n) = @$raw_range;

    # Handle special case: a range that wraps all the way around.
    ($m, $n) = ($MIN, $MAX) if $m == $n + 1;

    my $self = {min => $MIN, max => $MAX};
    if ($m <= $n){
        $self->{top}  = SimpleRange->new($m, $n);
        $self->{wrap} = undef;
    }
    else {
        $self->{top}  = SimpleRange->new($m, $MAX);
        $self->{wrap} = SimpleRange->new($MIN, $n);    
    }
    bless $self, $class;
}

sub top  { shift->{top}  }
sub wrap { shift->{wrap} }
sub is_simple { ! shift->{wrap} }

sub simple_ranges {
    my $self = shift;
    return $self->is_simple ? $self->top : ($self->top, $self->wrap);
}

sub covers {
    my @selfR  = shift->simple_ranges;
    my @otherR = shift->simple_ranges;
    while (@selfR and @otherR){
        if ( $selfR[0]->covers($otherR[0]) ){
            shift @otherR;
        }
        else {
            shift @selfR;
        }
    }
    return if @otherR;
    return 1;
}

运行一些测试:

package main;
main();

sub main {
    my ($MIN, $MAX) = (0, 200);

    my @raw_ranges = (
        [10, 100], [30, 90], [50, 80], [$MIN, $MAX],
        [180, 30], 
        [$MAX, $MAX - 1], [$MAX, $MAX - 2],
        [50, 49], [50, 48],
    );
    my @wrapping_ranges = map WrappingRange->new($_, $MIN, $MAX), @raw_ranges;

    my @tests = ( [1, 10], [30, 70], [160, 10], [190, 5] );
    for my $t (@tests){
        $t = WrappingRange->new($t, $MIN, $MAX);

        my @covers = map $_->covers($t) ? 1 : 0, @wrapping_ranges;

        my $n;
        $n += $_ for @covers;
        print "@covers  N=$n\n";
    }
}

输出:

0 0 0 1 1 1 1 1 1  N=6
1 1 0 1 0 1 1 1 0  N=6
0 0 0 1 0 1 0 1 1  N=4
0 0 0 1 1 1 0 1 1  N=5