Struttura dei dati o algoritmo per trovare rapidamente le differenze tra le stringhe


19

Ho un array di 100.000 stringhe, tutte di lunghezza . Voglio confrontare ogni stringa con ogni altra stringa per vedere se due stringhe differiscono di 1 carattere. In questo momento, quando aggiungo ogni stringa all'array, la sto confrontando con ogni stringa già presente nell'array, che ha una complessità temporale di .n ( n - 1 )kn(n1)2k

Esiste una struttura dati o un algoritmo in grado di confrontare le stringhe tra loro più velocemente di quello che sto già facendo?

Alcune informazioni aggiuntive:

  • L'ordine è importante: abcdee xbcdedifferiscono di 1 carattere, mentre abcdee edcbadifferire di 4 caratteri.

  • Per ogni coppia di stringhe che differiscono di un carattere, rimuoverò una di quelle stringhe dall'array.

  • In questo momento, sto cercando stringhe che differiscono solo per 1 carattere, ma sarebbe bello se quella differenza di 1 carattere potesse essere aumentata, diciamo, di 2, 3 o 4 caratteri. Tuttavia, in questo caso, penso che l'efficienza sia più importante della capacità di aumentare il limite di differenza del personaggio.

  • k è di solito nell'intervallo 20-40.


4
Cercare un dizionario di stringhe con 1 errore è un problema abbastanza noto, ad esempio cs.nyu.edu/~adi/CGL04.pdf
KWillets,

1
20-40 metri possono usare un bel po 'di spazio. Potresti guardare un filtro Bloom ( en.wikipedia.org/wiki/Bloom_filter ) per verificare se le stringhe degenerate - l'insieme di tutti i mers da una, due o più sostituzioni su un mer test - sono "forse-in" o "sicuramente -non-in "una serie di km. Se ottieni un "forse-in", confronta ulteriormente le due stringhe per determinare se si tratta o meno di un falso positivo. I casi "sicuramente non-in" sono veri negativi che ridurranno il numero complessivo di confronti lettera per lettera che devi fare, limitando i confronti solo ai potenziali colpi "forse-in".
Alex Reynolds,

Se lavorassi con un intervallo più piccolo di k, potresti usare un bitset per memorizzare una tabella hash di booleani per tutte le stringhe degenerate (ad esempio github.com/alexpreynolds/kmer-boolean per esempio giocattolo). Per k = 20-40, tuttavia, i requisiti di spazio per un bitset sono semplicemente troppo.
Alex Reynolds,

Risposte:


12

È possibile raggiungere il tempo di esecuzione nel caso peggiore di .O(nklogk)

Cominciamo semplice. Se ti interessa una soluzione facile da implementare che sarà efficiente su molti input, ma non tutti, ecco una soluzione semplice, pragmatica e facile da implementare che molti sono sufficienti nella pratica per molte situazioni. Tuttavia, nel peggiore dei casi ricade nel tempo di esecuzione quadratico.

Prendi ogni stringa e memorizzala in una tabella hash, digitata sulla prima metà della stringa. Quindi, scorrere i bucket con hash. Per ogni coppia di stringhe nello stesso bucket, controlla se differiscono in 1 carattere (ovvero verifica se la loro seconda metà differisce in 1 carattere).

Quindi, prendi ogni stringa e memorizzala in una tabella hash, questa volta digitata sulla seconda metà della stringa. Controlla di nuovo ogni coppia di stringhe nello stesso bucket.

Supponendo che le stringhe siano ben distribuite, il tempo di esecuzione sarà probabilmente di circa . Inoltre, se esiste una coppia di stringhe che differiscono di 1 carattere, verrà trovata durante uno dei due passaggi (poiché differiscono di 1 solo carattere, il carattere diverso deve trovarsi nella prima o nella seconda metà della stringa, quindi la seconda o prima metà della stringa deve essere la stessa). Tuttavia, nel peggiore dei casi (ad esempio, se tutte le stringhe iniziano o finiscono con gli stessi k / 2 caratteri), questo degrada a O ( n 2 k ) tempo di esecuzione, quindi il suo tempo di esecuzione nel caso peggiore non è un miglioramento della forza bruta .O(nk)k/2O(n2k)

Come ottimizzazione delle prestazioni, se un bucket ha troppe stringhe, puoi ripetere lo stesso processo in modo ricorsivo per cercare una coppia che differisce di un carattere. L'invocazione ricorsiva sarà su stringhe di lunghezza .k/2

