Как я могу эффективно подсчитать диапазоны, которые охватывают данный диапазон в Perl? - PullRequest
3 голосов
/ 24 сентября 2010

У меня есть база данных из примерно 30 000 диапазонов, каждый из которых представлен в виде пары начальной и конечной точек:

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

Я хотел бы написать подпрограмму Perl этого диапазона (не из базы данных)и возвращает количество диапазонов в базе данных, которые полностью «включают» данный диапазон.

Например, если у нас было только эти 4 диапазона в базе данных, а диапазон запроса равен [38,70], подпрограмма должнаreturn 2, так как первый и третий диапазоны полностью содержат диапазон запроса.

Проблема: Я хочу сделать запросы как можно более дешевыми, я невоздержитесь от предварительной обработки, если это поможет.

Несколько замечаний:

  1. Я свободно использовал слово «база данных», я не имею в видуфактическая база данных (например, SQL);это просто длинный список диапазонов.

  2. Мой мир круговой ... Есть заданное значение 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, который похож на мои оригинальные решения, и требующий памяти один , предложенный Аристотелем Пагальцисом. Еще раз спасибо вам обоим!

Для сравнения я создал следующие пакеты, которые используют один и тот же интерфейс:

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

Поздравляем Аристотеля Пагальтзиса!Ваша реализация очень быстрая!Однако, чтобы использовать это решение, я, очевидно, хотел бы выполнить предварительную обработку (создание) объекта один раз.Могу ли я сохранить (nstore) этот объект после его создания?Я никогда не делал этого раньше.И как мне retrieve это?Что-нибудь особенное?Надеемся, что поиск будет быстрым, поэтому он не повлияет на общую производительность этой великолепной структуры данных.

ОБНОВЛЕНИЕ 3

Я попробовал простой nstore и получилдля объекта RangeMap.Кажется, это работает нормально.Единственная проблема - результирующий файл размером около 1 ГБ, и у меня будет около 1000 таких файлов.Я мог бы жить с ТБ хранилища для этого, но мне интересно, есть ли способ хранить его более эффективно без значительного снижения производительности поиска.Также смотрите здесь: http://www.perlmonks.org/?node_id=861961.

ОБНОВЛЕНИЕ 4 - RangeMap ошибка

К сожалению, RangeMap имеет ошибку.Спасибо BrowserUK от PerlMonks за указание на это.Например, создайте объект с $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 ]

2 голосов
/ 25 сентября 2010

Есть более простой способ, чем бросать свои собственные диапазоны: используйте 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, чтобы добавить эту функциональность ...: -)

2 голосов
/ 24 сентября 2010

Много ли у вас памяти?

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 голосов
/ 24 сентября 2010

Вот один подход к решению проблемы грубой силы:

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 голос
/ 25 сентября 2010

Я думаю, что подобные проблемы иллюстрируют преимущества удобства сопровождения, разбивая работу на мелкие, легко воспринимаемые фрагменты (по общему признанию, с одной ценой, составляющей больше строк кода).

Самая простая идея - идея обычного диапазона без упаковки.

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
1 голос
/ 24 сентября 2010

Уверен, есть лучший способ сделать это, но вот отправная точка:

Предварительная обработка:

  • Создайте два списка, один из которых отсортирован по начальному значению диапазонов,один за концом.

Как только вы получите свой диапазон:

  • Используйте двоичный поиск, чтобы сопоставить его начало в отсортированном по началу списка
  • Использованиедругой двоичный поиск, чтобы соответствовать его концу в отсортированном по концу списке
  • Найти диапазоны, которые появляются в обоих списках (@start [0 .. $ start_index] и @end [$ end_index .. $ # end]).
1 голос
/ 24 сентября 2010

С какой частью у вас проблемы? что ты уже испробовал? Это довольно простая задача:

  * 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 и вместо того, чтобы получить помощь, вы действительно выполняете свою работу на стороне. Это не очень хорошо. Нам вообще не платят, и особенно не платят за выполнение порученной вам работы. Это может сильно отличаться, если вы хотя бы попытались реализовать свою проблему, но многие ваши вопросы указывают на то, что вы даже не пытались.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...