Localizador de Subseqüência Comum Mais Longo e Mais Rápido

11

Sua tarefa é resolver o problema de Subseqüência Comum Mais Longa para n seqüências de comprimento 1000.

A solução válida para o problema LCS para duas ou mais cadeias S 1 , ... S n é qualquer string T de comprimento máximo de tal forma que os personagens de T aparecer em todos S i , na mesma ordem como em T .

Note-se que T não tem que ser uma sub seqüência de S i .

Já resolvemos esse problema na menor quantidade de código . Desta vez, o tamanho não importa.

Exemplo

As cadeias axbycze xaybzctêm 8 subsequências comuns de comprimento 3:

abc abz ayc ayz xbc xbz xyc xyz

Qualquer uma dessas seria uma solução válida para o problema do LCS.

Detalhes

Escreva um programa completo que resolva o problema do LCS, conforme explicado acima, cumprindo as seguintes regras:

  • A entrada consistirá em duas ou mais cadeias de comprimento 1000, consistindo em caracteres ASCII com pontos de código entre 0x30 e 0x3F.

  • Você precisa ler a entrada de STDIN.

    Você tem duas opções para o formato de entrada:

    • Cada sequência (incluindo a última) é seguida por um avanço de linha.

    • As cadeias são encadeadas sem separador e sem avanço de linha à direita.

  • O número de seqüências de caracteres será passado como um parâmetro de linha de comando para o seu programa.

  • Você deve escrever a saída, ou seja, qualquer uma das soluções válidas para o LCS, para STDOUT, seguida por um avanço de linha.

  • Seu idioma de escolha deve ter um compilador / intérprete gratuito (como na cerveja) para o meu sistema operacional (Fedora 21).

  • Se você precisar de sinalizadores de compilador ou de um intérprete específico, mencione-o em sua postagem.

Pontuação

Executarei seu código com 2, 3 etc. seqüências de caracteres até que demore mais de 120 segundos para imprimir uma solução válida. Isso significa que você tem 120 segundos para cada valor de n .

A maior quantidade de strings para as quais seu código terminou no tempo é a sua pontuação.

No caso de uma pontuação empatada de n , a finalização que resolveu o problema de n strings no menor tempo será declarada a vencedora.

Todos os envios serão cronometrados na minha máquina (Intel Core i7-3770, 16 GiB de RAM, sem troca).

As n seqüências do (n-1) ésimo teste serão geradas chamando rand n(e eliminando os feeds de linha, se solicitado), onde randé definido da seguinte maneira:

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

A chave está 0no código acima, mas reservo-me o direito de alterá-lo para um valor não revelado se suspeitar que alguém (parcialmente) codifique a saída.

Dennis
fonte
Podemos lançar exceções?
HyperNeutrino
@JamesSmith Desde que a saída esteja correta, com certeza.
Dennis
Desde que eu estou lendo com bufferedreader, posso lançar ioexception public static void main(...)?
HyperNeutrino
@ JamesSmith Eu realmente não conheço Java, então não sei o que é isso, mas não se preocupe com exceções.
Dennis
4
@JamesSmith Como o tamanho do código não importa para esse desafio, você não pode simplesmente capturar as exceções?
Reto Koradi 8/08/15

Respostas:

5

C, n = 3 em ~ 7 segundos

Algoritmo

O algoritmo é uma generalização direta da solução de programação dinâmica padrão para nseqüências. Para 2 strings Ae B, a recorrência padrão é assim:

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

Para 3 cordas A, B, Ceu 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

O código implementa essa lógica para valores arbitrários de n.

Eficiência

A complexidade do meu código é O (s ^ n), com so comprimento das strings. Com base no que descobri, parece que o problema está completo. Portanto, embora o algoritmo publicado seja muito ineficiente para valores maiores de n, talvez não seja possível obter um desempenho massivamente melhor. A única coisa que vi foram algumas abordagens que melhoram a eficiência de pequenos alfabetos. Como o alfabeto é moderadamente pequeno aqui (16), isso poderia levar a uma melhoria. Eu ainda prevejo que ninguém encontrará uma solução legítima que seja mais alta que n = 4em 2 minutos e que n = 4já pareça ambiciosa.

Reduzi o uso de memória na implementação inicial, para que ele pudesse lidar n = 4com tempo suficiente. Mas apenas produziu o comprimento da sequência, não a própria sequência. Verifique o histórico de revisões desta postagem para ver esse código.

Código

Como os loops sobre matrizes n-dimensionais exigem mais lógica do que os loops fixos, estou usando um loop fixo para a dimensão mais baixa e use apenas a lógica de loop genérica para as dimensões restantes.