Se ti interessa il tempo di esecuzione nel caso peggiore:

Con l'ottimizzazione delle prestazioni di cui sopra credo che il tempo di esecuzione nel caso peggiore sia .O(nklogk)


3
Se le stringhe condividono lo stesso primo semestre, il che può benissimo accadere nella vita reale, allora non hai migliorato la complessità. Ω(n)
einpoklum,

@einpoklum, sicuro! Ecco perché ho scritto l'affermazione nella mia seconda frase secondo cui ricade nel tempo di esecuzione quadratico nel caso peggiore, così come l'affermazione nella mia ultima frase che descrive come raggiungere la complessità peggiore di se ti interessa sul caso peggiore. Ma suppongo che forse non l'ho espresso molto chiaramente, quindi ho modificato la mia risposta di conseguenza. Va meglio adesso? O(nklogk)
DW

15

La mia soluzione è simile a j_random_hacker's ma utilizza solo un singolo set di hash.

Vorrei creare un set di stringhe di hash. Per ogni stringa nell'input, aggiungi al set stringhe. In ciascuna di queste stringhe sostituisci una delle lettere con un carattere speciale, che non si trova in nessuna delle stringhe. Mentre li aggiungi, controlla che non siano già nel set. Se lo sono, hai due stringhe che differiscono solo per (al massimo) un carattere.k

Un esempio con stringhe 'abc', 'adc'

Per abc aggiungiamo '* bc', 'a * c' e 'ab *'

Per adc aggiungiamo '* dc', 'a * c' e 'ad *'

Quando aggiungiamo 'a * c' la seconda volta notiamo che è già nel set, quindi sappiamo che ci sono due stringhe che differiscono solo per una lettera.

Il tempo di esecuzione totale di questo algoritmo è . Questo perché creiamo k nuove stringhe per tutte le n stringhe nell'input. Per ciascuna di queste stringhe dobbiamo calcolare l'hash, che in genere richiede tempo O ( k ) .O(nk2)knO(k)

La memorizzazione di tutte le stringhe richiede spazio .O(nk2)

Ulteriori miglioramenti

Possiamo migliorare ulteriormente l'algoritmo non memorizzando direttamente le stringhe modificate ma invece memorizzando un oggetto con un riferimento alla stringa originale e all'indice del carattere mascherato. In questo modo non è necessario creare tutte le stringhe e serve solo lo spazio per memorizzare tutti gli oggetti.O(nk)

Dovrai implementare una funzione hash personalizzata per gli oggetti. Possiamo prendere l'implementazione Java come esempio, vedere la documentazione di Java . Java hashCode moltiplica il valore unicode di ciascun carattere per (con k la lunghezza della stringa e i l'indice a base singola del carattere. Nota che ogni stringa modificata differisce solo di un carattere dall'originale. Possiamo facilmente calcolare il contributo di quel carattere al codice hash. Possiamo sottrarlo e aggiungere invece il nostro carattere di mascheramento. Questo richiede O ( 1 ) per calcolare. Questo ci consente di ridurre il tempo di esecuzione totale a O ( n31kikiO(1)O(nk)


4
@JollyJoker Sì, lo spazio è una preoccupazione per questo metodo. È possibile ridurre lo spazio non memorizzando le stringhe modificate, ma invece memorizzando un oggetto con un riferimento alla stringa e all'indice mascherato. Questo dovrebbe lasciarti con O (nk) spazio.
Simon Prins,

