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 )
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:
abcde
exbcde
diferem por 1 personagem, enquantoabcde
eedcba
diferem 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.
está geralmente na faixa de 20 a 40.
Respostas:
É possível obter pior tempo de execução de O ( n k log k ) .O ( n k logk )
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 ( n k ) k / 2 O ( 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 ( n k logk )
fonte
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 ( n ∗ k2) k n O ( k )
Armazenar todas as strings ocupa espaço .O ( n ∗ k2)
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 ( n ∗ k )
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 ( n31k - i k Eu O ( 1 ) O ( n ∗ k )
fonte
equals
e personalizadoshashCode
que 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.*bc
,a*c
,ab*
. Eu me pergunto se isso poderia ser mostrado impossível?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 Ak H1, ... , Hk ( k - 1 ) HEu Eu k = 6 H3[ A B D EF] , onde ⋅ significa "qualquer caractere". Em seguida, para processar a j -ésima sequência de entrada s j :A B ⋅ D EF ⋅ j sj
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 ( n k2) k O ( k ) O ( n k ) O ( n k ) 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).k k O ( 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).
fonte
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 kk r1 .. k M 0 ≤ rEu<M x1..k (∑ki=1xiri)modM r1..k k aumenta 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/M O(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) i 1 k c x1..k (∑ki=1r(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/M n
fonte
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) a b a<b k
Primeiro, basta classificar as strings regularmente e fazer uma varredura linear para remover as duplicatas.
Então, para cada :k
Classifique as seqüências com como comparador.Ck
As strings que diferem apenas em agora são adjacentes e podem ser detectadas em uma varredura linear.k
fonte
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
abc
seja*bc
, torna-sea*c
eab*
. 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.
fonte
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 ( l o g( 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.
fonte
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 ( n k + n2) O ( n k )
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. . . . xn xEu, ∀ 1 ≤ i ≤ n X
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 ( i - 1 ) k xEu xj . 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 < i xj xEu= xj xEu[ 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.xj xEu xj
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 ) .X O ( n k ) O ( n2)
fonte
k
*
*bcde
a*cde
Você também pode usar essa abordagem para dividir o trabalho entre vários núcleos de CPU / GPU.
fonte
Esta é uma versão curta da resposta do @SimonPrins que não envolve hashes.
Supondo que nenhuma de suas seqüências contenha um asterisco:
Uma solução alternativa com uso implícito de hashes no Python (não pode resistir à beleza):
fonte
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 índice
k-1
consiste no símbolostr[k-1]
seguido porstr[0]
. E a substring de comprimento 2 no índice-1
é a mesma!M
k
M
k=20
M=4
abcd*efgh*ijkl*mnop*
Agora, o algoritmo para pesquisar todas as incompatibilidades até os
M
símbolos entre as cadeias dek
símbolos:str[i..i+L-1]
, whereL = mlen(k,M)
. Fe seL=4
e você tiver um alfabeto de apenas 4 símbolos (do DNA), isso criará 256 grupos.L
símbolos de grupo que já correspondemosstr[i..i+L1-1]
, whereL1 = mlen(k-L,M)
. Fe sek=20, M=4, alphabet of 4 symbols
, entãoL=4
eL1=3
, isso fará 64 grupos.Por que não começamos
j
do zero? Como já criamos esses grupos com o mesmo valori
, então trabalhe comj<=i-L
será exatamente equivalente ao trabalho com os valores iej trocados.Otimizações adicionais:
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.*
0..k-1
M-1
k-1
fonte
Eu trabalho todos os dias inventando e otimizando algos; portanto, se você precisar de todo o desempenho, esse é o plano:
*
cada posição independentemente, ou seja, em vez den*k
variantes de sequência de processamento de tarefa única - iniciek
tarefas independentes cada verificação den
sequê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.*
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:
fonte