#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;
}

Instruções de corrida

Para correr:

  • Salve o código em um arquivo, por exemplo lcs.c.
  • Compile com opções de alta otimização. Eu usei:

    clang -O3 lcs.c
    

    No Linux, eu tentaria:

    gcc -Ofast lcs.c
    
  • Execute com 2 a 4 seqüências fornecidas como argumentos de linha de comando:

    ./a.out axbycz xaybzc
    

    Coloque aspas simples nos argumentos da linha de comando, se necessário, pois o alfabeto usado para os exemplos contém caracteres especiais do shell.

Resultados

test2.she test3.shsão as sequências de teste de Dennis. Não sei os resultados corretos, mas a saída parece pelo menos plausível.

$ ./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
Reto Koradi
fonte
Desculpas se isso não estava claro, mas você precisa imprimir o LCS, não apenas seu tamanho.
Dennis
@ Dennis eu vejo. Algumas das minhas otimizações foram em vão então. Vou ter que voltar para uma versão que armazena a matriz completa para que eu possa reconstruir a string. Isso não poderá ser executado por n = 4, mas ainda deverá terminar abaixo de 10 segundos para n = 3. Eu acho que tinha cerca de 6 a 7 segundos quando ainda tinha a matriz completa.
Reto Koradi 14/08/2015
Mais uma vez, desculpe. A questão não era muito clara sobre isso ... Quando você postar sua saída, poderei compará-la à da BrainSteel. O comprimento que o seu programa relata excede o comprimento da saída dele em 5 para n = 2. A propósito, eu tive que definir N_MAXcomo uma macro e adicionar o sinalizador do compilador -std=c99para compilar seu código com o GCC.
Dennis
@ Dennis Sem problemas. Ele disse que a solução "é uma string", de modo que deveria ter sido suficientemente clara. Eu quase exclusivamente uso C ++, então nunca tenho certeza do que é permitido no C. Esse código começou como C ++, mas depois que percebi que não estava usando nenhum recurso de C ++, mudei para C. clang no meu Mac ficou satisfeito com isso, mas provavelmente usa uma versão C diferente por padrão ou é apenas mais branda.
Reto Koradi
1
@ Dennis Ok, eu adicionei a lógica do traceback para que eu possa produzir a string. Leva cerca de 7 segundos agora para n = 3.
Reto Koradi 15/08/2015
3

Esta resposta está quebrada no momento devido a um erro. A corrigir em breve ...

C, 2 cordas em ~ 35 segundos

Este é um trabalho em andamento (como mostra a horrível confusão), mas espero que acenda algumas boas respostas!

O código:

#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;
}

A função relevante que realiza todo o cálculo LCS é a função LCS. O código acima cronometrará sua própria chamada para esta função.

Salvar como main.ce compilar com:gcc -Ofast main.c -o FLCS

O código pode ser executado com argumentos de linha de comando ou através do stdin. Ao usar stdin, espera-se um número de cadeias seguidas pelas próprias cadeias.

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

Ou:

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

Em uma caixa do Mac OS X com um Intel Core i7 de 1,7 Ghz e o caso de teste fornecido por Dennis, obtemos a seguinte saída para 2 strings:

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

A abordagem é muito semelhante à minha abordagem ao desafio anterior, aqui . Além da otimização anterior, agora verificamos um número total de caracteres compartilhados entre as strings a cada recursão e saímos mais cedo, se não houver maneira de obter uma substring mais longa do que o que já existe.

Por enquanto, ele lida com duas seqüências de caracteres, mas tende a ficar travado com mais. Mais melhorias e uma melhor explicação para vir!

BrainSteel
fonte
1
Eu acho que perdi alguma coisa. Com 2 strings, este não é um problema clássico de programação dinâmica que deve demorar cerca de 1000 ^ 2 etapas para resolver? Em outras palavras, uma fração de segundo.
@ Lembik Sim, deveria. Esse método foi criado para lidar com muito mais do que 2 strings, mas acabou sendo muito pouco dimensionado com o comprimento da string para obter bons resultados. Tenho muitos truques na manga e, se algum deles realmente funcionar ... As coisas devem melhorar imensamente.
BrainSteel
Parece haver um problema em algum lugar. @ Código de RetoKoradi encontra um comum válida substring de comprimento 391 para n = 2, enquanto o seu código relata um comprimento de 386 e imprime uma cadeia de comprimento 229.
Dennis
@Dennis Umm ... Sim, sim, sim ... Oh, querida. Bem, isso é constrangedor. Estou trabalhando nisso :) Vou editar a resposta para refletir o bug.
precisa saber é o seguinte