문자열 간의 차이점을 빠르게 찾을 수있는 데이터 구조 또는 알고리즘


19

길이가 인 100,000 개의 문자열 배열이 k있습니다. 각 문자열을 다른 모든 문자열과 비교하여 두 문자열이 1 문자 씩 다른지 확인하고 싶습니다. 지금은 각 문자열을 배열에 추가함에 따라 배열에 이미있는 모든 문자열에 대해 확인하고 있으며 시간 복잡도는 (1)2케이.

내가 이미하고있는 것보다 문자열을 서로 빠르게 비교할 수있는 데이터 구조 또는 알고리즘이 있습니까?

몇 가지 추가 정보 :

  • 주문 사항 : abcdexbcde동안, 1 개 문자 차이 abcdeedcba4 자에 의해 다릅니다.

  • 한 문자 씩 다른 문자열 쌍마다 배열에서 해당 문자열 중 하나를 제거합니다.

  • 지금은 1 문자 만 다른 문자열을 찾고 있지만 1 문자 차이를 2, 3 또는 4 문자로 늘릴 수 있다면 좋을 것입니다. 그러나이 경우 문자 차이 제한을 늘리는 기능보다 효율성이 더 중요하다고 생각합니다.

  • 케이 는 일반적으로 20-40의 범위에 있습니다.


4
꽤 잘 알려진 문제가 1 오류가있는 문자열 사전입니다 검색, 예를 들어 cs.nyu.edu/~adi/CGL04.pdf
KWillets

1
20-40 명의 사람들은 상당한 공간을 사용할 수 있습니다. 블룸 필터 ( en.wikipedia.org/wiki/Bloom_filter )를보고 문자열이 퇴화되는 경우 (테스트 머에서 하나, 둘 이상의 대체에서 온 모든 머 세트)가 "어쩌면"또는 "확정"인지 테스트 할 수 있습니다. kmers 집합이 아닙니다. "아마도"인 경우 두 문자열을 더 비교하여 오 탐지 여부를 확인하십시오. "확실하지 않은"사례는 잠재적 인 "아마도"히트로만 비교를 제한하여 문자 별 비교의 전체 수를 줄이는 진정한 부정입니다.
Alex Reynolds이 (가)

더 작은 범위의 k로 작업하는 경우 비트 세트 를 사용 하여 모든 퇴화 문자열 (예 : 장난감 예제의 경우 github.com/alexpreynolds/kmer-boolean)에 대한 부울 해시 테이블을 저장할 수 있습니다 . k = 20-40의 경우 비트 세트에 필요한 공간이 너무 많습니다.
Alex Reynolds이 (가)

답변:


12

O ( n k log k ) 를 달성 할 수 있습니다O(nklogk)최악의 경우 실행 시간을 있습니다.

간단하게 시작합시다. 많은 입력에서 효율적일 수있는 솔루션을 구현하기 쉽지만 모두가 아닌 솔루션을 염두에 둔다면 여기에는 많은 상황에서 많은 사람들이 실제로 사용하기에 간단하고 실용적이며 구현하기 쉬운 솔루션이 있습니다. 그러나 최악의 경우 2 차 실행 시간으로 돌아갑니다.

각 문자열을 가져 와서 문자열의 첫 절반을 키로 해시 테이블에 저장하십시오. 그런 다음 해시 테이블 버킷을 반복합니다. 동일한 버킷에있는 각 문자열 쌍에 대해 1 문자가 다른지 확인합니다 (즉, 두 번째 절반이 1 문자가 다른지 확인).

그런 다음 각 문자열을 가져 와서 해시 테이블에 저장하십시오. 이번에는 두 번째 키를 사용하십시오. 문자열의 절반을. 동일한 버킷에서 각 문자열 쌍을 다시 확인하십시오.

스트링이 잘 분산되어 있다고 가정하면, 실행 시간은 약 입니다. 또한 한 문자 씩 다른 한 쌍의 문자열이있는 경우 두 패스 중 하나에서 발견됩니다 (한 문자 만 다르므로 다른 문자는 문자열의 첫 번째 또는 두 번째 절반에 있어야합니다. 따라서 문자열의 후반 또는 전반은 동일해야합니다). 그러나 최악의 경우 (예를 들어, 모든 문자열이 동일한 k / 2 문자로 시작하거나 끝나는 경우 ) 이것은 O ( n 2 k ) 실행 시간으로 저하 되므로 최악의 실행 시간은 무차별 대입으로 향상되지 않습니다 .O(nk)k/2O(n2k)

