Самый быстрый способ определить перекрытие диапазона в Perl - PullRequest
4 голосов
/ 13 января 2011

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

$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 до перекрытия может быть с 1 или 2 диапазонами из набора B. Мой подход перечисляет каждую отдельную возможность, которая излишество. Это приводит ко второй моей проблеме - к тому, что она очень плохо масштабируется. Код завершается очень быстро (менее минуты), когда в каждом наборе сотни диапазонов, но занимает очень много времени (+/- 30 минут), когда в каждом наборе тысячи диапазонов.

Есть ли лучший способ индексировать эти диапазоны, чтобы я не делал так много ненужных проверок на перекрытие?

Обновление : вывод, который я ищу, - это два хэша (по одному для каждого набора диапазонов), где ключи - это идентификаторы диапазонов, а значения - идентификаторы диапазонов из другого набора, которые перекрываются с заданным диапазоном в этом наборе.

Ответы [ 5 ]

10 голосов
/ 13 января 2011

Это звучит как идеальный вариант использования для дерева интервалов , которое представляет собой структуру данных, специально разработанную для поддержки этой операции.Если у вас есть два набора интервалов размеров m и n, то вы можете встроить один из них в дерево интервалов за время O (m lg m), а затем выполнить n запросов на пересечение во времени O (n lg m + k), гдеk - общее количество найденных перекрестков.Это дает чистое время выполнения O ((m + n) lg m + k).Помните, что в худшем случае k = O (нм), так что это не лучше, чем у вас, но для случаев, когда число пересечений мало, это может быть значительно лучше, чем время выполнения O (mn), которое вы имеете правосейчас.

У меня нет большого опыта работы с деревьями интервалов (и ноль опыта в Perl, извините!), но из описания кажется, что их не должно быть так сложно построить.Я был бы очень удивлен, если бы его уже не было.

Надеюсь, это поможет!

4 голосов
/ 13 января 2011

Если вы не связаны исключительно с Perl; Пакет IRanges в R имеет дело с интервальной арифметикой. У него очень мощные примитивы, и, вероятно, было бы легко написать решение для них.

Второе замечание: проблема может стать очень простой, если интервалы имеют дополнительную структуру; например, если в пределах каждого набора диапазонов нет перекрытия (в этом случае возможен линейный подход, просеивающий через два упорядоченных набора одновременно). Даже в отсутствие такой структуры, по крайней мере, вы можете отсортировать один набор диапазонов по начальной точке, а другой - по конечной точке, а затем выйти из внутреннего цикла, когда совпадение больше невозможно. Конечно, существующие и общие алгоритмы и структуры данных, такие как дерево интервалов, упомянутое ранее, являются наиболее мощными.

3 голосов
/ 30 марта 2012

Существует несколько существующих модулей CPAN, которые решают эту проблему, я разработал 2 из них: 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 голос
/ 17 сентября 2013

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

1 голос
/ 01 апреля 2011

Попробуйте Tree :: RB, но чтобы найти взаимоисключающие диапазоны, без перекрытий

Производительность неплохая, если бы у меня было около 10000 сегментов и мне нужно было найти сегмент для каждого дискретного числа.Мой вклад имел 300 миллионов записей.Я должен был положить их в отдельные ведра.Как разделение данных.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;
}
...