Il più veloce cercatore di conseguenze comune più lungo


11

Il tuo compito è risolvere il problema della susseguenza comune più lunga per n stringhe di lunghezza 1000.

Una valida soluzione al problema LCS per due o più stringhe S 1 , S ... n è qualsiasi stringa T di lunghezza massima tale che i caratteri di T appaiono in tutta S i , nello stesso ordine in T .

Si noti che T non deve essere un sub stringa di S i .

Abbiamo già risolto questo problema con la più breve quantità di codice . Questa volta, le dimensioni non contano.

Esempio

Le stringhe axbycze xaybzchanno 8 sottosequenze comuni di lunghezza 3:

abc abz ayc ayz xbc xbz xyc xyz

Ognuno di questi sarebbe una soluzione valida per il problema LCS.

Dettagli

Scrivi un programma completo che risolva il problema LCS, come spiegato sopra, rispettando le seguenti regole:

  • L'input sarà costituito da due o più stringhe di lunghezza 1000, costituite da caratteri ASCII con punti di codice compresi tra 0x30 e 0x3F.

  • Devi leggere l'input da STDIN.

    Hai due scelte per il formato di input:

    • Ogni stringa (compresa l'ultima) è seguita da un avanzamento riga.

    • Le stringhe sono concatenate insieme senza separatore né avanzamento riga finale.

  • Il numero di stringhe verrà passato come parametro della riga di comando al programma.

  • Devi scrivere l'output, ovvero una delle soluzioni valide su LCS, su STDOUT, seguito da un avanzamento di riga.

  • La tua lingua preferita deve avere un compilatore / interprete gratuito (come nella birra) per il mio sistema operativo (Fedora 21).

  • Se hai bisogno di flag di compilazione o di un interprete specifico, menzionalo nel tuo post.

punteggio

Eseguirò il tuo codice con 2, 3, ecc. Stringhe fino a quando ci vorranno più di 120 secondi per stampare una soluzione valida. Ciò significa che hai 120 secondi per ogni valore di n .

Il maggior numero di stringhe per cui il codice è terminato in tempo è il tuo punteggio.

In caso di punteggio pari a n , la domanda che ha risolto il problema per n stringhe nel minor tempo sarà dichiarata vincente.

Tutte le comunicazioni saranno programmate sulla mia macchina (Intel Core i7-3770, 16 GiB RAM, nessuno scambio).

Le n stringhe del (n-1) th test saranno generate chiamando rand n(e rimuovendo gli avanzamenti di riga, se richiesto), dove randè definito come segue:

rand()
{
    head -c$[500*$1] /dev/zero |
    openssl enc -aes-128-ctr -K 0 -iv $1 |
    xxd -c500 -ps |
    tr 'a-f' ':-?'
}

La chiave è 0nel codice sopra, ma mi riservo il diritto di cambiarlo in un valore non divulgato se sospetto che qualcuno abbia (parzialmente) codificato l'output.


Possiamo gettare eccezioni?
HyperNeutrino,

@JamesSmith Finché l'output è corretto, certo.
Dennis,

Dal momento che sto leggendo con bufferedreader, posso lanciare ioexception public static void main(...)?
HyperNeutrino,

@JamesSmith Non conosco davvero Java, quindi non so cosa sia, ma non mi preoccupo delle eccezioni.
Dennis,

4
@JamesSmith Poiché la lunghezza del codice non è importante per questa sfida, non puoi semplicemente cogliere le eccezioni?
Reto Koradi,

Risposte:


5

C, n = 3 in ~ 7 secondi

Algoritmo

L'algoritmo è una generalizzazione diretta della soluzione di programmazione dinamica standard alle nsequenze. Per 2 stringhe Ae B, la ricorrenza standard è simile alla seguente:

L(p, q) = 1 + L(p - 1, q - 1)           if A[p] == B[q]
        = max(L(p - 1, q), L(p, q - 1)) otherwise

Per 3 stringhe A, B, Cio uso:

L(p, q, r) = 1 + L(p - 1, q - 1, r - 1)                          if A[p] == B[q] == C[r]
           = max(L(p - 1, q, r), L(p, q - 1, r), L(p, q, r - 1)) otherwise

Il codice implementa questa logica per valori arbitrari di n.

Efficienza

La complessità del mio codice è O (s ^ n), con sla lunghezza delle stringhe. Sulla base di quello che ho trovato, sembra che il problema sia NP-completo. Quindi, mentre l'algoritmo pubblicato è molto inefficiente per valori più grandi di n, in realtà potrebbe non essere possibile fare molto meglio. L'unica cosa che ho visto sono alcuni approcci che migliorano l'efficienza per i piccoli alfabeti. Poiché l'alfabeto è moderatamente piccolo qui (16), ciò potrebbe portare a un miglioramento. Prevedo ancora che nessuno troverà una soluzione legittima che vada più n = 4in alto che in 2 minuti e che abbia n = 4già un aspetto ambizioso.

Ho ridotto l'utilizzo della memoria nell'implementazione iniziale, in modo che potesse gestire n = 4con un tempo sufficiente. Ma ha prodotto solo la lunghezza della sequenza, non la sequenza stessa. Controlla la cronologia delle revisioni di questo post per vedere quel codice.

Codice

Poiché i loop su matrici n-dimensionali richiedono più logica rispetto ai loop fissi, sto usando un loop fisso per la dimensione più bassa e uso solo la logica di loop generica per le dimensioni rimanenti.

#include <stdint.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>

#define N_MAX 4

int main(int argc, char* argv[]) {
    int nSeq = argc - 1;
    if (nSeq > N_MAX) {
        nSeq = N_MAX;
    }

    char** seqA = argv + 1;

    uint64_t totLen = 1;
    uint64_t lenA[N_MAX] = {0};
    uint64_t offsA[N_MAX] = {1};
    uint64_t offsSum = 0;
    uint64_t posA[N_MAX] = {0};

    for (int iSeq = 0; iSeq < nSeq; ++iSeq) {
        lenA[iSeq] = strlen(seqA[iSeq]);
        totLen *= lenA[iSeq] + 1;

        if (iSeq + 1 < nSeq) {
            offsA[iSeq + 1] = totLen;
        }

        offsSum += offsA[iSeq];
    }

    uint16_t* mat = calloc(totLen, 2);
    uint64_t idx = offsSum;

    for (;;) {
        for (uint64_t pos0 = 0; pos0 < lenA[0]; ++pos0) {
            char firstCh = seqA[0][pos0];
            int isSame = 1;
            uint16_t maxVal = mat[idx - 1];

            for (int iSeq = 1; iSeq < nSeq; ++iSeq) {
                char ch = seqA[iSeq][posA[iSeq]];
                isSame &= (ch == firstCh);

                uint16_t val = mat[idx - offsA[iSeq]];
                if (val > maxVal) {
                    maxVal = val;
                }
            }

            if (isSame) {
                mat[idx] = mat[idx - offsSum] + 1;
            } else {
                mat[idx] = maxVal;
            }

            ++idx;
        }

        idx -= lenA[0];

        int iSeq = 1;
        while (iSeq < nSeq && posA[iSeq] == lenA[iSeq] - 1) {
            posA[iSeq] = 0;
            idx -= (lenA[iSeq] - 1) * offsA[iSeq];
            ++iSeq;
        }
        if (iSeq == nSeq) {
            break;
        }

        ++posA[iSeq];
        idx += offsA[iSeq];
    }

    int score = mat[totLen - 1];

    char* resStr = malloc(score + 1);
    resStr[score] = '\0';

    for (int iSeq = 0; iSeq < nSeq; ++iSeq) {
        posA[iSeq] = lenA[iSeq] - 1;
    }

    idx = totLen - 1;
    int resIdx = score - 1;

    while (resIdx >= 0) {
        char firstCh = seqA[0][posA[0]];
        int isSame = 1;
        uint16_t maxVal = mat[idx - 1];
        int maxIdx = 0;

        for (int iSeq = 1; iSeq < nSeq; ++iSeq) {
            char ch = seqA[iSeq][posA[iSeq]];
            isSame &= (ch == firstCh);

            uint16_t val = mat[idx - offsA[iSeq]];
            if (val > maxVal) {
                maxVal = val;
                maxIdx = iSeq;
            }
        }

        if (isSame) {
            resStr[resIdx--] = firstCh;
            for (int iSeq = 0; iSeq < nSeq; ++iSeq) {
                --posA[iSeq];
            }
            idx -= offsSum;
        } else {
            --posA[maxIdx];
            idx -= offsA[maxIdx];
        }
    }

    free(mat);

    printf("score: %d\n", score);
    printf("%s\n", resStr);

    return 0;
}

Istruzioni per la corsa

Correre:

  • Salvare il codice in un file, ad es lcs.c.
  • Compilare con opzioni di ottimizzazione elevate. Ero solito:

    clang -O3 lcs.c
    

    Su Linux, proverei:

    gcc -Ofast lcs.c
    
  • Esegui da 2 a 4 sequenze fornite come argomenti della riga di comando:

    ./a.out axbycz xaybzc
    

    Virgoletta singola agli argomenti della riga di comando, se necessario, poiché l'alfabeto utilizzato per gli esempi contiene caratteri speciali della shell.

risultati

test2.she test3.shsono le sequenze di test di Dennis. Non conosco i risultati corretti, ma l'output sembra almeno plausibile.

$ ./a.out axbycz xaybzc
score: 3
abc

$ time ./test2.sh 
score: 391
16638018802020>3??3232270178;47240;24331395?<=;99=?;178675;866002==23?87?>978891>=9<6<9381992>>7030829?255>6804:=3>:;60<9384=081;0:?66=51?0;5090724<85?>>:2>7>3175?::<9199;5=0:494<5<:7100?=95<91>1887>33>67710==;48<<327::>?78>77<6:2:02:<7=5?:;>97<993;57=<<=:2=9:8=118563808=962473<::8<816<464<1==925<:5:22?>3;65=>=;27?7:5>==3=4>>5>:107:20575347>=48;<7971<<245<??219=3991=<96<??735698;62?<98928

real  0m0.012s
user  0m0.008s
sys   0m0.003s

$ time ./test3.sh 
score: 269
662:2=2::=6;738395=7:=179=96662649<<;?82?=668;2?603<74:6;2=04=>6;=6>=121>1>;3=22=3=3;=3344>0;5=7>>7:75238;559133;;392<69=<778>3?593?=>9799<1>79827??6145?7<>?389?8<;;133=505>2703>02?323>;693995:=0;;;064>62<0=<97536342603>;?92034<?7:=;2?054310176?=98;5437=;13898748==<<?4

real  0m7.218s
user  0m6.701s
sys   0m0.513s

Ci scusiamo se non è stato chiaro, ma devi stampare la LCS, non solo la sua lunghezza.
Dennis,

@Dennis che vedo. Alcune delle mie ottimizzazioni erano vane allora. Dovrò tornare a una versione che memorizza la matrice completa in modo da poter ricostruire la stringa. Quello non sarà in grado di funzionare per n = 4, ma dovrebbe comunque finire sotto i 10 secondi per n = 3. Penso di avere circa 6-7 secondi quando avevo ancora la matrice completa.
Reto Koradi,

Ancora scusa. La domanda non era molto chiara al riguardo ... Quando pubblicherai il tuo output, sarò in grado di confrontarlo con quello di BrainSteel. La lunghezza segnalata dal programma supera di 5 la lunghezza del suo output per n = 2. A proposito, ho dovuto definire N_MAXuna macro e aggiungere il flag del compilatore -std=c99per compilare il codice con GCC.
Dennis,

@Dennis Nessun problema. Ha detto che la soluzione "è una stringa", quindi avrebbe dovuto essere abbastanza chiara. Uso quasi esclusivamente C ++, quindi non sono mai sicuro di cosa sia consentito in C. Questo codice è iniziato come C ++, ma una volta ho capito che non stavo davvero usando alcuna funzionalità C ++, l'ho passato a C. clang sul mio Mac ne ero felice, ma probabilmente usa una versione C diversa per impostazione predefinita, o è solo più indulgente.
Reto Koradi,

1
@Dennis Ok, ho aggiunto la logica di traceback in modo da poter produrre la stringa. Ora impiega circa 7 secondi per n = 3.
Reto Koradi,

3

Questa risposta è attualmente interrotta a causa di un bug. Correzione presto ...

C, 2 stringhe in ~ 35 secondi

Questo è davvero un lavoro in corso (come dimostrato dall'orribile disordine), ma speriamo che dia delle buone risposte!

Il codice:

#include "stdlib.h"
#include "string.h"
#include "stdio.h"
#include "time.h"

/* For the versatility */
#define MIN_CODEPOINT 0x30
#define MAX_CODEPOINT 0x3F
#define NUM_CODEPOINT (MAX_CODEPOINT - MIN_CODEPOINT + 1)
#define CTOI(c) (c - MIN_CODEPOINT)

#define SIZE_ARRAY(x) (sizeof(x) / sizeof(*x))

int LCS(char** str, int num);
int getshared(char** str, int num);
int strcount(char* str, char c);

int main(int argc, char** argv) {
    char** str = NULL;
    int num = 0;
    int need_free = 0;
    if (argc > 1) {
        str = &argv[1];
        num = argc - 1;
    }
    else {
        scanf(" %d", &num);
        str = malloc(sizeof(char*) * num);
        if (!str) {
            printf("Allocation error!\n");
            return 1;
        }

        int i;
        for (i = 0; i < num; i++) {
            /* No string will exceed 1000 characters */
            str[i] = malloc(1001);
            if (!str[i]) {
                printf("Allocation error!\n");
                return 1;
            }

            scanf(" %1000s", str[i]);

            str[i][1000] = '\0';
        }

        need_free = 1;
    }

    clock_t start = clock();

    /* The call to LCS */
    int size = LCS(str, num);

    double dt = ((double)(clock() - start)) / CLOCKS_PER_SEC;
    printf("Size: %d\n", size);
    printf("Elapsed time on LCS call: %lf s\n", dt);

    if (need_free) {
        int i;
        for (i = 0; i < num; i++) {
            free(str[i]);
        }
        free(str);
    }

    return 0;
}

/* Some terribly ugly global variables... */
int depth, maximum, mapsize, lenmap[999999][2];
char* (strmap[999999][20]);
char outputstr[1000];

/* Solves the LCS problem on num strings str of lengths len */
int LCS(char** str, int num) {
    /* Counting variables */
    int i, j;

    if (depth + getshared(str, num) <= maximum) {
        return 0;
    }

    char* replace[num];
    char match;
    char best_match = 0;
    int earliest_set = 0;
    int earliest[num];
    int all_late;
    int all_early;
    int future;
    int best_future = 0;
    int need_future = 1;

    for (j = 0; j < mapsize; j++) {
        for (i = 0; i < num; i++)
            if (str[i] != strmap[j][i])
                break;
        if (i == num) {
            best_match = lenmap[j][0];
            best_future = lenmap[j][1];
            need_future = 0;
            if (best_future + depth < maximum || !best_match)
                goto J;
            else {
                match = best_match;
                goto K;
            }
        }
    }

    for (match = MIN_CODEPOINT; need_future && match <= MAX_CODEPOINT; match++) {

    K:
        future = 1;
        all_late = earliest_set;
        all_early = 1;
        for (i = 0; i < num; replace[i++]++) {
            replace[i] = strchr(str[i], match);
            if (!replace[i]) {
                future = 0;
                break;
            }

            if (all_early && earliest_set && replace[i] - str[i] > earliest[i])
                all_early = 0;
            if (all_late && replace[i] - str[i] < earliest[i])
                all_late = 0;
        }
        if (all_late) {
            future = 0;
        }

    I:
        if (future) {
            if (all_early || !earliest_set) {
                for (i = 0; i < num; i++) {
                    earliest[i] = (int)(replace[i] - str[i]);
                }
            }

            /* The recursive bit */
            depth++;
            future += LCS(replace, num);
            depth--;

            best_future = future > best_future ? (best_match = match), future : best_future;
        }
    }

    if (need_future) {
        for (i = 0; i < num; i++)
            strmap[mapsize][i] = str[i];
        lenmap[mapsize][0] = best_match;
        lenmap[mapsize++][1] = best_future;
    }

J:
    if (depth + best_future >= maximum) {
        maximum = depth + best_future;
        outputstr[depth] = best_match;
    }

    if (!depth) {
        mapsize = 0;
        maximum = 0;
        puts(outputstr);
    }

    return best_future;
}

/* Return the number of characters total (not necessarily in order) that the strings all share */
int getshared(char** str, int num) {
    int c, i, tot = 0, min;
    for (c = MIN_CODEPOINT; c <= MAX_CODEPOINT; c++) {
        min = strcount(str[0], c);
        for (i = 1; i < num; i++) {
            int count = strcount(str[i], c);
            if (count < min) {
                min = count;
            }
        }
        tot += min;
    }

    return tot;
}

/* Count the number of occurrences of c in str */
int strcount(char* str, char c) {
    int tot = 0;
    while ((str = strchr(str, c))) {
        str++, tot++;
    }
    return tot;
}

La funzione rilevante che esegue tutto il calcolo LCS è la funzione LCS. Il codice sopra indicherà la propria chiamata a questa funzione.

Salva come main.ce compila con:gcc -Ofast main.c -o FLCS

Il codice può essere eseguito con argomenti da riga di comando o tramite stdin. Quando si utilizza lo stdin, si aspetta un numero di stringhe seguite dalle stringhe stesse.

~ Me$ ./FLCS "12345" "23456"
2345
Size: 4
Elapsed time on LCS call: 0.000056 s

O:

~ Me$ ./FLCS
6 
2341582491248123139182371298371239813
2348273123412983476192387461293472793
1234123948719873491234120348723412349
1234129388234888234812834881423412373
1111111112341234128469128377293477377
1234691237419274737912387476371777273
1241231212323
Size: 13
Elapsed time on LCS call: 0.001594 s

Su un Mac OS X con un Intel Core i7 da 1,7 Ghz e il test case fornito da Dennis, otteniamo il seguente output per 2 stringhe:

16638018800200>3??32322701784=4240;24331395?<;=99=?;178675;866002==23?87?>978891>=9<66=381992>>7030829?25265804:=3>:;60<9384=081;08?66=51?0;509072488>2>924>>>3175?::<9199;330:494<51:>748571?153994<45<??20>=3991=<962508?7<2382?489
Size: 386
Elapsed time on LCS call: 33.245087 s

L'approccio è molto simile al mio approccio alla sfida precedente, qui . Oltre all'ottimizzazione precedente, ora controlliamo un numero totale di caratteri condivisi tra le stringhe ad ogni ricorsione e usciamo presto se non c'è modo di ottenere una sottostringa più lunga di quella già esistente.

Per ora, gestisce bene 2 stringhe ma tende a bloccarsi di più. Altri miglioramenti e una spiegazione migliore a venire!


1
Penso di aver perso qualcosa. Con 2 stringhe non è questo un classico problema di programmazione dinamica che dovrebbe risolvere circa 1000 ^ 2 passaggi? In altre parole, una frazione di secondo.

@Lembik Sì, dovrebbe. Questo metodo è stato creato per gestire molte più di 2 stringhe, ma ha finito per ridimensionarsi troppo male con la lunghezza della stringa per avere buoni risultati. Ho molti altri trucchi nella manica e se qualcuno di loro funziona davvero ... Le cose dovrebbero migliorare immensamente.
BrainSteel,

Sembra che ci sia un problema da qualche parte. Il codice di @RetoKoradi trova una sottostringa comune valida di lunghezza 391 per n = 2, mentre il codice riporta una lunghezza di 386 e stampa una stringa di lunghezza 229.
Dennis

@Dennis Umm ... Sì, sì, sì ... Oh caro. Beh, questo è imbarazzante. Ci sto lavorando :) Modificherò la risposta per riflettere il bug.
BrainSteel,
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.