Estrutura ou algoritmo de dados para encontrar rapidamente diferenças entre cadeias

19

Eu tenho uma matriz de 100.000 cordas, todas de comprimento . Eu quero comparar cada seqüência de caracteres com todas as outras para ver se existem duas seqüências diferentes por 1 caractere. No momento, enquanto adiciono cada string à matriz, eu a comparo com todas as strings já existentes na matriz, que possuem uma complexidade de tempo de .n ( n - 1 )kn(n1)2k

Existe uma estrutura de dados ou algoritmo que possa comparar seqüências de caracteres mais rapidamente do que o que eu já estou fazendo?

Algumas informações adicionais:

  • A ordem importa: abcdee xbcdediferem por 1 personagem, enquanto abcdee edcbadiferem por 4 caracteres.

  • Para cada par de cadeias que diferem por um caractere, removerei uma dessas cadeias da matriz.

  • No momento, estou procurando cadeias que diferem em apenas 1 caractere, mas seria bom se essa diferença de 1 caractere pudesse ser aumentada para, digamos, 2, 3 ou 4 caracteres. No entanto, neste caso, acho que a eficiência é mais importante do que a capacidade de aumentar o limite de diferença de caracteres.

  • k está geralmente na faixa de 20 a 40.

JGut
fonte
4
Procurar um dicionário de cadeias com 1 erro é um problema bastante conhecido, por exemplo, cs.nyu.edu/~adi/CGL04.pdf
KWillets
1
20-40mers podem usar um pouco de espaço. Você pode olhar para um filtro Bloom ( en.wikipedia.org/wiki/Bloom_filter ) para testar se seqüências de caracteres degeneradas - o conjunto de todos os meros de uma, duas ou mais substituições em um teste - são "talvez entrando" ou "definitivamente" -not-in "um conjunto de kmers. Se você receber um "talvez", compare melhor as duas cadeias para determinar se é um falso positivo ou não. Os casos "definitivamente não presente" são verdadeiros negativos que reduzirão o número geral de comparações letra a letra que você deve fazer, limitando as comparações apenas aos possíveis hits "talvez entrados".
Alex Reynolds
Se você estava trabalhando com um intervalo menor de k, pode usar um conjunto de bits para armazenar uma tabela hash de booleanos para todas as seqüências de caracteres degeneradas (por exemplo, github.com/alexpreynolds/kmer-boolean, por exemplo). Para k = 20-40, no entanto, os requisitos de espaço para um conjunto de bits são simplesmente demais.
Alex Reynolds

Respostas:

12

É possível obter pior tempo de execução de O ( n k log k ) .O(nklogk)

Vamos começar simples. Se você se preocupa com uma solução fácil de implementar que seja eficiente em muitas entradas, mas não todas, aqui está uma solução simples, pragmática e fácil de implementar, que muitas são suficientes na prática para muitas situações. No entanto, ele volta ao tempo de execução quadrático no pior dos casos.

Pegue cada string e armazene-a em uma hashtable, com chave na primeira metade da string. Em seguida, itere sobre os baldes de hashtable. Para cada par de strings no mesmo bucket, verifique se elas diferem em 1 caractere (ou seja, verifique se a segunda metade difere em 1 caractere).

Em seguida, pegue cada string e armazene-a em uma hashtable, desta vez digitada na segunda metade da string. Verifique novamente cada par de cordas no mesmo balde.

Supondo que as strings sejam bem distribuídas, o tempo de execução provavelmente será de cerca de . Além disso, se existir um par de cadeias que diferem em 1 caractere, ele será encontrado durante uma das duas passagens (como diferem em apenas 1 caractere, esse caractere diferente deverá estar na primeira ou na segunda metade da cadeia, portanto, a segunda ou a primeira metade da string deve ser a mesma). No entanto, no pior caso (por exemplo, se todas as seqüências começarem ou terminarem com os mesmos caracteres k / 2 ), isso degradará para O ( n 2 k ) tempo de execução, portanto, o pior caso de execução não é uma melhoria na força bruta .O(nk)k/2O(n2k)

Como uma otimização de desempenho, se algum depósito tiver muitas seqüências, você poderá repetir o mesmo processo recursivamente para procurar um par que diferencie um caractere. A chamada recursiva será em cadeias de comprimento .k/2

