Пошук цілих послідовностей


14

У мене досить складна проблема пошуку, яку мені вдалося звести до наступного опису. Я гуглився, але не зміг знайти алгоритм, який, здається, відповідає моїй проблемі. Зокрема, потрібно пропустити довільні цілі числа. Можливо, хтось тут може мене на щось вказати?

Візьмемо, наприклад, послідовність цілих чисел A (1 2 3 4)

Візьміть різні послідовності цілих чисел і протестуйте, чи будь-яке з них відповідає A такому, що.

  1. A містить усі цілі числа в тестованій послідовності
  2. Порядок введення цілих чисел у тестованій послідовності однаковий у A
  3. Нам не байдуже будь-які цілі числа A, які не входять у тестову послідовність
  4. Ми хочемо, щоб усі тестові послідовності не були першими.

Приклад

A = (1 2 3 4)
B = (1 3)
C = (1 3 4)
D = (3 1)
E = (1 2 5)

B відповідає А

C відповідає A

D не відповідає A, оскільки впорядкування відрізняється

E не відповідає A, оскільки містить ціле число, не в A

Я сподіваюся, що це пояснення досить зрозуміле. Найкраще, що мені вдалося зробити, - це скласти дерево тестових послідовностей і повторити значення А. Необхідність мати можливість пропускати цілі числа призводить до безлічі невдалих пошукових шляхів.

Спасибі

Читаючи кілька пропозицій, я вважаю, що мені потрібно уточнити пару пунктів, які я залишив занадто розпливчастими.

  1. Допускається повторне число, насправді це досить важливо, оскільки це дозволяє одній послідовності тестування збігатися з A - це декілька способів

    A = (1234356), B = (236), відповідність може бути або -23 --- 6, або -2--3-6

  2. Я очікую, що буде дуже велика кількість тестових послідовностей, принаймні, у тисячах, а послідовність A має тенденцію до максимальної довжини, можливо, 20. Таким чином, просто намагання зіставити кожну тестову послідовність по черзі шляхом повторення стає вкрай неефективною.

Вибачте, якщо це було не ясно.


4
Ви звучаєте так, ніби просто хочете виявити наступності ( en.wikipedia.org/wiki/Subsequence ). Є те, що його? Потім спробуйте знайти "алгоритм підпорядкування".
Кіліан Фот

Чесно кажучи, кілька тисяч послідовностей з максимальною довжиною <= 20 для мене не звучать великою кількістю. Простий підхід грубої сили повинен зробити свою справу. Або у вас є тисячі послідовностей "А", кожна з яких перевіряється на тисячі можливих послідовностей?
Док Браун

Існує постійний потік послідовностей А, але вони повністю незалежні одна від одної. Однак затримка обробки одночасно затримує всіх інших, тому важлива швидкість.
Девід Гібсон

1
Наскільки великий ваш алфавіт? Чи справді у вас є довільні цілі числа, чи існує обмежений діапазон значень, таким чином, щоб ми могли зробити попередній розрахунок?
Френк

Можливий діапазон цілих чисел у 100 000
Девід Гібсон

Відповіді:


18

Хм, я можу придумати два можливі алгоритми: лінійне сканування через послідовність A , або побудова словника з постійним пошуком індексів у постійному часі.

Якщо ви протестуєте багато потенційних підрядів B на одну більшу послідовність A , я б запропонував використовувати варіант зі словником.

Лінійне сканування

Опис

Ми підтримуємо курсор для послідовності A . Потім ми перебираємо всі елементи в підпослідовності B . Для кожного елемента ми переміщуємо курсор в A, поки не знайдемо відповідний елемент. Якщо не було знайдено відповідного елемента, то B - це не підпорядкованість.

Це завжди працює в O (seq.size) .

Псевдокод

Загальний стиль:

def subsequence? seq, subseq:
  i = 0
  for item in subseq:
    i++ while i < seq.size and item != seq[i]
    return false if i == seq.size
  return true

Функціональний стиль:

let rec subsequence? = function
| _ [] -> true
| [] _ -> false
| cursor::seq item::subseq ->
  if   cursor = item
  then subsequence? seq subseq
  else subsequence? seq item::subseq

