Ricerca sequenze di numeri interi


14

Ho un problema di ricerca abbastanza complesso che sono riuscito a ridurre alla seguente descrizione. Ho cercato su Google ma non sono riuscito a trovare un algoritmo che sembra adattarsi perfettamente al mio problema. In particolare la necessità di saltare numeri interi arbitrari. Forse qualcuno qui può indicarmi qualcosa?

Prendi una sequenza di numeri interi A, ad esempio (1 2 3 4)

Prendi varie sequenze di numeri interi e verifica se qualcuno di loro corrisponde ad A tale.

  1. A contiene tutti i numeri interi nella sequenza testata
  2. L'ordinamento degli interi nella sequenza testata è lo stesso in A
  3. Non ci interessa alcun numero intero in A che non sia nella sequenza di test
  4. Vogliamo tutte le sequenze di test corrispondenti, non solo la prima.

Un esempio

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

B corrisponde ad A

C corrisponde A

D non corrisponde ad A poiché l'ordinamento è diverso

E non corrisponde ad A poiché contiene un numero intero non in A

Spero che questa spiegazione sia abbastanza chiara. La cosa migliore che sono riuscito a fare è quella di costruire un albero delle sequenze di test e di scorrere su A. La necessità di essere in grado di saltare numeri interi porta a molti percorsi di ricerca non riusciti.

Grazie

Leggendo alcuni suggerimenti, sento di dover chiarire un paio di punti che ho lasciato troppo vago.

  1. Sono consentiti numeri ripetuti, in effetti questo è abbastanza importante in quanto consente a una singola sequenza di test di abbinare A in più modi

    A = (1234356), B = (236), le partite potrebbero essere -23 --- 6 o -2--3-6

  2. Mi aspetto che ci sia un numero molto grande di sequenze di test, almeno in migliaia e la sequenza A tenderà ad avere una lunghezza massima di forse 20. Quindi, semplicemente cercare di abbinare ciascuna sequenza di test uno per uno ripetendo diventa estremamente inefficiente.

Scusa se non è stato chiaro.


4
Sembri come se volessi semplicemente rilevare sottosequenze ( en.wikipedia.org/wiki/Subsequence ). È così? Quindi prova a cercare "algoritmo di sottosequenza".
Kilian Foth,

Onestamente, alcune migliaia di sequenze con lunghezza massima <= 20 non mi suonano un gran numero. Un semplice approccio a forza bruta dovrebbe fare il trucco. O hai migliaia di sequenze "A", ognuna da testare contro migliaia di possibili sottosequenze?
Doc Brown,

Esiste un flusso continuo di sequenze A, ma sono completamente indipendenti l'una dall'altra. Tuttavia, un ritardo nell'elaborazione di uno ritarderà direttamente tutti gli altri, quindi la velocità è importante.
David Gibson,

1
Quanto è grande il tuo alfabeto? Hai davvero numeri interi arbitrari o esiste un intervallo finito di valori in modo che possiamo fare alcuni calcoli?
Frank,

La possibile gamma di numeri interi è tra le 100.000
David Gibson,

Risposte:


18

Hmm, posso pensare a due possibili algoritmi: una scansione lineare attraverso la sequenza A , o la costruzione di un dizionario con ricerca a tempo costante degli indici.

Se stai testando molte potenziali sottosequenze B rispetto a una singola sequenza più grande A , ti suggerirei di utilizzare la variante con il dizionario.

Scansione lineare

Descrizione

Abbiamo un cursore per la sequenza A . Poi abbiamo scorrere tutti gli elementi nella sottosequenza B . Per ogni elemento, spostiamo il cursore in avanti in A fino a quando non abbiamo trovato un elemento corrispondente. Se non viene trovato alcun elemento corrispondente, B non è una sottosequenza.

Questo funziona sempre in O (seq.size) .

pseudocodice

Stile imperativo:

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

Stile funzionale:

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