Se você se preocupa com o pior tempo de execução:

Com a otimização de desempenho acima, acredito que o pior caso de execução é .O(nklogk)

DW
fonte
3
Se strings compartilham a mesma primeira metade, o que pode muito bem acontecer na vida real, você não melhorou a complexidade. Ω(n)
einpoklum - restabelece Monica
@einpoklum, com certeza! É por isso que eu escrevi a declaração na minha segunda frase que ele volta para o tempo de execução quadrática, no pior caso, bem como a declaração na minha última frase descrevendo como conseguir pior caso complexidade se você se importa sobre o pior caso. Mas acho que talvez não tenha expressado isso com muita clareza - por isso editei minha resposta de acordo. É melhor agora? O(nklogk)
DW
15

Minha solução é semelhante à do j_random_hacker, mas usa apenas um único conjunto de hash.

Eu criaria um conjunto de strings de hash. Para cada sequência na entrada, adicione ao conjunto strings. Em cada uma dessas cadeias, substitua uma das letras por um caractere especial, não encontrado em nenhuma das cadeias. Enquanto você os adiciona, verifique se eles ainda não estão no conjunto. Se forem, você tem duas cadeias que diferem apenas em (no máximo) um caractere.k

Um exemplo com as seqüências de caracteres 'abc', 'adc'

Para abc, adicionamos '* bc', 'a * c' e 'ab *'

Para adc, adicionamos '* dc', 'a * c' e 'ad *'

Quando adicionamos 'a * c' na segunda vez que percebemos que já está no conjunto, sabemos que existem duas cadeias que diferem apenas por uma letra.

O tempo total de execução desse algoritmo é . Isso ocorre porque criamos k novas strings para todas as n strings na entrada. Para cada uma dessas cadeias, precisamos calcular o hash, que normalmente leva tempo O ( k ) .O(nk2)knO(k)

Armazenar todas as strings ocupa espaço .O(nk2)

Melhorias adicionais

Podemos melhorar ainda mais o algoritmo, não armazenando diretamente as cadeias modificadas, mas armazenando um objeto com uma referência à cadeia original e o índice do caractere que está mascarado. Desta forma, não é necessário criar todas as cordas e nós só precisamos de espaço para armazenar todos os objetos.O(nk)