Приклад реалізації (Perl):

use strict; use warnings; use signatures; use Test::More;

sub is_subsequence_i ($seq, $subseq) {
  my $i = 0;
  for my $item (@$subseq) {
    $i++ while $i < @$seq and $item != $seq->[$i];
    return 0 if $i == @$seq;
  }
  return 1;
}

sub is_subsequence_f ($seq, $subseq) {
  return 1 if @$subseq == 0;
  return 0 if @$seq == 0;
  my ($cursor, @seq) = @$seq;
  my ($item, @subseq) = @$subseq;
  return is_subsequence_f(\@seq, $cursor == $item ? \@subseq : $subseq);
}

my $A = [1, 2, 3, 4];
my $B = [1, 3];
my $C = [1, 3, 4];
my $D = [3, 1];
my $E = [1, 2, 5];

for my $is_subsequence (\&is_subsequence_i, \&is_subsequence_f) {
  ok $is_subsequence->($A, $B), 'B in A';
  ok $is_subsequence->($A, $C), 'C in A';
  ok ! $is_subsequence->($A, $D), 'D not in A';
  ok ! $is_subsequence->($A, $E), 'E not in A';
  ok $is_subsequence->([1, 2, 3, 4, 3, 5, 6], [2, 3, 6]), 'multiple nums';
}

done_testing;

Пошук словника

Опис

Зображуємо елементи послідовності А до їх індексів. Потім ми шукаємо відповідні індекси для кожного елемента в B , пропускаємо ті індекси, які мають малий розмір, і вибираємо найменший можливий індекс як нижню межу. Якщо індекси не знайдені, B не є сукупністю.

Виконує щось на зразок O (subseq.size · k) , де k описує кількість дублікатів чисел seq. Плюс накладні витрати на O (seq.size)

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

Псевдокод:

Загальний стиль:

# preparing the lookup table
dict = {}
for i, x in seq:
  if exists dict[x]:
    dict[x].append(i)
  else:
    dict[x] = [i]

def subsequence? subseq:
  min_index = -1
  for x in subseq:
    if indices = dict[x]:
      suitable_indices = indices.filter(_ > min_index)
      return false if suitable_indices.empty?
      min_index = suitable_indices[0]
    else:
      return false
  return true

Функціональний стиль:

let subsequence? subseq =
  let rec subseq-loop = function
  | [] _ -> true
  | x::subseq min-index ->
    match (map (filter (_ > min-index)) data[x])
    | None -> false
    | Some([]) -> false
    | Some(new-min::_) -> subseq-loop subseq new-min
  in
    subseq-loop subseq -1

Приклад реалізації (Perl):

use strict; use warnings; use signatures; use Test::More;

sub build_dict ($seq) {
  my %dict;
  while (my ($i, $x) = each @$seq) {
    push @{ $dict{$x} }, $i;
  }
  return \%dict;
}

sub is_subsequence_i ($seq, $subseq) {
  my $min_index = -1;
  my $dict = build_dict($seq);
  for my $x (@$subseq) {
    my $indices = $dict->{$x} or return 0;
    ($min_index) = grep { $_ > $min_index } @$indices or return 0;
  }
  return 1;
}

