我有一个大约30k范围的数据库,每个范围都有一对起点和终点:
[12,80],[34,60],[34,9000],[76,743],...
我想编写一个范围(不是来自数据库)的Perl子例程,并返回数据库中完全“包含”给定范围的范围数。
例如,如果数据库中只有这4个范围且查询范围为[38,70]
,则子例程应返回2
,因为第一个和第三个范围都完全包含查询范围。
问题:我希望查询尽可能“便宜”,如果有帮助的话,我不介意做很多预处理。
几点说明:
我自由地使用了“数据库”这个词,我不是指实际的数据库(例如SQL);这只是一长串的范围。
我的世界是循环的...有一个给定的max_length
(例如9999
),像[8541,6]
这样的范围是合法的(您可以将其视为单一范围这是[8541,9999]
和[1,6]
)的结合。
谢谢, 戴夫
更新 这是我的原始代码:
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;
欢迎您的评论。
答案 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)
非常确定有更好的方法可以做到这一点,但这是一个起点:
预处理:
一旦你得到你的范围:
答案 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