Você precisará implementar uma função de hash personalizada para os objetos. Podemos tomar a implementação Java como um exemplo, consulte a documentação do Java . O java hashCode multiplica o valor unicode de cada caractere por (com k o comprimento da string ei o índice baseado em um). Observe que cada string alterada difere apenas um caractere do original. Podemos calcular facilmente a contribuição desse caractere para o código de hash. Podemos subtrair isso e adicionar nosso caractere de mascaramento. Isso leva O ( 1 ) a ser computado, o que nos permite reduzir o tempo total de execução para O ( n31kikiO(1)O(nk)

Simon Prins
fonte
4
@JollyJoker Sim, o espaço é uma preocupação com esse método. Você pode reduzir o espaço não armazenando as cadeias modificadas, mas armazenando um objeto com uma referência à cadeia e ao índice mascarado. Isso deve deixar você com espaço O (nk).
Simon Prins
Para calcular os hashes de cada string no tempo O ( k ) , acho que você precisará de uma função hash caseira especial (por exemplo, calcule o hash da cadeia original no tempo O ( k ) e , em seguida, faça o XOR com cada um dos itens excluídos caracteres em O ( 1 ) cada vez (embora essa seja provavelmente uma função de hash muito ruim de outras maneiras)). BTW, isso é bem parecido com a minha solução, mas com uma única hashtable em vez de k separadas, e substituindo um caractere por "*" em vez de excluí-lo. kO(k)O(k)O(1)k
Jrandom_hacker
@SimonPrins Com métodos equalse personalizados hashCodeque podem funcionar. Apenas criar a string no estilo a * b nesses métodos deve torná-la à prova de balas; Eu suspeito que algumas das outras respostas aqui terão problemas de colisão de hash.
jollyjoker
1
@DW Modifiquei meu post para refletir o fato de que o cálculo dos hashes leva tempo e adicionei uma solução para reduzir o tempo total de execução de volta a O ( n k ) . O(k)O(nk)
Simon Prins
1
O pior caso do @SimonPrins talvez seja nk ^ 2 devido à verificação da igualdade de String em hashset.contains quando os hashes colidem. Claro, o pior caso é quando cada corda tem o mesmo hash exata, o que exigiria um conjunto praticamente artesanal de cordas, especialmente para obter o mesmo hash para *bc, a*c, ab*. Eu me pergunto se isso poderia ser mostrado impossível?
JollyJoker
7

Eu criaria hashtables H 1 , , H k , cada uma das quais com uma string de comprimento ( k - 1 ) como chave e uma lista de números (IDs de string) como valor. A hashtable H i conterá todas as strings processadas até o momento, mas com o caractere na posição i excluída . Por exemplo, se k = 6 , então H 3 [ A B D E F ] conterá uma lista de todas as cadeias vistas até agora que têm o padrão AkH1,,Hk(k1)Hiik=6H3[ABDEF] , onde significa "qualquer caractere". Em seguida, para processar a j -ésima sequência de entrada s j :ABDEFjsj

  1. Para cada no intervalo de 1 a k : ik
    • Forme a string excluindo o i- ésimo caractere de s j .sjisj
    • Procure . Cada ID de string aqui identifica uma string original que é igual a s ou difere apenas na posição i . Produza-os como correspondências para a sequência s j . (Se você deseja excluir duplicatas exatas, torne o tipo de valor das tabelas de hash um par (ID da string, caractere excluído), para que você possa testar aqueles que tiveram o mesmo caractere excluído, como acabamos de excluir de s j .)Hi[sj]sisjsj
    • Insira em H i para consultas futuras a serem usadas.jHi

Se armazenar cada chave de hash explicitamente, em seguida, devemos usar espaço e, portanto, têm complexidade de tempo, pelo menos, que. Mas, como descrito por Simon Prins , é possível representar uma série de modificações em uma string (no caso dele descrita como alterar caracteres únicos para , no meu como exclusões) implicitamente de tal maneira que todas as chaves de hash k de uma string específica precisem apenas O ( k ) espaço, levando a O ( n k ) espaço global e abrindo a possibilidade de O ( n k )O(nk2)*kO(k)O(nk)O(nk)tempo também. Para atingir essa complexidade de tempo, precisamos de uma maneira de calcular os hashes para todas as variações de uma string de comprimento k em tempo O ( k ) : por exemplo, isso pode ser feito usando hashes polinomiais, conforme sugerido por DW (e isso é provavelmente muito melhor do que simplesmente XORing o caractere excluído com o hash da string original).kkO(k)

O truque implícito de representação de Simon Prins também significa que a "exclusão" de cada caractere não é realmente executada; portanto, podemos usar a representação habitual baseada em array de uma string sem uma penalidade de desempenho (em vez de listas vinculadas, como sugeri originalmente).

j_random_hacker
fonte
2
Ótima solução. Um exemplo de uma função de hash sob medida adequada seria um hash polinomial.
DW
Thanks @DW Talvez você possa esclarecer um pouco o que você quer dizer com "hash polinomial"? Pesquisar o termo no Google não me deu nada que parecesse definitivo. (Sinta-se livre para editar meu post diretamente se quiser.)
j_random_hacker
1
Basta ler a string como uma base número modulo p , em que p é um primo menor do que o seu tamanho hashmap, e q é uma raiz primitiva de p , e q é mais do que o tamanho do alfabeto. É chamado de "hash polinomial" porque é como avaliar o polinômio cujos coeficientes são dados pela string em q . Vou deixar isso como um exercício para descobrir como calcular todos os hashes desejados no tempo O ( k ) . Observe que essa abordagem não é imune a um adversário, a menos que você escolha aleatoriamente ambos p , q satisfazendo as condições desejadas.qppqpqqO(k)p,q
user21820
1
Acho que essa solução pode ser refinada ainda mais, observando que apenas uma das k tabelas de hash precisa existir a qualquer momento, reduzindo assim o requisito de memória.
Michael Kay
1
@ MichaelKay: Isso não funcionará se você quiser calcular os hashes das possíveis alterações de uma string no tempo O ( k ) . Você ainda precisa armazená-los em algum lugar. Portanto, se você marcar apenas uma posição por vez, levará k vezes, contanto que verifique todas as posições usando k vezes quantas entradas de hashtable. kO(k)kk
user21820
2

Aqui está uma abordagem hashtable mais robusta do que o método polinomial-hash. Em primeiro lugar gerar inteiros positivos aleatórios r 1 .. k que são primos entre si para a tabela de dispersão tamanho M . Ou seja, 0 r i < M . Em seguida, cada corda de hash x 1 .. k a ( Σ k i = 1 x i r i ) mod M . Não há quase nada que um adversário possa fazer para causar colisões muito desiguais, pois você gera r 1 .. k em tempo de execução e assim como kkr1..kM0ri<Mx1..k(i=1kxiri)modMr1..kkaumenta a probabilidade máxima de colisão de um dado par de cadeias distintas passa rapidamente para . Também é óbvio como calcular em O ( k ) tempo todos os hashes possíveis para cada string com um caractere alterado.1/MO(k)

Se você realmente deseja garantir um hash uniforme, pode gerar um número natural aleatório menor que M para cada par ( i , c ) para i de 1 a ke para cada caractere c , e então hash de cada string x 1 .. k a ( k i = 1 r ( i , x i ) ) mod Mr(i,c)M(i,c)i1kcx1..k(i=1kr(i,xi))modM. Então a probabilidade de colisão de um dado par de cadeias distintas é exactamente . Essa abordagem é melhor se o seu conjunto de caracteres for relativamente pequeno comparado a n .1/Mn

user21820
fonte
2

Muitos dos algoritmos postados aqui usam bastante espaço nas tabelas de hash. Aqui está um algoritmo simples de tempo de execução armazenamento auxiliar O ( ( n lg n ) k 2 ) .O(1)O((nlgn)k2)

O truque é usar , que é um comparador entre dois valores um e b que retorna verdadeiro se um < b (lexicograficamente), ignorando o k th personagem. Então o algoritmo é o seguinte.Ck(a,b)aba<bk

Primeiro, basta classificar as strings regularmente e fazer uma varredura linear para remover as duplicatas.

Então, para cada :k

  1. Classifique as seqüências com como comparador.Ck

  2. As strings que diferem apenas em agora são adjacentes e podem ser detectadas em uma varredura linear.k

orlp
fonte
1

Duas cadeias de comprimento k , diferindo em um caractere, compartilham um prefixo de comprimento l e um sufixo de comprimento m, de modo que k = l + m + 1 .

A resposta de Simon Prins codifica isso armazenando todas as combinações de prefixo / sufixo explicitamente, ou abcseja *bc, torna-se a*ce ab*. Isso é k = 3, l = 0,1,2 em = 2,1,0.

Como valarMorghulis aponta, você pode organizar as palavras em uma árvore de prefixos. Há também a árvore de sufixos muito semelhante. É bastante fácil aumentar a árvore com o número de nós de folhas abaixo de cada prefixo ou sufixo; isso pode ser atualizado em O (k) ao inserir uma nova palavra.

O motivo pelo qual você deseja essas contagens de irmãos é saber, com uma nova palavra, se deseja enumerar todas as cadeias com o mesmo prefixo ou se deve enumerar todas as cadeias com o mesmo sufixo. Por exemplo, para "abc" como entrada, os possíveis prefixos são "", "a" e "ab", enquanto os sufixos correspondentes são "bc", "c" e "". Como é óbvio, para sufixos curtos, é melhor enumerar irmãos na árvore de prefixos e vice-versa.

Como o @einpoklum aponta, certamente é possível que todas as strings compartilhem o mesmo prefixo k / 2 . Isso não é um problema para essa abordagem; a árvore do prefixo será linear até a profundidade k / 2, com cada nó até a profundidade k / 2 sendo o ancestral de 100.000 nós de folha. Como resultado, a árvore de sufixos será usada até (k / 2-1) de profundidade, o que é bom porque as strings precisam diferir em seus sufixos, uma vez que compartilham prefixos.

[edit] Como otimização, depois de determinar o prefixo único mais curto de uma string, você sabe que se houver um caractere diferente, ele deverá ser o último caractere do prefixo e você teria encontrado a duplicata quase quando verificando um prefixo mais curto. Portanto, se "abcde" tiver um prefixo único mais curto "abc", isso significa que existem outras strings que começam com "ab?" mas não com "abc". Ou seja, se eles diferissem em apenas um personagem, esse seria o terceiro personagem. Você não precisa mais verificar "abc? E".

Pela mesma lógica, se você achar que "cde" é um sufixo mais curto e exclusivo, saberá que precisa verificar apenas o prefixo comprimento 2 "ab" e não o prefixo comprimento 1 ou 3.

Observe que esse método funciona apenas para diferenças de exatamente um caractere e não generaliza para 2 diferenças de caracteres. Ele confia em um caractere, sendo a separação entre prefixos e sufixos idênticos.

MSalters
fonte
Você está sugerindo que, para cada string e cada 1 i k , encontramos o nó P [ s 1 , , s i - 1 ] correspondente ao comprimento - ( i - 1 ) no prefixo trie, e o nó S [ s i + 1 , , s k ] correspondente ao comprimento- ( k - i - 1 )s1EukP[s1,...,sEu-1](Eu-1)S[sEu+1,...,sk](k-Eu-1)sufixo no trie do sufixo (cada um leva o tempo amortizado ) e compare o número de descendentes de cada um, escolhendo o que tiver menos descendentes e, em seguida, "investigando" o resto da string nesse trie? O(1)
Jrandom_hacker
1
Qual é o tempo de execução da sua abordagem? Parece-me que, na pior das hipóteses, pode ser quadrático: considere o que acontece se cada sequência começar e terminar com os mesmos caracteres . k/4
DW
A idéia de otimização é inteligente e interessante. Você tinha em mente uma maneira específica de fazer a verificação de mtaches? Se o "abcde" tiver o menor prefixo único "abc", isso significa que deveríamos procurar outra string do formato "ab? De". Você tinha em mente uma maneira específica de fazer isso, que seria eficiente? Qual é o tempo de execução resultante?
DW
@DW: A idéia é que, para encontrar seqüências de caracteres no formato "ab? De", verifique a árvore de prefixos quantos nós de folhas existem abaixo de "ab" e na árvore de sufixos quantos nós existem em "de" e escolha o menor dos dois a enumerar. Quando todas as strings começam e terminam com os mesmos caracteres k / 4; isso significa que os primeiros nós k / 4 em ambas as árvores têm um filho cada. E sim, toda vez que você precisar dessas árvores, elas precisam ser atravessadas, o que é um passo O (n * k).
MSalters
Para verificar se há uma sequência do formato "ab? De" no prefixo trie, basta chegar ao nó para "ab" e, em seguida, para cada um de seus filhos , verifique se o caminho "de" existe abaixo de v . Ou seja, não se preocupe em enumerar outros nós nessas sub-tentativas. Isso leva tempo O ( a h ) , onde a é o tamanho do alfabeto e h é a altura do nó inicial na árvore. h é O ( k ) , portanto, se o tamanho do alfabeto é O ( n ), então é de fato O ( n k )vvO(umah)umahhO(k)O(n)O(nk)tempo geral, mas alfabetos menores são comuns. O número de filhos (não descendentes) é importante, assim como a altura.
Jrandom_hacker
1

Armazenar strings em buckets é uma boa maneira (já existem respostas diferentes descrevendo isso).

Uma solução alternativa poderia ser armazenar seqüências de caracteres em uma lista classificada . O truque é classificar por um algoritmo de hash sensível à localidade . Este é um algoritmo de hash que produz resultados semelhantes quando a entrada é semelhante [1].

Cada vez que você deseja investigar uma série, você pode calcular seu hash e pesquisar a posição desse hash na sua lista ordenada (tomando para arrays ou O ( n ) para listas ligadas). Se você achar que os vizinhos (considerando todos os vizinhos próximos, não apenas aqueles com um índice de +/- 1) dessa posição são semelhantes (desativados por um caractere), você encontrou a sua correspondência. Se não houver seqüências semelhantes, você poderá inserir a nova sequência na posição encontrada (que utiliza O ( 1 ) para listas vinculadas e O ( n ) para matrizes).O(euog(n))O(n)O(1)O(n)

Um possível algoritmo de hash sensível à localidade pode ser o Nilsimsa (com a implementação de código aberto disponível, por exemplo, em python ).

[1]: Observe que frequentemente algoritmos de hash, como SHA1, são projetados para o contrário: produzindo hashes muito diferentes para entradas semelhantes, mas não iguais.

Isenção de responsabilidade: para ser sincero, eu implementaria pessoalmente uma das soluções de bucket aninhadas / organizadas em árvore para um aplicativo de produção. No entanto, a ideia da lista classificada me pareceu uma alternativa interessante. Observe que esse algoritmo depende muito do algoritmo de hash escolhido. Nilsimsa é um algoritmo que encontrei - existem muitos outros (por exemplo, TLSH, Ssdeep e Sdhash). Não verifiquei se o Nilsimsa funciona com meu algoritmo descrito.

tessi
fonte
1
Idéia interessante, mas acho que precisaríamos ter alguns limites de quão distantes dois valores de hash podem estar quando suas entradas diferem em apenas 1 caractere - depois varre tudo dentro desse intervalo de valores de hash, em vez de apenas vizinhos. (É impossível ter uma função de hash que produza valores de hash adjacentes para todos os pares possíveis de cadeias que diferem por 1 caractere. Considere as cadeias de comprimento 2 em um alfabeto binário: 00, 01, 10 e 11. Se h (00) for adjacente a ambos H (10) e H (01), então ele deve estar entre eles, caso em que h (11) não pode estar adjacente a ambos, e vice-versa).
j_random_hacker
Olhar para os vizinhos não é suficiente. Considere a lista abcd, acef, agcd. Existe um par correspondente, mas seu procedimento não o encontrará, pois o abcd não é um vizinho do agcd.
DW
Vocês dois estão certos! Com vizinhos, eu não quis dizer apenas "vizinhos diretos", mas pensei em "um bairro" de posições próximas. Não especifiquei quantos vizinhos precisam ser vistos, pois isso depende do algoritmo de hash. Mas você está certo, eu provavelmente deveria anotar isso na minha resposta. Obrigado :)
tessi
1
"LSH ... itens semelhantes são mapeados para os mesmos" baldes "com alta probabilidade" - já que é um algoritmo de probabilidade, o resultado não é garantido. Portanto, depende do TS se ele precisa de 100% de solução ou 99,9% é suficiente.
Bulat
1

