Date due sequenze, trova la massima sovrapposizione tra la fine di una e l'inizio dell'altra


11

Devo trovare un codice (pseudo) efficiente per risolvere il seguente problema:

Date due sequenze di numeri interi (non necessariamente distinti) (a[1], a[2], ..., a[n])e (b[1], b[2], ..., b[n]), trovare il massimo dtale per cui a[n-d+1] == b[1], a[n-d+2] == b[2], ... e a[n] == b[d].

Non si tratta di compiti a casa, in realtà mi sono inventato quando ho cercato di contrarre due tensori lungo il maggior numero possibile di dimensioni. Sospetto che esista un algoritmo efficiente (forse O(n)?), Ma non riesco a trovare qualcosa che non lo sia O(n^2). L' O(n^2)approccio sarebbe l'ovvio loop attivo de quindi un loop interno sugli elementi per verificare le condizioni richieste fino a raggiungere il massimo d. Ma sospetto che qualcosa di meglio di questo sia possibile.


Se un hash rolling può essere calcolato per un gruppo di oggetti nel tuo array, penso che questo possa essere fatto in modo più efficiente. Calcola l'hash per gli elementi b[1] to b[d]e poi vai all'array acalcola l'hash per a[1] to a[d]se corrisponde, allora questa è la tua risposta, se non calcola l'hash per a[2] to a[d+1]riutilizzando l'hash calcolato per a[1] to a[d]. Ma non so se gli oggetti nell'array sono suscettibili di calcolarli su un hash rolling.
SomeDude,

2
@becko Mi dispiace, penso di aver finalmente capito cosa stai cercando di realizzare. Che è quello di trovare la massima sovrapposizione tra la fine di ae l'inizio di b. Ti piace questa .
user3386109

1
Mi sembra che il problema sia una variazione nella corrispondenza delle stringhe, che può essere risolta con una variazione dell'algoritmo Knuth – Morris – Pratt . Il tempo di esecuzione sarebbe O (m + n) dove mè il numero di elementi in a, ed nè il numero di elementi in b. Sfortunatamente, non ho esperienza sufficiente con KMP per dirti come adattarlo.
user3386109

1
@ user3386109 la mia soluzione è anche una variante di un algoritmo di adattamento delle stringhe chiamato Rabin-Karp , usando il metodo di Horner come funzione hash.
Daniel,

1
@Daniel Ah, sapevo di aver visto un hash rolling usato da qualche parte, ma non riuscivo a ricordare dove :)
user3386109

Risposte:


5

È possibile utilizzare l' algoritmo z , un algoritmo a tempo lineare ( O (n) ) che:

Data una stringa S di lunghezza n, l'algoritmo Z produce una matrice Z in cui Z [i] è la lunghezza della sottostringa più lunga a partire da S [i] che è anche un prefisso di S

È necessario concatenare le matrici ( b + a ) ed eseguire l'algoritmo sull'array costruito risultante fino alla prima i in modo tale che Z [i] + i == m + n .

Ad esempio, per a = [1, 2, 3, 6, 2, 3] & b = [2, 3, 6, 2, 1, 0], la concatenazione sarebbe [2, 3, 6, 2, 1 , 0, 1, 2, 3, 6, 2, 3] che darebbe Z [10] = 2 che soddisfa Z [i] + i = 12 = m + n .


Bellissimo! Grazie.
becko,

3

Per O (n) complessità tempo / spazio, il trucco è valutare gli hash per ogni sottosequenza. Considera l'array b:

[b1 b2 b3 ... bn]

Usando il metodo di Horner , puoi valutare tutti gli hash possibili per ogni sottosequenza. Scegli un valore di base B(più grande di qualsiasi valore in entrambi i tuoi array):

from b1 to b1 = b1 * B^1
from b1 to b2 = b1 * B^1 + b2 * B^2
from b1 to b3 = b1 * B^1 + b2 * B^2 + b3 * B^3
...
from b1 to bn = b1 * B^1 + b2 * B^2 + b3 * B^3 + ... + bn * B^n

Si noti che è possibile valutare ciascuna sequenza nel tempo O (1), utilizzando il risultato della sequenza precedente, quindi tutti i costi del lavoro O (n).

Ora hai un array Hb = [h(b1), h(b2), ... , h(bn)], dove Hb[i]è l'hash da b1fino a bi.

Fai la stessa cosa per l'array a, ma con un piccolo trucco:

from an to an   =  (an   * B^1)
from an-1 to an =  (an-1 * B^1) + (an * B^2)
from an-2 to an =  (an-2 * B^1) + (an-1 * B^2) + (an * B^3)
...
from a1 to an   =  (a1   * B^1) + (a2 * B^2)   + (a3 * B^3) + ... + (an * B^n)

È necessario notare che, quando si passa da una sequenza all'altra, si moltiplica l'intera sequenza precedente per B e si aggiunge il nuovo valore moltiplicato per B. Ad esempio:

from an to an =    (an   * B^1)

for the next sequence, multiply the previous by B: (an * B^1) * B = (an * B^2)
now sum with the new value multiplied by B: (an-1 * B^1) + (an * B^2) 
hence:

from an-1 to an =  (an-1 * B^1) + (an * B^2)