Per calcolare gli hash per ogni stringa nel tempo O ( k ) , penso che avrai bisogno di una speciale funzione hash casalinga (ad esempio, calcola l'hash della stringa originale nel tempo O ( k ) , quindi XOR con ciascuna delle voci eliminate personaggi in O ( 1 ) ogni volta (anche se questa è probabilmente una funzione hash piuttosto male in altri modi)). A proposito, questo è abbastanza simile alla mia soluzione, ma con una sola tabella hash invece di k separate, e sostituendo un carattere con "*" invece di eliminarlo. kO(k)O(k)O(1)k
j_random_hacker,

@SimonPrins Con personalizzazioni equalse hashCodemetodi che potrebbero funzionare. Basta creare la stringa di tipo a * b in questi metodi per renderla antiproiettile; Sospetto che alcune delle altre risposte qui avranno problemi di collisione dell'hash.
JollyJoker,

1
@DW Ho modificato il mio post per riflettere il fatto che il calcolo degli hash richiede tempo e ho aggiunto una soluzione per riportare il tempo di esecuzione totale a O ( n k ) . O(k)O(nk)
Simon Prins,

1
@SimonPrins Il caso peggiore potrebbe essere nk ^ 2 a causa del controllo dell'uguaglianza delle stringhe in hashset.contains quando gli hash si scontrano. Naturalmente, il caso peggiore è quando ogni stringa ha lo stesso hash esatto, che richiederebbe un insieme più o meno artigianale di stringhe, in particolare per ottenere lo stesso hash per *bc, a*c, ab*. Mi chiedo se potrebbe essere dimostrato impossibile?
JollyJoker,

7

Vorrei fare hashtables H 1 , ... , H k , ognuno dei quali ha una stringa di lunghezza ( k - 1 ) come chiave e un elenco di numeri (ID stringa) come valore. La tabella hash H i conterrà tutte le stringhe elaborato finora , ma con il carattere alla posizione i cancellato . Ad esempio, se k = 6 , quindi H 3 [ A B D E F ] conterrà un elenco di tutte le stringhe viste finora che hanno il modello AkH1,,Hk(k1)Hiik=6H3[ABDEF] , dove significa "qualsiasi carattere". Quindi per elaborare la j -esima stringa di input s j :ABDEFjsj

  1. Per ogni nell'intervallo da 1 a k : ik
    • Stringa Modulo cancellando l' ho personaggio -esimo da s j .sjisj
    • Cerca . Ogni ID stringa qui identifica una stringa originale che è uguale a s o differisce solo nella posizione i . Emetti questi come corrispondenze per la stringa s j . (Se si desidera escludere duplicati esatti, impostare il tipo di valore degli hashtable una coppia (ID stringa, carattere eliminato), in modo da poter verificare quelli a cui è stato eliminato lo stesso carattere che abbiamo appena eliminato da s j .)Hi[sj]sisjsj
    • Inserisci in H i per future query da usare.jHi

Se memorizziamo esplicitamente ogni chiave hash, allora dobbiamo usare lo spazio e quindi avere almeno una complessità temporale. Ma come descritto da Simon Prins , è possibile rappresentare una serie di modifiche a una stringa (nel suo caso descritta come modifica di singoli caratteri in , nella mia come eliminazioni) implicitamente in modo tale che tutte le k chiavi di hash per una particolare stringa necessitino solo O ( k ) spazio, portando allo spazio O ( n k ) in generale, e aprendo la possibilità di O ( n k )O(nk2)*kO(k)O(nk)O(nk)anche il tempo. Per ottenere questa complessità temporale, abbiamo bisogno di un modo per calcolare gli hash per tutte le variazioni di una stringa di lunghezza k nel tempo O ( k ) : ad esempio, questo può essere fatto usando gli hash polinomiali, come suggerito da DW (e questo è probabilmente molto meglio del semplice XORing del carattere cancellato con l'hash per la stringa originale).kkO(k)

Il trucco implicito della rappresentazione di Simon Prins significa anche che la "cancellazione" di ciascun personaggio non viene effettivamente eseguita, quindi possiamo usare la solita rappresentazione basata su array di una stringa senza penalità di prestazione (piuttosto che liste collegate come avevo suggerito inizialmente).


2
Bella soluzione. Un esempio di una funzione hash su misura adatta sarebbe un hash polinomiale.
DW

Grazie @DW Potresti forse chiarire un po 'cosa intendi per "hash polinomiale"? Cercare su Google il termine non mi ha procurato nulla di definitivo. (
Sentiti

1
Basta leggere la stringa come un numero base modulo p , dove p è un numero primo inferiore alla dimensione hashmap e q è una radice primitiva di p e q è maggiore della dimensione dell'alfabeto. Si chiama "hash polinomiale" perché è come valutare il polinomio i cui coefficienti sono dati dalla stringa in q . Lo lascerò come un esercizio per capire come calcolare tutti gli hash desiderati nel tempo O ( k ) . Nota che questo approccio non è immune da un avversario, a meno che tu non scelga casualmente entrambi p , q soddisfacendo le condizioni desiderate.qppqpqqO(k)p,q
user21820

1
Penso che questa soluzione possa essere ulteriormente perfezionata osservando che solo una delle tabelle di hash k deve esistere in qualsiasi momento, riducendo così il fabbisogno di memoria.
Michael Kay,

1
@MichaelKay: Non funzionerà se si desidera calcolare gli hash delle possibili alterazioni di una stringa nel tempo O ( k ) . Devi ancora conservarli da qualche parte. Quindi, se controlli solo una posizione alla volta, impiegherai k volte purché controlli tutte le posizioni insieme usando k volte quante voci hashtable. kO(k)kk
user21820,

2

Ecco un approccio hashtable più solido rispetto al metodo polinomiale-hash. In primo luogo generano interi positivi casuali r 1 .. k che sono primi con la dimensione tabella hash M . Vale a dire, 0 r i < M . Poi hash ogni stringa x 1 .. k a ( Σ k i = 1 x i r i ) mod M . Non c'è quasi nulla che un avversario possa fare per causare collisioni molto irregolari, poiché si genera r 1 .. k in fase di esecuzione e quindi come kkr1..kM0ri<Mx1..k(i=1kxiri)modMr1..kkaumenta la probabilità massima di collisione di una data coppia di stringhe distinti va rapidamente a . È anche ovvio come calcolare nel tempo O ( k ) tutti gli hash possibili per ogni stringa con un carattere modificato.1/MO(k)

Se vuoi davvero garantire un hash uniforme, puoi generare un numero naturale casuale inferiore a M per ogni coppia ( i , c ) per i da 1 a k e per ogni carattere c , quindi hash per ogni stringa x 1 .. da k a ( k i = 1 r ( i , x i ) ) mod Mr(i,c)M(i,c)i1kcx1..k(i=1kr(i,xi))modM. Allora la probabilità di collisione di una data coppia di stringhe distinte è esattamente . Questo approccio è migliore se il tuo set di caratteri è relativamente piccolo rispetto a n .1/Mn


2

Molti degli algoritmi pubblicati qui usano abbastanza spazio sulle tabelle hash. Ecco un algoritmo semplice di runtime memoria ausiliaria O ( ( n lg n ) k 2 ) .O(1)O((nlgn)k2)

Il trucco è quello di utilizzare , che è un comparatore tra due valori a e b che restituisce vero se una < b (lexicographically) ignorando la k esimo carattere. Quindi l'algoritmo è il seguente.Ck(a,b)aba<bk

Innanzitutto, basta semplicemente ordinare le stringhe regolarmente ed eseguire una scansione lineare per rimuovere eventuali duplicati.

Quindi, per ogni :k

  1. Ordinare le stringhe con come comparatore.Ck

  2. Le stringhe che differiscono solo in sono ora adiacenti e possono essere rilevate in una scansione lineare.k


1

Due stringhe di lunghezza k , che differiscono in un carattere, condividono un prefisso di lunghezza L e un suffisso di lunghezza m tale che k = l + m + 1 .

La risposta di Simon Prins codifica questo memorizzando tutte le combinazioni prefisso / suffisso in modo esplicito, cioè abcdiventa *bc, a*ce ab*. Quello è k = 3, l = 0,1,2 e m = 2,1,0.

Come sottolinea valarMorghulis, puoi organizzare le parole in un albero di prefissi. C'è anche l'albero dei suffissi molto simile. È abbastanza facile aumentare l'albero con il numero di nodi foglia sotto ciascun prefisso o suffisso; questo può essere aggiornato in O (k) quando si inserisce una nuova parola.

Il motivo per cui si desidera che questi conteggi dei fratelli siano così, come si sa, data una nuova parola, se si desidera enumerare tutte le stringhe con lo stesso prefisso o se enumerare tutte le stringhe con lo stesso suffisso. Ad esempio per "abc" come input, i possibili prefissi sono "", "a" e "ab", mentre i suffissi corrispondenti sono "bc", "c" e "". Come ovvio, per brevi suffissi è meglio elencare i fratelli nella struttura del prefisso e viceversa.

Come sottolinea @einpoklum, è certamente possibile che tutte le stringhe condividano lo stesso prefisso k / 2 . Questo non è un problema per questo approccio; l'albero del prefisso sarà lineare fino alla profondità k / 2 con ciascun nodo fino alla profondità k / 2 che è l'antenato di 100.000 nodi fogliari. Di conseguenza, l'albero dei suffissi verrà utilizzato fino alla profondità (k / 2-1), il che è positivo perché le stringhe devono differire nei loro suffissi dato che condividono i prefissi.

[modifica] Come ottimizzazione, una volta determinato il prefisso univoco più breve di una stringa, sai che se c'è un carattere diverso, deve essere l'ultimo carattere del prefisso e avresti trovato il quasi duplicato quando controllando un prefisso che era uno più corto. Quindi se "abcde" ha un prefisso univoco più breve "abc", significa che ci sono altre stringhe che iniziano con "ab?" ma non con "abc". Cioè se differissero in un solo personaggio, sarebbe quel terzo personaggio. Non è più necessario cercare "abc? E".

Con la stessa logica, se scoprissi che "cde" è un suffisso univoco più breve, allora sai che devi controllare solo il prefisso "ab" di lunghezza 2 e non i prefissi di lunghezza 1 o 3.

Si noti che questo metodo funziona solo per differenze di un carattere esatte e non generalizza a differenze di 2 caratteri, ma si basa su un carattere unico che è la separazione tra prefissi identici e suffissi identici.


Stai suggerendo che per ogni stringa e ogni 1 i k , troviamo il nodo P [ s 1 , , s i - 1 ] corrispondente al prefisso length- ( i - 1 ) nel prefisso trie e il nodo S [ s i + 1 , , s k ] corrispondente alla lunghezza- ( k - i - 1 )s1ikP[s1,,si1](i1)S[si+1,,sk](ki1)suffisso nel suffisso trie (ciascuno prende il tempo ammortizzato ) e confronta il numero di discendenti di ciascuno, scegliendo quale ha meno discendenti e quindi "sondando" per il resto della stringa in quel trie? O(1)
j_random_hacker,

1
Qual è il tempo di esecuzione del tuo approccio? Mi sembra che nel peggiore dei casi potrebbe essere quadratico: considera cosa succede se ogni stringa inizia e termina con gli stessi caratteri. k/4
DW

L'idea di ottimizzazione è intelligente e interessante. Avevi in ​​mente un modo particolare di fare il controllo per i mtach? Se "abcde" ha il prefisso univoco più breve "abc", ciò significa che dovremmo cercare qualche altra stringa del modulo "ab? De". Avevi in ​​mente un modo particolare per farlo, che sarebbe efficiente? Qual è il tempo di esecuzione risultante?
DW

@DW: L'idea è che per trovare le stringhe nella forma "ab? De", controlla l'albero dei prefissi quanti nodi foglia esistono sotto "ab" e nell'albero dei suffissi quanti nodi esistono sotto "de", quindi scegli il il più piccolo dei due da elencare. Quando tutte le stringhe iniziano e terminano con gli stessi k / 4 caratteri; ciò significa che i primi nodi k / 4 in entrambi gli alberi hanno un figlio ciascuno. E sì, ogni volta che hai bisogno di quegli alberi, quelli devono essere attraversati che è un passo O (n * k).
MSalters il

Per verificare una stringa del formato "ab? De" nel prefisso trie, è sufficiente accedere al nodo per "ab", quindi per ciascuno dei suoi figli , verificare se il percorso "de" esiste al di sotto di v . Cioè, non preoccuparti di enumerare altri nodi in questi sottotitoli. Questo richiede O ( a h ) tempo, dove a è la dimensione dell'alfabeto e h è l'altezza del nodo iniziale nel trie. h è O ( k ) , quindi se la dimensione dell'alfabeto è O ( n ) allora è effettivamente O ( n k )vvO(ah)ahhO(k)O(n)O(nk)tempo complessivo, ma alfabeti più piccoli sono comuni. Il numero di bambini (non discendenti) è importante, così come l'altezza.
j_random_hacker,

1

Conservare le stringhe nei bucket è un buon modo (ci sono già diverse risposte che lo delineano).

Una soluzione alternativa potrebbe essere quella di memorizzare le stringhe in un elenco ordinato . Il trucco è ordinare in base a un algoritmo di hash sensibile alla località . Questo è un algoritmo di hash che produce risultati simili quando l'input è simile [1].

Ogni volta che vuoi investigare una stringa, puoi calcolare il suo hash e cercare la posizione di quell'hash nella tua lista ordinata (prendendo per gli array o O ( n ) per gli elenchi collegati). Se scopri che i vicini (considerando tutti i vicini vicini, non solo quelli con un indice di +/- 1) di quella posizione sono simili (fuori da un personaggio) hai trovato la tua corrispondenza. Se non ci sono stringhe simili, puoi inserire la nuova stringa nella posizione trovata (che accetta O ( 1 ) per gli elenchi collegati e O ( n ) per gli array).O(log(n))O(n)O(1)O(n)

Un possibile algoritmo di hashing sensibile alla località potrebbe essere Nilsimsa (con l'implementazione open source disponibile ad esempio in Python ).

[1]: Nota che spesso gli algoritmi di hash, come SHA1, sono progettati per il contrario: produrre hash molto diversi per input simili, ma non uguali.

Disclaimer: Ad essere sincero, implementerei personalmente una delle soluzioni bucket nidificate / organizzate per alberi per un'applicazione di produzione. Tuttavia, l'idea dell'elenco ordinato mi è sembrata un'alternativa interessante. Si noti che questo algoritmo dipende fortemente dall'algoritmo di hash scelto. Nilsimsa è un algoritmo che ho trovato - ce ne sono molti altri (ad esempio TLSH, Ssdeep e Sdhash). Non ho verificato che Nilsimsa funzioni con il mio algoritmo delineato.


1
Un'idea interessante, ma penso che dovremmo avere dei limiti su quanto possono essere distanti due valori di hash quando i loro input differiscono di solo 1 carattere - quindi scansiona tutto all'interno di quell'intervallo di valori di hash, anziché solo i vicini. (È impossibile avere una funzione hash che produca valori hash adiacenti per tutte le possibili coppie di stringhe che differiscono di 1 carattere. Considerare le stringhe lunghezza-2 in un alfabeto binario: 00, 01, 10 e 11. Se h (00) è adiacente sia a h (10) che a h (01), allora deve essere tra loro, nel qual caso h (11) non può essere adiacente a entrambi e viceversa.)
j_random_hacker

Guardare i vicini non è sufficiente. Considera l'elenco abcd, acef, agcd. Esiste una coppia corrispondente, ma la tua procedura non la troverà, poiché abcd non è un vicino di casa di agcd.
DW

Entrambi avete ragione! Con i vicini non intendevo solo "vicini diretti", ma pensavo a "un quartiere" di posizioni vicine. Non ho specificato quanti vicini debbano essere esaminati poiché dipende dall'algoritmo hash. Ma hai ragione, dovrei probabilmente annotarlo nella mia risposta. grazie :)
tessi il

1
"LSH ... oggetti simili si mappano sugli stessi" bucket "con alta probabilità" - dato che è un algoritmo di probabilità, il risultato non è garantito. Quindi dipende da TS se ha bisogno di una soluzione al 100% o il 99,9% è sufficiente.
Bulat,

1

O(nk+n2)O(nk)

  1. nX=x1.x2.x3....xnxi,1inX

  2. xi(i1)kxixjj<ixjxi=xjxi[p]xj[p]xjxixj

    for (i=2; i<= n; ++i){
        i_pos = (i-1)k;
        for (j=1; j < i; ++j){
            j_pos = (j-1)k;
            lcp_len = LCP (i_pos, j_pos);
            if (lcp_len < k) { // mismatch
                if (lcp_len == k-1) { // mismatch at the last position
                // Output the pair (i, j)
                }
                else {
                  second_lcp_len = LCP (i_pos+lcp_len+1, j_pos+lcp_len+1);
                  if (lcp_len+second_lcp_len>=k-1) { // second lcp goes beyond
                    // Output the pair(i, j)
                  }
                }
            }
        }
    }
    

È possibile utilizzare la libreria SDSL per creare l'array di suffissi in forma compressa e rispondere alle query LCP.

XO(nk)O(n2)

O(nk+qn2)q

j<ij


O(kn2)k

O(nk+n2)O(kn2)O(1)

Il mio punto è che k = 20..40 per l'autore della domanda e il confronto di stringhe così piccole richiedono solo pochi cicli della CPU, quindi la differenza pratica tra la forza bruta e il tuo approccio probabilmente non esiste.
Bulat,

1

O(nk)**bcdea*cde

È inoltre possibile utilizzare questo approccio per dividere il lavoro tra più core CPU / GPU.


n=100,000k40O(nk)

0

Questa è una versione breve della risposta di @SimonPrins che non comporta hash.

Supponendo che nessuna delle tue stringhe contenga un asterisco:

  1. nkkO(nk2)
  2. O(nk2lognk)
  3. O(nk2)

Una soluzione alternativa con l'uso implicito di hash in Python (non può resistere alla bellezza):

def has_almost_repeats(strings,k):
    variations = [s[:i-1]+'*'+s[i+1:] for s in strings for i in range(k)]
    return len(set(variations))==k*len(strings)

kO(nk)

O(n2)

0

Ecco la mia opinione su 2+ findmmatches finder. Si noti che in questo post considero ogni stringa circolare, una sottostringa di lunghezza 2 in corrispondenza dell'indice è k-1composta dal simbolo str[k-1]seguito da str[0]. E la sottostringa di lunghezza 2 all'indice -1è la stessa!

Mkmlen(k,M)=k/M1Mk=20M=4abcd*efgh*ijkl*mnop*

Ora, l'algoritmo per la ricerca di tutti i disallineamenti fino ai Msimboli tra stringhe di ksimboli:

  • per ogni i da 0 a k-1
    • dividere tutte le stringhe in gruppi per str[i..i+L-1], dove L = mlen(k,M). Fe se L=4e hai un alfabeto di soli 4 simboli (dal DNA), questo farà 256 gruppi.
    • I gruppi più piccoli di ~ 100 stringhe possono essere controllati con l'algoritmo a forza bruta
    • Per gruppi più grandi, dovremmo eseguire la divisione secondaria:
      • Rimuovi da ogni stringa nei Lsimboli di gruppo che abbiamo già abbinato
      • per ogni j da i-L + 1 a kL-1
        • dividere tutte le stringhe in gruppi per str[i..i+L1-1], dove L1 = mlen(k-L,M). Fe se k=20, M=4, alphabet of 4 symbols, così L=4e L1=3, questo farà 64 gruppi.
        • il resto è lasciato come esercizio per il lettore: D

Perché non iniziamo jda 0? Poiché abbiamo già creato questi gruppi con lo stesso valore di i, quindi il lavoro con j<=i-Lsarà esattamente equivalente al lavoro con i valori i e j scambiati.

Ulteriori ottimizzazioni:

  • In ogni posizione, considera anche le stringhe str[i..i+L-2] & str[i+L]. Questo raddoppia solo la quantità di posti di lavoro creati, ma consente di aumentare Ldi 1 (se la mia matematica è corretta). Quindi, invece di 256 gruppi, dividerai i dati in 1024 gruppi.
  • L[i]*0..k-1M-1k-1

0

Lavoro quotidianamente sull'invenzione e l'ottimizzazione di algos, quindi se hai bisogno di ogni ultima prestazione, questo è il piano:

  • Verificare *in ogni posizione in modo indipendente, ovvero anziché n*kvarianti di stringhe di elaborazione di singoli lavori - avviare klavori indipendenti controllando ciascuna nstringa. È possibile distribuire questi klavori tra più core CPU / GPU. Questo è particolarmente importante se hai intenzione di controllare 2+ differenze di carattere. Le dimensioni ridotte del lavoro miglioreranno anche la localizzazione della cache, che di per sé può rendere il programma 10 volte più veloce.
  • Se hai intenzione di utilizzare le tabelle hash, usa la tua implementazione impiegando il probing lineare e un fattore di carico del ~ 50%. È veloce e abbastanza facile da implementare. Oppure utilizza un'implementazione esistente con indirizzamento aperto. Le tabelle hash STL sono lente a causa dell'uso di concatenamenti separati.
  • Puoi provare a pre-filtrare i dati usando il filtro Bloom a 3 stati (distinguendo 0/1/1 + occorrenze) come proposto da @AlexReynolds.
  • Per ogni i da 0 a k-1, eseguire il seguente lavoro:
    • Genera strutture a 8 byte contenenti hash a 4-5 byte per ogni stringa (con *all'i-esima posizione) e indice di stringa, quindi ordinale o crea una tabella hash da questi record.

Per l'ordinamento, puoi provare la seguente combinazione:

  • il primo passaggio è l'ordinamento radix MSD in 64-256 modi utilizzando il trucco TLB
  • il secondo passaggio è l'ordinamento MSD radix in 256-1024 modi senza trucco TLB (64K modi in totale)
  • il terzo passaggio è l'ordinamento per inserzione per correggere le incoerenze rimanenti
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.