Pode-se obter a solução no tempo e no espaço O ( n k ) usando matrizes de sufixos aprimoradas ( matriz de sufixo juntamente com a matriz LCP ) que permitem consultas LCP (Longest Common Prefix) em tempo constante (ou seja, dados dois índices de uma string, qual é o tamanho do prefixo mais longo dos sufixos começando nesses índices). Aqui, poderíamos tirar proveito do fato de que todas as strings têm o mesmo comprimento. Especificamente,O(nk+n2)O(nk)

  1. Crie a matriz de sufixos aprimorada de todas as seqüências concatenadas juntas. Seja X = x 1 .n onde x i , 1 i n é uma sequência na coleção. Construir a matriz de sufixo e variedade LCP para X .X=x1.x2.x3....xnxEu,1EunX

  2. Agora, cada começa na posição ( i - 1 ) k na indexação baseada em zero. Para cada sequência x i , use LCP com cada sequência x j , de forma que j <xEu(Eu-1)kxEuxj . Se o LCP ultrapassar o final de x j, então x i = x j . Caso contrário, existe um desfasamento (digamos x i [ p ] x j [ P ]j<EuxjxEu=xjxEu[p]xj[p]); nesse caso, pegue outro LCP começando nas posições correspondentes após a incompatibilidade. Se o segundo LCP ultrapassar o final de , x i e x j diferem apenas em um caractere; caso contrário, existem mais de uma incompatibilidade.xjxEuxj

    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)
                  }
                }
            }
        }
    }
    