성능 최적화로 버킷에 문자열이 너무 많은 경우 동일한 프로세스를 반복적으로 반복하여 한 문자 씩 다른 쌍을 찾을 수 있습니다. 재귀 호출은 길이가 문자열에 있습니다 .k/2

최악의 실행 시간이 걱정되는 경우 :

위의 성능 최적화를 통해 최악의 실행 시간은 라고 생각합니다 .O(nklogk)


3
경우 문자열이 잘 실제 생활에서 일어날 수있는 동일한 상반기를 공유, 당신은 복잡성을 개선하지 않았습니다. Ω(n)
einpoklum-복원 Monica Monica

@einpoklum, 확실합니다! 그렇기 때문에 두 번째 문장으로 최악의 경우 2 차 실행 시간으로 돌아가는 진술을 작성하고 마지막 문장에서 성명을 통해 최악의 경우 복잡성 을 달성하는 방법에 대해 설명했습니다. 최악의 경우에 대해. 그러나 나는 그것을 매우 명확하게 표현하지 않았을 것입니다. 그래서 나는 그에 따라 답변을 편집했습니다. 지금은 더 나아 졌습니까? O(nklogk)
DW

15

내 솔루션은 j_random_hacker와 유사하지만 단일 해시 세트 만 사용합니다.

해시 문자열 집합을 만들 것입니다. 입력의 각 문자열에 대해 설정된 문자열에 추가하십시오 . 이러한 각 문자열에서 문자 중 하나를 특수 문자로 바꾸십시오 (문자열에서 찾을 수 없음). 추가하는 동안 세트에 아직 없는지 확인하십시오. 그것들이 그렇다면 하나의 문자 만 다른 두 개의 문자열이 있습니다.케이

문자열 'abc', 'adc'가있는 예

abc의 경우 '* bc', 'a * c'및 'ab *'를 추가합니다

adc의 경우 '* dc', 'a * c'및 'ad *'를 추가합니다

우리가 두 번째로 'a * c'를 추가하면 이미 세트에 있음을 알기 때문에 한 문자 만 다른 두 개의 문자열이 있음을 알 수 있습니다.

이 알고리즘의 총 실행 시간은 . 입력의 모든 n 개 문자열에 대해 k 개의 새 문자열을 생성하기 때문 입니다. 이러한 각 문자열에 대해 해시를 계산해야하며 일반적으로 O ( k )영형(케이2)케이영형(케이) 시간 .

모든 문자열을 저장함으로써 얻어 공간.영형(케이2)

추가 개선

수정 된 문자열을 직접 저장하지 않고 원본 문자열과 마스크 된 문자의 인덱스를 참조하여 객체를 저장하여 알고리즘을 더욱 향상시킬 수 있습니다. 이렇게하면 모든 문자열을 만들 필요가 없으며 모든 객체를 저장하기 위해 공간 만 있으면 됩니다.영형(케이)