Esempio di implementazione (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;

Ricerca nel dizionario

Descrizione

Mappiamo gli elementi della sequenza A sui loro indici. Quindi cerchiamo gli indici adatti per ogni elemento in B , saltiamo quegli indici che sono troppo piccoli e scegliamo l'indice più piccolo possibile come limite inferiore. Quando non vengono trovati indici, quindi B non è una sottosequenza.

Funziona in qualcosa come O (subseq.size · k) , dove k descrive quanti numeri duplicati ci sono seq. Più un overhead O (seq.size)

Il vantaggio di questa soluzione è che una decisione negativa può essere raggiunta molto più rapidamente (fino a un tempo costante), una volta pagato il sovraccarico di costruzione della tabella di ricerca.

pseudocodice:

Stile imperativo:

# 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

Stile funzionale:

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

Esempio di implementazione (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;

Variante di ricerca nel dizionario: codifica come macchina a stati finiti

Descrizione

Possiamo ridurre ulteriormente la complessità algoritmica fino a O (subseq.size) se scambiamo più memoria. Invece di mappare gli elementi nei loro indici, creiamo un grafico in cui ciascun nodo rappresenta un elemento nel suo indice. I bordi mostrano possibili transizioni, ad esempio la sequenza a, b, aavrebbe i bordi a@1 → b@2, a@1 → a@3, b@2 → a@3. Questo grafico è equivalente a una macchina a stati finiti.

Durante la ricerca manteniamo un cursore che inizialmente è il primo nodo dell'albero. Abbiamo poi a piedi il bordo per ogni elemento della lista parziale B . Se non esiste tale limite, allora B non è un elenco secondario. Se dopo tutti gli elementi il ​​cursore contiene un nodo valido, allora B è un sottoelenco.

pseudocodice

Stile imperativo:

# 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

Stile funzionale:

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

Esempio di implementazione (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;

A parte questo, hai dato un'occhiata a come studyfunziona e se gli algoritmi applicati potrebbero avere qualche applicazione pratica qui?

1
@MichaelT Non sono sicuro di capire ... Sono un laureando, ma non ho ancora scoperto come studiare davvero </joke>. Se stai parlando della funzione integrata Perl: al giorno d'oggi è una no-op. L'attuale implementazione è solo una dozzina di linee di compatibilità con le versioni precedenti. Il motore regex utilizza direttamente tale euristica, come la ricerca di stringhe costanti prima di abbinare modelli di dimensioni variabili. studyaveva precedentemente creato tabelle di ricerca da carattere a posizione, non diversamente dalla mia seconda soluzione.
amon,

aggiornato con algoritmo ancora migliore
amon

Elaborando di più su tale FSM, è possibile "compilare" tutte le sequenze di test in un FSM e quindi eseguire l'intera sequenza. A seconda dello stato in cui ti trovavi alla fine determina quali sottosequenze sono state abbinate. È certamente la cosa che si vorrebbe fare fare al computer piuttosto che a mano per qualsiasi cosa non banale.

@MichaelT Lei ha ragione che abbiamo potuto costruire un sistema di riconoscimento in questo modo. Tuttavia, siamo già scesi a n · O (B) + costo di inizializzazione in O (f (A)) . Costruire la struttura a forma di trie di tutti i Bs richiederebbe qualcosa come O (n · B) , con la corrispondenza essendo in O (A) . Ciò ha una reale possibilità di essere più economico in teoria (costruire il grafico nella terza soluzione può essere costoso, ma è solo un costo una tantum). Penso che un trie sia più adatto per A ≫ n · B e abbia lo svantaggio di non poter gestire l'input di streaming - tutti i B&B devono essere caricati prima della corrispondenza. Probabilmente aggiornerò la risposta tra 6 ore.
amon

6

Ecco un approccio pratico che evita "il duro lavoro" di implementare il tuo algoritmo e evita anche di "reinventare la ruota": utilizzare un motore di espressione regolare per il problema.

Basta inserire tutti i numeri di A in una stringa e tutti i numeri di B in una stringa separati dall'espressione regolare (.*). Aggiungi un ^personaggio all'inizio e $alla fine. Quindi lascia che il tuo motore di espressioni regolari preferito cerchi tutte le corrispondenze. Ad esempio, quando

A = (1234356), B = (236)

crea un registro exp per B come ^(.*)2(.*)3(.*)6(.*)$. Ora esegui una ricerca regexp globale. Per scoprire a quali posizioni corrisponde la tua sottosequenza, controlla la lunghezza dei primi 3 invii.

Se il tuo intervallo di numeri interi va da 0 a 9, potresti considerare prima di codificarli con singole lettere per far funzionare questo, oppure devi adattare l'idea usando un carattere di separazione.

Ovviamente, la velocità di questo approccio dipenderà molto dalla velocità del motore di registro che stai utilizzando, ma ci sono motori altamente ottimizzati disponibili, e immagino che sarà difficile implementare un algoritmo più veloce "pronto all'uso" .


Non è necessario andare fino in fondo per invocare una regex e il suo motore. Sarebbe possibile utilizzare un semplice automa finito deterministico per eseguirlo. È un percorso in linea retta.

@MichaelT: beh, non ho una libreria di "automi finiti generici" e l'OP non ci ha parlato del linguaggio di programmazione che usa, ma oggi sono disponibili espressioni regolari per quasi tutti i linguaggi di programmazione " ". Ciò dovrebbe rendere il mio suggerimento molto semplice da implementare, con molto meno codice rispetto, ad esempio, alla soluzione di Amon. IMHO l'OP dovrebbe provarlo, se è troppo lento per lui, potrebbe ancora provare se una soluzione più complicata gli servirà meglio.
Doc Brown,

Non hai bisogno di una biblioteca generica. Tutto ciò che serve è l'array del "modello" e un puntatore all'indice nell'array. L'indice punta al valore "cerca" successivo e quando lo leggi dalla sorgente, incrementa l'indice. Quando hai raggiunto la fine dell'array, l'hai abbinato. Se leggi la fine della fonte senza raggiungere la fine, non l'hai abbinata.

@MichaelT: allora perché non pubblichi uno schizzo dell'algoritmo come risposta?
Doc Brown,

Soprattutto perché ha già una risposta migliore - "Manteniamo un cursore per la sequenza A. Quindi ripetiamo tutti gli elementi nella sottosequenza B. Per ogni elemento, spostiamo il cursore in avanti in A fino a quando non abbiamo trovato un elemento corrispondente. Se no l'elemento corrispondente è stato trovato, quindi B non è una sottosequenza. "

0

Questo algoritmo dovrebbe essere abbastanza efficace se ottenere la lunghezza e iterare la sequenza è efficace.

  1. Confronta la lunghezza di entrambe le sequenze. Conservare il più a lungo sequencee il più corto insubsequence
  2. Inizia all'inizio di entrambe le sequenze e continua fino alla fine di sequence.
    1. Il numero nella posizione corrente è sequenceuguale al numero nella posizione corrente disubsequence
    2. Se sì, sposta entrambe le posizioni di una posizione
    3. In caso contrario, spostare solo la posizione di sequenceun altro
  3. È la posizione di subsequencealla fine delsequence
  4. Se sì, le due sequenze corrispondono
  5. In caso contrario, le due sequenze non corrispondono
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.