Você pode usar a biblioteca SDSL para criar a matriz de sufixos em formato compactado e responder às consultas do LCP.

Análise: A construção da matriz de sufixos aprimorada é linear no comprimento de ou seja, O ( n k ) . Cada consulta LCP leva tempo constante. Assim, o tempo de consulta é O ( n 2 ) .XO(nk)O(n2)

O(nk+qn2)q

j<Euj

Ritu Kundu
fonte
Posso dizer isso O(kn2)k
O(nk+n2)O(kn2)O(1)
O que quero dizer é que k = 20..40 para o autor da pergunta e a comparação de cadeias pequenas requerem apenas alguns ciclos de CPU; portanto, provavelmente não existe diferença prática entre força bruta e sua abordagem.
Bulat
1

O(nk)**bcdea*cde

Você também pode usar essa abordagem para dividir o trabalho entre vários núcleos de CPU / GPU.

Bulat
fonte
n=100,000k40.O(nk)
0

Esta é uma versão curta da resposta do @SimonPrins que não envolve hashes.

Supondo que nenhuma de suas seqüências contenha um asterisco:

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

Uma solução alternativa com uso implícito de hashes no Python (não pode resistir à beleza):

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)
Bananach
fonte
kO(nk)
O(n2)
0

Aqui está minha opinião sobre o localizador de incompatibilidades 2+. Observe que, neste post, considero cada string como uma substring circular, fe, de comprimento 2 no índicek-1 consiste no símbolo str[k-1]seguido por str[0]. E a substring de comprimento 2 no índice -1é a mesma!