객체에 대한 사용자 지정 해시 함수를 구현해야합니다. Java 구현을 예로 들어 Java 문서를 참조하십시오 . java hashCode는 각 문자의 유니 코드 값에 곱합니다 ( k 는 문자열 길이와 i 는 문자의 1 기반 색인입니다). 변경된 각 문자열은 원래 문자와 하나의 문자 만 다릅니다. 이 문자를 해시 코드에 공헌하여이를 빼고 대신 마스킹 문자를 추가 할 수 있는데, 계산에 O ( 1 ) 가 걸리므로 총 실행 시간을 O ( n31케이나는케이나는O(1)O(nk)


4
@JollyJoker 네, 공간은이 방법에서 중요한 문제입니다. 수정 된 문자열을 저장하지 않고 문자열 및 마스크 된 인덱스를 참조하여 객체를 저장하면 공간을 줄일 수 있습니다. O (nk) 공간을 남겨 두어야합니다.
Simon Prins 2016 년

O ( k ) 시간 에 각 문자열에 대한 해시 를 계산하려면 특별한 수제 해시 함수가 필요하다고 생각합니다 (예 : O ( k ) 시간 에 원래 문자열의 해시를 계산 한 다음 삭제 된 각각과 XOR O ( 1 )의 문자는 각각 시간마다 (다른 방법으로는 꽤 나쁜 해시 함수이지만). BTW, 이것은 내 솔루션과 매우 유사하지만 k 개의 개별 문자 대신 단일 해시 테이블을 사용 하고 문자를 삭제하는 대신 "*"로 대체합니다. kO(k)O(k)O(1)k
j_random_hacker

@SimonPrins 작동 할 수 있는 사용자 정의 equalshashCode메소드. 이러한 메소드에서 a * b 스타일 문자열을 작성하면 방탄이됩니다. 여기에있는 다른 답변 중 일부에 해시 충돌 문제가 있다고 생각합니다.
JollyJoker

1
@DW 해시를 계산하는 데 시간 이 걸린다는 사실을 반영하여 게시물을 수정 하고 총 실행 시간을 다시 O ( n * k ) 로 낮추는 솔루션을 추가했습니다 . O(k)O(nk)
Simon Prins 2016 년

1
@SimonPrins 해시가 충돌 할 때 hashset.contains의 문자열 동등 검사로 인해 최악의 경우 nk ^ 2 수 있습니다. 모든 문자열에 대해 동일한 해시를 얻을 특히, 캐릭터 라인의 꽤 많은 손으로 만든 세트를 필요로 똑같은 해시를 가질 때 물론, 최악의 경우는 *bc, a*c, ab*. 그것이 불가능한 것으로 보일 수 있는지 궁금합니다.
JollyJoker

7

나는 해시 테이블 H 1 , , H k를 만들 것입니다 . 각각의 키 길이 는 ( k - 1 ) 길이 문자열이고 값 목록은 숫자 (문자열 ID)입니다. 해시 테이블 H ikH1,,Hk(k1)Hi 는 지금까지 처리 된 모든 문자열을 포함 하지만 위치의 문자는 삭제됩니다i . 예를 들어, 이면 H 3 [ A B D E F ] 는 지금까지 본 패턴 A 를 가진 모든 문자열의 목록을 포함합니다 .k=6H3[ABDEF] 여기서 은 "모든 문자"를 의미합니다. 그런 다음 j 번째 입력 문자열 s j 를 처리하십시오.ABDEFjsj

  1. 1에서 k 까지의 각 에 대해 : ik
    • 형식 문자열 삭제하여 에서 번째 문자 의의 J를 .sjisj
    • 조회 . 여기에서 모든 문자열 ID는 s 와같거나 위치 i 에서만다른원래 문자열을 식별합니다. 이것을 문자열 s j에 일치하는 것으로 출력하십시오. (정확한 중복을 제외하려면 해시 테이블의 값 유형을 (문자열 ID, 삭제 된 문자) 쌍으로 지정하여 s j 에서 방금 삭제 한 것과 동일한 문자가 삭제 된 항목을 테스트 할 수 있습니다.)Hi[sj]sisjsj
    • 향후 쿼리에서 사용할 수 있도록 H i 에 삽입하십시오 .jHi

각 해시 키를 명시 적으로 저장하는 경우 공간을 사용해야 하므로 최소한 시간이 복잡해야합니다. 그러나 Simon Prins에 의해 설명 된 것처럼 특정 문자열에 대한 모든 k 해시 키가 필요로 하는 방식으로 문자열에 대한 일련의 수정 사항을 표시 할 수 있습니다 (그의 경우 삭제로 광산에서 단일 문자를 변경하는 것으로 설명 됨 )O(nk2)*k 공간으로 이어지는 O ( N 개의 K ) 전체 공간 및 가능성 개방 O ( N 케이 )O(k)O(nk)O(nk)시간도. 이 시간의 복잡성을 달성하려면 O ( k ) 시간 에 길이 k 문자열 의 모든 변형에 대한 해시를 계산하는 방법이 필요합니다 . 예를 들어 DW에서 제안한대로 다항식 해시를 사용하여이 작업을 수행 할 수 있습니다. 삭제 된 문자를 원래 문자열의 해시로 단순히 XOR하는 것보다 훨씬 낫습니다.)kkO(k)

Simon Prins의 암시 적 표현 트릭은 또한 각 문자의 "삭제"가 실제로 수행되지 않기 때문에 성능 저하없이 원래의 배열 기반 문자열 표현을 사용할 수 있습니다 (원래 제안한 링크 목록 대신).


2
좋은 해결책. 적합한 맞춤형 해시 함수의 예는 다항식 해시입니다.
DW

감사합니다 @DW "다항식 해시"가 의미하는 바를 명확하게 설명해 주시겠습니까? 인터넷 검색 용어는 결정적인 것으로 보이지 않았습니다. (원하는 경우 내 게시물을 직접 편집 해주십시오.)
j_random_hacker

1
문자열을 기본 숫자 모듈로 p 로 읽으십시오 . 여기서 p 는 해시 맵 크기보다 소수이고 qp 의 기본 루트 이고 q 는 알파벳 크기보다 큽니다. q 의 문자열에 의해 계수가 주어진 다항식을 평가하는 것과 같기 때문에 "다항식 해시"라고합니다 . O ( k ) 시간 내에 원하는 모든 해시를 계산하는 방법을 알아내는 연습으로 남겨 두겠습니다 . 원하는 조건을 만족하는 p , q를 모두 임의로 선택하지 않는 한이 방법은 적에게 면역이되지 않습니다 .qppqpqqO(k)p,q
user21820

1
k 해시 테이블 중 하나만 언제든지 존재하면 메모리 요구 사항이 줄어드는 것을 관찰 함으로써이 솔루션을 더 세분화 할 수 있다고 생각합니다 .
Michael Kay

1
@ MichaelKay : O ( k ) 시간 에 가능한 문자열 변경의 해시 를 계산하려는 경우 작동하지 않습니다 . 여전히 어딘가에 보관해야합니다. 따라서 한 번에 하나의 위치 만 확인하는 경우 해시 테이블 항목 을 k 배로 사용하여 모든 위치를 함께 확인하는 경우 k 배가 걸립니다 . kO(k)kk
user21820 2016 년

2

다항식 해시 방법보다 더 강력한 해시 테이블 방식이 있습니다. 먼저 해시 테이블 크기 M 에 대한 프라임 인 랜덤 양의 정수 r 1 .. k 를 생성하십시오 . 즉, 0 r i < M 입니다. 그런 다음 각 문자열 x 1 .. k( k i = 1 x i r i ) mod M에 해시하십시오 . 런타임에 r 1 .. k 를 생성 하므로 k 와 같이 매우 고르지 않은 충돌을 유발할 수있는 공격자는 거의 없습니다 .kr1..kM0ri<Mx1..k(i=1kxiri)modMr1..kk주어진 한 쌍의 고유 한 문자열 쌍의 충돌 가능성을 최대 으로 빠르게 증가시킵니다 . 하나의 문자가 변경된 각 문자열에 대해 가능한 모든 해시를 O ( k ) 시간 으로 계산하는 방법도 분명합니다 .1/MO(k)

당신이 정말로 보장 유니폼 해싱하려는 경우, 당신은 하나 개의 임의의 자연수를 생성 할 수 있습니다 이하 M 각 쌍 ( I , C ) 에 대한 에서 k는 각 문자에 대한 C 각각의 문자열을 해시 다음과 x 1 .. k ~ ( k i = 1 r ( i , x)r(i,c)M(i,c)i1kcx1..k(i=1kr(i,xi))modM. 그러면 주어진 한 쌍의 개별 문자열이 충돌 할 확률은 정확히 입니다. 이 접근법은 문자 세트가 n에 비해 상대적으로 작은 경우에 더 좋습니다 .1/Mn


2

여기에 게시 된 많은 알고리즘은 해시 테이블에서 꽤 많은 공간을 사용합니다. 다음은 보조 저장소 O ( ( n lg n ) k 2 ) 런타임 단순 알고리즘입니다.O(1)O((nlgn)k2)

트릭 사용이고 두 개의 값 사이의 비교이다 하고 , B 그 반환 참이면 < B는Ck(a,b)aba<b (전적으로)를 무시하면서 번째 문자. 알고리즘은 다음과 같습니다.k

먼저 문자열을 정기적으로 정렬하고 선형 스캔을 수행하여 중복을 제거하십시오.

그런 다음 각 k 에 대해k :

  1. C k를 사용 하여 문자열 정렬Ck 를 비교기로 .

  2. 만 다른 문자열 은 이제 인접 해 있으며 선형 스캔에서 감지 될 수 있습니다.k


1

하나의 문자가 다른 길이 k 의 두 문자열은 길이 l 의 접두사 와 길이 m 의 접미사를 공유 합니다. K = L + m + 1 .

사이먼 프린스에 의해 대답은 모두 / 접미사 조합이 명시 적으로, 즉 접두사 저장하여이 인코딩 abc되고 *bc,a*c 하고 ab*. k = 3, l = 0,1,2, m = 2,1,0입니다.

valarMorghulis가 지적한 것처럼 접두사 트리에서 단어를 구성 할 수 있습니다. 매우 유사한 접미사 트리도 있습니다. 각 접두사 또는 접미사 아래에 리프 노드 수를 사용하여 트리를 확장하는 것은 매우 쉽습니다. 새 단어를 삽입 할 때 O (k)로 업데이트 할 수 있습니다.

이 형제 수를 원하는 이유는 동일한 접두사로 모든 ​​문자열을 열거할지 또는 동일한 접미사로 모든 ​​문자열을 열거할지 여부를 새로운 단어로 알 수 있기 때문입니다. 예를 들어 "abc"를 입력으로 사용할 경우 접두어는 "", "a"및 "ab"이며 해당 접미사는 "bc", "c"및 ""입니다. 명백하게, 짧은 접미어의 경우 접두사 트리에서 형제를 열거하고 그 반대의 경우도 더 좋습니다.

@einpoklum이 지적했듯이 모든 문자열이 동일한 k / 2 접두사를 공유 할 수 있습니다 . 이 방법에는 문제가되지 않습니다. 접두사 트리는 최대 k / 2 깊이까지 100.000 리프 노드의 조상으로 깊이 k / 2까지 선형입니다. 결과적으로 접미사 트리는 최대 (k / 2-1) 깊이까지 사용됩니다. 접두사를 공유하면 문자열의 접미사가 달라야하기 때문에 좋습니다.

당신은 문자열의 짧은 고유의 접두사를 결정하고 나면 최적화로 [편집], 당신이 알고 있는 경우 때 거의 중복 한 다른 문자가있다, 그것은 접두사의 마지막 문자 여야합니다, 당신은 발견 한 것 하나 더 짧은 접두사를 확인합니다. 따라서 "abcde"에 가장 짧은 고유 접두사 "abc"가 있으면 "ab?"로 시작하는 다른 문자열이 있음을 의미합니다. "abc"는 아닙니다. 즉, 한 문자 만 다르면 세 번째 문자가됩니다. 더 이상 "abc? e"를 확인할 필요가 없습니다.

같은 논리로 "cde"가 가장 짧은 고유 접미사 인 경우 길이 1 또는 3 접두사가 아닌 길이 -2 "ab"접두사 만 확인하면된다는 것을 알고 있습니다.

이 방법은 정확히 하나의 문자 차이에 대해서만 작동하며 2 개의 문자 차이로 일반화되지 않으며 동일한 접두사와 동일한 접미사로 구분되는 한 문자를 사용합니다.


각 문자열 와 각 1 i k 에 대해 접두사 trie 의 길이 - ( i - 1 ) 접두사에 해당하는 노드 P [ s 1 , , s i - 1 ] 을 찾고 , 길이에 해당하는 노드 S [ s i + 1 , , s k ] - ( k i - 1 )s1ikP[s1,,si1](i1)S[si+1,,sk](ki1)접미사 트리의 접미사 (각각 상각 된 시간 이 소요됨 )를 각각의 후손 수와 비교하여 더 적은 자손이있는 것을 선택하고 해당 트리의 나머지 문자열에 대해 "탐색"합니까? O(1)
j_random_hacker

1
당신의 접근 시간은 무엇입니까? 최악의 경우 이차적 일 수 있습니다. 모든 문자열이 동일한 문자로 시작하고 끝나는 경우 어떻게되는지 고려하십시오 . k/4
DW

최적화 아이디어는 영리하고 흥미 롭습니다. mtaches 검사를 수행하는 특별한 방법을 염두에 두셨습니까? "abcde"에 가장 짧은 고유 접두사 "abc"가 있으면 "ab? de"형식의 다른 문자열을 확인해야합니다. 그렇게하는 특별한 방법을 염두에 두셨습니까? 효율적입니까? 결과적인 실행 시간은 얼마입니까?
DW

@DW : "ab? de"형식으로 문자열을 찾으려면 접두사 트리에서 "ab"아래에 몇 개의 리프 노드가 있는지, 접미사 트리에서 "de"아래에 몇 개의 노드가 있는지 확인한 다음 열거 할 둘 중 가장 작은 것. 모든 문자열이 동일한 k / 4 문자로 시작하고 끝나는 경우 즉, 두 트리의 첫 번째 k / 4 노드에는 각각 하나의 자식이 있습니다. 그리고 네, 그 나무가 필요할 때마다 O (n * k) 단계 인 나무를 통과해야합니다.
MSalters

접두사 trie에서 "ab? de"형식의 문자열을 확인하려면 "ab"에 대한 노드로 이동 한 다음 각 하위 에 대해 경로 "de"가 v 아래에 있는지 확인하십시오 . 즉,이 하위 작업에서 다른 노드를 열거하지 않아도됩니다. 여기에는 O ( a h ) 시간 이 걸립니다 . 여기서 a 는 알파벳 크기이고 h 는 trie의 초기 노드 높이입니다. hO ( k ) 이므로 알파벳 크기가 O ( n ) 이면 실제로 O ( n k )입니다vvO(ah)ahhO(k)O(n)O(nk)전반적으로 시간이 적지 만 더 작은 알파벳이 일반적입니다. 자녀의 수 (자손이 아닌)와 키가 중요합니다.
j_random_hacker

1

버킷에 문자열을 저장하는 것이 좋은 방법입니다 (이에 대한 대답은 이미 다릅니다.)

다른 해결책은 문자열을 정렬 된 목록 에 저장하는 것 입니다. 트릭은 지역에 민감한 해싱 알고리즘 을 기준으로 정렬하는 것 입니다 . 이것은 입력이 유사 할 때 유사한 결과를 산출하는 해시 알고리즘입니다 [1].

당신이 문자열을 조사 할 때마다 해시를 계산하고 (복용하여 정렬 된 목록에서 그 해시의 위치를 조회 할 수 배열이나에 대한 O ( N을 ) 연결리스트에 대한). 해당 위치의 이웃 (모든 가까운 이웃을 고려할 때 인덱스가 +/- 1 인 이웃뿐만 아니라)도 비슷한 것으로 나타났습니다 (한 문자 제외). 더 유사한 문자열이없는 경우 (소요 당신이 볼 수있는 위치에 새로운 문자열을 삽입 할 수 있습니다 O ( 1 ) 연결리스트와 대한 O ( N ) 배열을).O(log(n))O(n)O(1)O(n)

로컬 리티에 민감한 해싱 알고리즘 중 하나는 Nilsimsa 일 수 있습니다 (예 : python 에서 오픈 소스 구현 가능 ).

[1] : SHA1과 같은 해시 알고리즘은 종종 반대를 위해 설계되었습니다. 유사하지만 동일하지 않은 입력에 대해 크게 다른 해시를 생성합니다.

면책 조항 : 솔직히 말해서 프로덕션 응용 프로그램에 대해 중첩 / 트리 구성 버킷 솔루션 중 하나를 개인적으로 구현하려고합니다. 그러나 정렬 된 목록 아이디어는 흥미로운 대안으로 나를 놀라게했습니다. 이 알고리즘은 선택한 해시 알고리즘에 따라 크게 달라집니다. Nilsimsa는 내가 찾은 하나의 알고리즘입니다-더 많은 것이 있습니다 (예 : TLSH, Ssdeep 및 Sdhash). Nilsimsa가 내 개요 알고리즘과 작동하는지 확인하지 못했습니다.


1
흥미로운 생각이지만 입력이 1 문자 만 다른 경우 두 해시 값이 얼마나 멀리 떨어져 있는지에 대한 경계가 있어야한다고 생각합니다. 그런 다음 이웃 대신 해시 값 범위 내의 모든 것을 스캔하십시오. ( 1 문자 씩 다른 가능한 모든 문자열 쌍에 대해 인접한 해시 값을 생성하는 해시 함수를 갖는 것은 불가능합니다 . 00, 01, 10 및 11 인 2 진 문자열의 길이 2 문자열을 고려하십시오. h (00)이 다음과 같은 경우) h (10)과 h (01) 모두에 인접하면 둘 사이에 있어야하며,이 경우 h (11)은 둘 다에 인접 할 수 없으며 그 반대도 마찬가지입니다.)
j_random_hacker

이웃을 보는 것만으로는 충분하지 않습니다. abcd, acef, agcd 목록을 고려하십시오. 일치하는 쌍이 있지만 abcd가 agcd의 이웃이 아니기 때문에 프로 시저가이를 찾지 못합니다.
D.W.

You both are right! With neighbours I didn't mean only "direct neighbours" but thought of "a neighbourhood" of close positions. I didn't specify how many neighbours need to be looked at since that depends on the hash algorithm. But you're right, I should probably note this down in my answer. thanks :)
tessi

1
"LSH ... 유사 항목은 확률이 높은 동일한"버킷 "에 매핑됩니다."확률 알고리즘이므로 결과가 보장되지 않습니다. 따라서 100 % 솔루션이 필요한지 또는 99.9 %로 충분한 지 TS에 따라 다릅니다.
Bulat

1

하나에서 해결책을 얻을 수 있습니다 영형(케이+2) 시간과 영형(케이) space using enhanced suffix arrays (Suffix array along with the LCP array) that allows constant time LCP (Longest Common Prefix) query (i.e. Given two indices of a string, what is the length of the longest prefix of the suffixes starting at those indices). Here, we could take advantage of the fact that all strings are of equal length. Specifically,

  1. Build the enhanced suffix array of all the n strings concatenated together. Let X=x1.x2.x3....xn where xi,1in is a string in the collection. Build the suffix array and LCP array for X.

  2. Now each xi starts at position (i1)k in the zero-based indexing. For each string xi, take LCP with each of the string xj such that j<i. If LCP goes beyond the end of xj then xi=xj. Otherwise, there is a mismatch (say xi[p]xj[p]); in this case take another LCP starting at the corresponding positions following the mismatch. If the second LCP goes beyond the end of xj then xi and xj differ by only one character; otherwise there are more than one mismatches.

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

You could use SDSL library to build the suffix array in compressed form and answer the LCP queries.

Analysis: Building the enhanced suffix array is linear in the length of X i.e. O(nk). Each LCP query takes constant time. Thus, querying time is O(n2).

Generalisation: This approach can also be generalised to more than one mismatches. In general, running time is O(nk+qn2) where q is the number of allowed mismatches.

If you wish to remove a string from the collection, instead of checking every j<i, you could keep a list of only 'valid' j.


Can i say that O(kn2) algo is trivial - just compare each string pair and count number of matches? And k in this formula practically can be omitted, since with SSE you can count matching bytes in 2 CPU cycles per 16 symbols (i.e. 6 cycles for k=40).
Bulat

사과하지만 쿼리를 이해할 수 없습니다. 위의 접근 방식은영형(케이+2) 그리고 아닙니다 영형(케이2). 또한 사실상 알파벳 크기와 무관합니다. 해시 테이블 방식과 함께 사용할 수 있습니다. 두 개의 문자열이 동일한 해시를 가진 것으로 확인되면 단일 불일치가 포함 된 경우 테스트 할 수 있습니다영형(1) 시각.
Ritu Kundu

필자의 요점은 질문 작성자의 경우 k = 20..40이며 이러한 작은 문자열을 비교하면 CPU 사이클이 몇 번이면 충분하므로 무차별 대입 방식과 실제 접근 방식의 실질적인 차이는 없을 것입니다.
Bulat

1

제안 된 모든 솔루션을 한 가지 개선했습니다. 그들은 모두 필요합니다영형(케이)최악의 경우 메모리. 당신과 문자열의 해시를 계산하여이를 감소시킬 수있다 *즉 대신 각 문자, *bcde, a*cde... 그리고 특정 정수 범위에서 해시 값으로 변형 각 패스에서 처리. 첫 번째 패스에는 짝수 해시 값이 있고 두 번째 패스에는 홀수 해시 값이있는 Fe가 있습니다.

이 방법을 사용하여 작업을 여러 CPU / GPU 코어로 분할 할 수도 있습니다.


영리한 제안! 이 경우 원래 질문에=100,000케이40그래서 영형(케이)메모리가 문제가되지 않는 것 같습니다 (4MB와 같을 수 있음). 그래도이를 확장해야하는지 아는 것이 좋습니다.
DW

0

This is a short version of @SimonPrins' answer not involving hashes.

Assuming none of your strings contain an asterisk:

  1. Create a list of size nk where each of your strings occurs in k variations, each having one letter replaced by an asterisk (runtime O(nk2))
  2. Sort that list (runtime O(nk2lognk))
  3. Check for duplicates by comparing subsequent entries of the sorted list (runtime O(nk2))

An alternative solution with implicit usage of hashes in Python (can't resist the beauty):

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)

감사. 또한 언급하십시오케이정확한 사본을 복사하면 +1합니다. (흠, 방금 내가 똑같은 주장을했음을 알았습니다.영형(케이) 내 자신의 대답에 시간 ... 그것을 더 나은 수정 ...)
j_random_hacker

@j_random_hacker OP가 정확히 무엇을보고해야하는지 모르겠 기 때문에 3 단계의 모호한 부분을 남겼지 만 (a) 이진은 중복 / 복제 결과가 아님 또는 (b) 목록을보고하는 추가 작업이 쉽지 않다고 생각합니다. 중복없이 최대 한 위치에서 다른 문자열 쌍. 문자 그대로 OP를 취하면 ( "...... 두 문자열이 있는지 확인하기 위해 ..."), (a)가 바람직한 것 같습니다. 또한, 만약 (b)가 필요하다면 당연히 짝 목록을 만드는 것은영형(2) 모든 문자열이 같은 경우
바나나

0

Here is my take on 2+ mismatches finder. Note that in this post I consider each string as circular, f.e. substring of length 2 at index k-1 consists of symbol str[k-1] followed by str[0]. And substring of length 2 at index -1 is the same!

If we have M mismatches between two strings of length k, they have matching substring with length at least mlen(k,M)=k/M1 since, in the worst case, mismatched symbols split (circular) string into M equal-sized segments. F.e. with k=20 and M=4 the "worst" match may have the pattern abcd*efgh*ijkl*mnop*.

Now, the algorithm for searching all mismatches up to M symbols among strings of k symbols:

  • for each i from 0 to k-1
    • split all strings into groups by str[i..i+L-1], where L = mlen(k,M). F.e. if L=4 and you have alphabet of only 4 symbols (from DNA), this will make 256 groups.
    • Groups smaller than ~100 strings can be checked with brute-force algorithm
    • For larger groups, we should perform secondary division:
      • Remove from every string in the group L symbols we already matched
      • for each j from i-L+1 to k-L-1
        • split all strings into groups by str[i..i+L1-1], where L1 = mlen(k-L,M). F.e. if k=20, M=4, alphabet of 4 symbols, so L=4 and L1=3, this will make 64 groups.
        • the rest is left as exercise for the reader :D

Why we don't start j from 0? Because we already made these groups with the same value of i, so job with j<=i-L will be exactly equivalent to job with i and j values swapped.

Further optimizations:

  • At every position, also consider strings str[i..i+L-2] & str[i+L]. This only doubles amount of jobs created, but allows to increase L by 1 (if my math is correct). So, f.e. instead of 256 groups, you will split data into 1024 groups.
  • If some L[i] becomes too small, we can always use the * trick: for each i in in 0..k-1, remove i'th symbol from each string and create job searching for M-1 mismatches in those strings of length k-1.

0

I work everyday on inventing and optimizing algos, so if you need every last bit of performance, that is the plan:

  • Check with * in each position independently, i.e. instead of single job processing n*k string variants - start k independent jobs each checking n strings. You can spread these k jobs among multiple CPU/GPU cores. This is especially important if you are going to check 2+ char diffs. Smaller job size will also improve cache locality, which by itself can make program 10x faster.
  • If you are going to use hash tables, use your own implementation employing linear probing and ~50% load factor. It's fast and pretty easy to implement. Or use an existing implementation with open addressing. STL hash tables are slow due to use of separate chaining.
  • You may try to prefilter data using 3-state Bloom filter (distinguishing 0/1/1+ occurrences) as proposed by @AlexReynolds.
  • For each i from 0 to k-1 run the following job:
    • Generate 8-byte structs containing 4-5 byte hash of each string (with * at i-th position) and string index, and then either sort them or build hash table from these records.

For sorting, you may try the following combo:

  • first pass is MSD radix sort in 64-256 ways employing TLB trick
  • second pass is MSD radix sort in 256-1024 ways w/o TLB trick (64K ways total)
  • third pass is insertion sort to fix remaining inconsistencies
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.