sub is_subsequence_f ($seq, $subseq) {
  my $dict = build_dict($seq);
  use feature 'current_sub';
  return sub ($subseq, $min_index) {
    return 1 if @$subseq == 0;
    my ($x, @subseq) = @$subseq;
    my ($new_min) = grep { $_ > $min_index } @{ $dict->{$x} // [] } or return 0;
    __SUB__->(\@subseq, $new_min);
  }->($subseq, -1);
}

my $A = [1, 2, 3, 4];
my $B = [1, 3];
my $C = [1, 3, 4];
my $D = [3, 1];
my $E = [1, 2, 5];

for my $is_subsequence (\&is_subsequence_i, \&is_subsequence_f) {
  ok $is_subsequence->($A, $B), 'B in A';
  ok $is_subsequence->($A, $C), 'C in A';
  ok ! $is_subsequence->($A, $D), 'D not in A';
  ok ! $is_subsequence->($A, $E), 'E not in A';
  ok $is_subsequence->([1, 2, 3, 4, 3, 5, 6], [2, 3, 6]), 'multiple nums';
}

done_testing;

Варіант пошуку словника: Кодування як машина кінцевого стану

Опис

Ми можемо додатково зменшити алгоритмічну складність до О (subseq.size), якщо торгувати більшою кількістю пам'яті. Замість відображення елементів у їх індекси ми створюємо графік, де кожен вузол представляє елемент у своєму індексі. На краях показані можливі переходи, наприклад, послідовність a, b, aповинна мати краї a@1 → b@2, a@1 → a@3, b@2 → a@3. Цей графік еквівалентний машині з кінцевим станом.

Під час пошуку ми підтримуємо курсор, який спочатку є першим вузлом дерева. Потім ми ходити краю для кожного елемента в подспісок B . Якщо такого краю немає, то B - це не підпис. Якщо після всіх елементів курсор містить дійсний вузол, то B - це підпис.

Псевдокод

Загальний стиль:

# preparing the graph
graph = {}
for x in seq.reverse:
  next_graph = graph.clone
  next_graph[x] = graph
  graph = next_graph

def subseq? subseq:
  cursor = graph
  for x in subseq:
    cursor = graph[x]
    return false if graph == null
  return true

Функціональний стиль:

let subseq? subseq =
  let rec subseq-loop = function
  | [] _ -> true
  | x::subseq graph -> match (graph[x])
    | None -> false
    | Some(next-graph) -> subseq-loop subseq next-graph
  in
    subseq-loop subseq graph

Приклад реалізації (Perl):

use strict; use warnings; use signatures; use Test::More;

sub build_graph ($seq) {
  my $graph = {};
  for (reverse @$seq) {
    $graph = { %$graph, $_ => $graph };
  }
  return $graph;
}

sub is_subsequence_i ($seq, $subseq) {
  my $cursor = build_graph($seq);
  for my $x (@$subseq) {
    $cursor = $cursor->{$x} or return 0;
  }
  return 1;
}

sub is_subsequence_f ($seq, $subseq) {
  my $graph = build_graph($seq);
  use feature 'current_sub';
  return sub ($subseq, $graph) {
    return 1 if @$subseq == 0;
    my ($x, @subseq) = @$subseq;
    my $next_graph = $graph->{$x} or return 0;
    __SUB__->(\@subseq, $next_graph);
  }->($subseq, $graph);
}

my $A = [1, 2, 3, 4];
my $B = [1, 3];
my $C = [1, 3, 4];
my $D = [3, 1];
my $E = [1, 2, 5];

for my $is_subsequence (\&is_subsequence_i, \&is_subsequence_f) {
  ok $is_subsequence->($A, $B), 'B in A';
  ok $is_subsequence->($A, $C), 'C in A';
  ok ! $is_subsequence->($A, $D), 'D not in A';
  ok ! $is_subsequence->($A, $E), 'E not in A';
  ok $is_subsequence->([1, 2, 3, 4, 3, 5, 6], [2, 3, 6]), 'multiple nums';
}

done_testing;

Як осторонь, чи ти замислювався над тим, як studyпрацює, і якщо алгоритми, які він застосовує, можуть мати тут практичне застосування?

1
@MichaelT Я не впевнений, що розумію ... Я магістр, але ще не дізнався, як насправді вчитися </joke>. Якщо ви говорите про вбудовану функцію Perl: це сьогодні неоперація. Поточна реалізація - це лише десяток ліній зворотної сумісності. Двигун regex використовує таку евристику безпосередньо, як пошук постійних рядків перед узгодженням шаблонів змінних розмірів. studyраніше будував таблиці пошуку символів-позицій, на відміну від мого другого рішення.
amon

оновлено ще кращим алгоритмом
amon

Більш детально розробляючи цей FSM, ви зможете «зібрати» всі тестові послідовності в одну FSM, а потім пропустити всю послідовність. Залежно від того, в якому стані ви знаходилися в кінці, визначається, які підпорядкування були збігані. Це, звичайно, те, що можна було б скористатися комп'ютером, а не робити вручну для будь-якого нетривіального.

@MichaelT Ви праві, що ми могли створити розпізнавач таким чином. Однак ми вже знизилися до n · O (B) + вартість ініціалізації в O (f (A)) . Побудова трійчастої структури всіх B повинна мати щось на зразок O (n · B) , при цьому відповідність знаходиться в O (A) . Це має реальний шанс теоретично дешевше (побудова графіка в третьому рішенні може бути дорогим, але це лише разова вартість). Я думаю, що трие краще підходить для A ≫ n · B , і він має той недолік, що він не може обробляти потоковий вхід - всі B повинні бути завантажені перед узгодженням. Я, мабуть, оновлю відповідь через 6 год.
амон

6

Ось практичний підхід, який дозволяє уникнути "важкої роботи" щодо впровадження власного алгоритму, а також уникає "винаходити колесо": використовувати механізм регулярного вираження проблеми.

Просто покладіть всі числа A у рядок, а всі числа B у рядок, розділену регулярним виразом (.*). Додайте ^символ на початку та $в кінці. Тоді нехай ваш улюблений движок регулярних виразів шукає всі матчі. Наприклад, коли

A = (1234356), B = (236)

створити реєстр ехр для B , як ^(.*)2(.*)3(.*)6(.*)$. Тепер запустіть глобальний пошук в регулярному вираженні. Щоб дізнатись, на яких позиціях у вашій послідовності збігається, просто перевірте довжину перших трьох підмножин.

Якщо ваш діапазон цілих чисел залишає від 0 до 9, ви можете розглянути можливість їх кодування першими буквами, щоб зробити цю роботу, або вам доведеться адаптувати ідею, використовуючи роздільний символ.

Звичайно, швидкість такого підходу буде багато залежати від швидкості двигуна reg exp, який ви використовуєте, але є високооптимізовані двигуни, і я думаю, що буде важко реалізувати більш швидкий алгоритм "поза коробкою" .


Не потрібно йти по всьому, щоб викликати регулярний вираз і його двигун. Можна було б використовувати прості детерміновані кінцеві автомати для його запуску. Це "пряма лінія" через.

@MichaelT: ну, я не маю під рукою бібліотеки "загальних кінцевих автоматів", і ОП не розповіла нам про мову програмування, якою він користується, але регулярні вирази доступні сьогодні майже для кожної серйозної мови програмування "поза полем ". Це повинно зробити мою пропозицію дуже простою для реалізації, із значно меншим кодом, ніж, наприклад, рішення Amon. ІМХО ОП повинен спробувати, якщо це занадто повільно для нього, він все одно може спробувати, якщо складніше рішення буде служити йому краще.
Док Браун

Вам не потрібна родова бібліотека. Все, що вам потрібно, це масив 'шаблону' та вказівник на індекс у масиві. Індекс вказує на наступне значення "шукає", і коли ви читаєте його з джерела, збільшуйте індекс. Коли ви потрапили в кінець масиву, ви зіставили його. Якщо ви читаєте кінець джерела, не доходячи до кінця, ви його не відповідали.

@MichaelT: то чому б ти не опублікувавш ескіз цього алгоритму як відповідь?
Док Браун

Переважно тому, що на його відповідь вже було краще - "Ми підтримуємо курсор для послідовності A. Потім ми повторюємо всі елементи в підрядці B. Для кожного елемента переміщуємо курсор в A, поки не знайдемо відповідний елемент. Якщо ні знайдено відповідний елемент, тоді B - це не підпорядкованість. "

0

Цей алгоритм повинен бути досить ефективним, якщо отримання довжини та повторення послідовності є ефективним.

  1. Порівняйте довжину обох послідовностей. Зберігайте довше sequenceі коротшеsubsequence
  2. Почніть з початку обох послідовностей та циклу до кінця sequence.
    1. Чи число в поточному положенні sequenceдорівнює числу в поточному положенніsubsequence
    2. Якщо так, перенесіть обидві позиції вперед
    3. Якщо ні, рухайте лише положення sequenceодного далі
  3. Є позиція subsequenceв кінціsequence
  4. Якщо так, то дві послідовності відповідають
  5. Якщо ні, то дві послідовності не збігаються
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.