Mkmeuen(k,M)=k/M-1Mk=20M=4abcd*efgh*ijkl*mnop*

Agora, o algoritmo para pesquisar todas as incompatibilidades até os Msímbolos entre as cadeias de ksímbolos:

  • para cada i de 0 a k-1
    • divida todas as strings em grupos por str[i..i+L-1], where L = mlen(k,M). Fe se L=4e você tiver um alfabeto de apenas 4 símbolos (do DNA), isso criará 256 grupos.
    • Grupos menores que ~ 100 strings podem ser verificados com o algoritmo de força bruta
    • Para grupos maiores, devemos executar a divisão secundária:
      • Remova de cada sequência nos Lsímbolos de grupo que já correspondemos
      • para cada j de i-L + 1 a kL-1
        • divida todas as strings em grupos por str[i..i+L1-1], where L1 = mlen(k-L,M). Fe se k=20, M=4, alphabet of 4 symbols, então L=4e L1=3, isso fará 64 grupos.
        • o restante é deixado como exercício para o leitor: D

Por que não começamos jdo zero? Como já criamos esses grupos com o mesmo valor i, então trabalhe comj<=i-L será exatamente equivalente ao trabalho com os valores iej trocados.

Otimizações adicionais:

  • Em todas as posições, considere também as strings str[i..i+L-2] & str[i+L]. Isso apenas dobra a quantidade de empregos criados, mas permite aumentarL em 1 (se minha matemática estiver correta). Portanto, fe, em vez de 256 grupos, você dividirá os dados em 1024 grupos.
  • eu[Eu]*0..k-1M-1k-1