Ora hai un array Ha = [h(an), h(an-1), ... , h(a1)], dove Ha[i]è l'hash da aifino a an.

Ora puoi confrontare Ha[d] == Hb[d]tutti i dvalori da n a 1, se corrispondono, hai la tua risposta.


ATTENZIONE : questo è un metodo hash, i valori possono essere grandi e potrebbe essere necessario utilizzare un metodo di esponenziazione veloce e aritmetica modulare , che potrebbe (difficilmente) darti collisioni , rendendo questo metodo non del tutto sicuro. Una buona pratica è quella di scegliere una base Bcome un numero primo davvero grande (almeno più grande del valore più grande nelle tue matrici). Dovresti anche stare attento poiché i limiti dei numeri potrebbero traboccare ad ogni passaggio, quindi dovrai usare (modulo K) in ogni operazione (dove Kpuò essere un numero primo maggiore di B).

Ciò significa che due sequenze diverse potrebbero avere lo stesso hash, ma due sequenze uguali avranno sempre lo stesso hash.


Potete per favore iniziare questa risposta con una valutazione dei requisiti delle risorse?
Barbarossa

2

Questo può effettivamente essere fatto in tempo lineare, O (n) e O (n) spazio extra. Presumo che le matrici di input siano stringhe di caratteri, ma questo non è essenziale.

Un metodo ingenuo dovrebbe - dopo aver trovato k caratteri uguali - trovare un carattere che non corrisponde, e tornare indietro di k-1 unità in a , resettare l'indice in b e quindi iniziare il processo di corrispondenza da lì. Ciò rappresenta chiaramente un caso peggiore O (n²) .

Per evitare questo processo di backtracking, possiamo osservare che tornare indietro non è utile se non abbiamo riscontrato il carattere b [0] durante la scansione degli ultimi caratteri k-1 . Se abbiamo fatto scoprire che il carattere, poi marcia indietro a quella posizione sarebbe solo utile, se in quel k size substring abbiamo avuto una ripetizione periodica.

Ad esempio, se osserviamo la sottostringa "abcabc" da qualche parte in a , e b è "abcabd", e troviamo che il carattere finale di b non corrisponde, dobbiamo considerare che una corrispondenza corretta potrebbe iniziare alla seconda "a" nella sottostringa e dovremmo spostare di conseguenza il nostro indice corrente in b prima di continuare il confronto.

L'idea è quindi di eseguire una pre-elaborazione basata sulla stringa b per registrare i riferimenti a ritroso in b che sono utili per verificare in caso di mancata corrispondenza. Quindi, ad esempio, se b è "acaacaacd", potremmo identificare questi riferimenti indietro basati su 0 (posti sotto ogni carattere):

index: 0 1 2 3 4 5 6 7 8
b:     a c a a c a a c d
ref:   0 0 0 1 0 0 1 0 5

Ad esempio, se abbiamo un "acaacaaca" uguale, la prima discrepanza si verifica sul personaggio finale. Le informazioni di cui sopra dicono quindi all'algoritmo di tornare in b all'indice 5, poiché "acaac" è comune. E quindi con la sola modifica dell'indice corrente in b possiamo continuare la corrispondenza con l'indice corrente di a . In questo esempio, la corrispondenza del personaggio finale ha esito positivo.

Con questo siamo in grado di ottimizzare la ricerca e fare in modo che l'indice in un può sempre progredire in avanti.

Ecco un'implementazione di quell'idea in JavaScript, usando solo la sintassi più semplice di quella lingua:

function overlapCount(a, b) {
    // Deal with cases where the strings differ in length
    let startA = 0;
    if (a.length > b.length) startA = a.length - b.length;
    let endB = b.length;
    if (a.length < b.length) endB = a.length;
    // Create a back-reference for each index
    //   that should be followed in case of a mismatch.
    //   We only need B to make these references:
    let map = Array(endB);
    let k = 0; // Index that lags behind j
    map[0] = 0;
    for (let j = 1; j < endB; j++) {
        if (b[j] == b[k]) {
            map[j] = map[k]; // skip over the same character (optional optimisation)
        } else {
            map[j] = k;
        }
        while (k > 0 && b[j] != b[k]) k = map[k]; 
        if (b[j] == b[k]) k++;
    }
    // Phase 2: use these references while iterating over A
    k = 0;
    for (let i = startA; i < a.length; i++) {
        while (k > 0 && a[i] != b[k]) k = map[k];
        if (a[i] == b[k]) k++;
    }
    return k;
}

console.log(overlapCount("ababaaaabaabab", "abaababaaz")); // 7

Sebbene ci siano whileloop nidificati , questi non hanno più iterazioni in totale di n . Questo perché il valore di k diminuisce rigorosamente nel whilecorpo e non può diventare negativo. Questo può accadere solo quando è k++stato eseguito più volte per dare abbastanza spazio per tali riduzioni. Quindi, tutto sommato, non ci possono essere più esecuzioni del whilecorpo di quante ci siano k++esecuzioni, e quest'ultima è chiaramente O (n).

Per completare, qui puoi trovare lo stesso codice sopra, ma in uno snippet interattivo: puoi inserire le tue stringhe e vedere il risultato in modo interattivo:

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.