Bulat
fonte
0

Eu trabalho todos os dias inventando e otimizando algos; portanto, se você precisar de todo o desempenho, esse é o plano:

  • Verifique com *cada posição independentemente, ou seja, em vez de n*kvariantes de sequência de processamento de tarefa única - inicie ktarefas independentes cada verificação de nsequência. Você pode espalhar estesk trabalhos entre vários núcleos de CPU / GPU. Isso é especialmente importante se você verificar 2 ou mais diferenças de caracteres. Um tamanho menor de trabalho também melhorará a localidade do cache, o que por si só pode tornar o programa 10x mais rápido.
  • Se você for usar tabelas de hash, use sua própria implementação empregando análise linear e fator de carga de ~ 50%. É rápido e fácil de implementar. Ou use uma implementação existente com endereçamento aberto. As tabelas de hash STL são lentas devido ao uso de encadeamento separado.
  • Você pode tentar pré-filtrar os dados usando o filtro Bloom de 3 estados (distinguindo 0/1/1 + ocorrências) como proposto por @AlexReynolds.
  • Para cada i de 0 a k-1, execute o seguinte trabalho:
    • Gere estruturas de 8 bytes contendo o hash de 4-5 bytes de cada sequência (com *a i-ésima posição) e o índice da sequência e, em seguida, classifique-os ou crie uma tabela de hash a partir desses registros.

Para classificação, você pode tentar a seguinte combinação:

  • A primeira passagem é a classificação do MSD Radix de 64 a 256 maneiras usando o truque TLB
  • segunda passagem é a classificação do MSD radix de 256 a 1024 maneiras sem o truque do TLB (total de 64K maneiras)
  • terceira passagem é o tipo de inserção para corrigir as inconsistências restantes
Bulat
fonte