Структура даних або алгоритм для швидкого пошуку відмінностей між рядками


19

У мене є масив із 100 000 рядків, усіма довжиною . Я хочу порівняти кожну рядок з кожною іншою рядком, щоб побачити, чи відрізняються будь-які два рядки на 1 символ. Зараз, додаючи кожен рядок до масиву, я перевіряю його проти кожної рядки, яка вже є в масиві, який має складність у часі .n ( n - 1 )kn(n1)2k

Чи є структура даних або алгоритм, який може порівнювати рядки один з одним швидше, ніж те, що я вже роблю?

Деякі додаткові відомості:

  • Порядок має значення: abcdeі xbcdeвідрізняється на 1 символ, тоді як abcdeі edcbaвідрізняється на 4 символи.

  • Для кожної пари рядків, які відрізняються одним символом, я буду видаляти один із цих рядків із масиву.

  • Зараз я шукаю рядки, які відрізняються лише 1 символом, але було б добре, якби цю різницю символів можна збільшити до, скажімо, 2, 3 або 4 символів. Однак у цьому випадку я думаю, що ефективність важливіша, ніж здатність збільшувати межу різниці символів.

  • k зазвичай знаходиться в межах 20-40.


4
Пошук рядкового словника з 1 помилкою - досить відома проблема, наприклад, cs.nyu.edu/~adi/CGL04.pdf
KWillets

1
20-40 мешканців можуть використовувати небагато місця. Ви можете подивитися на фільтр Bloom ( en.wikipedia.org/wiki/Bloom_filter ), щоб перевірити, чи вироджені рядки - набір усіх mers з однієї, двох чи більше підстановок на тестовому рівні - є "можливо-в" або "безумовно -не-в "набір кмерів. Якщо ви отримали "можливо-в", то порівняйте ці два рядки, щоб визначити, чи це хибний позитив. Випадки "точно не вводяться" - це справжні негативи, які зменшать загальну кількість порівнянь по листі, що вам доведеться зробити, обмеживши порівняння лише потенційними хітами "можливо-в".
Алекс Рейнольдс

Якщо ви працювали з меншим діапазоном k, ви можете використовувати біт, щоб зберігати хеш-таблицю булей для всіх вироджених рядків (наприклад, github.com/alexpreynolds/kmer-boolean для прикладу іграшки). Для k = 20-40, проте, вимоги до місця для біт просто небагато.
Алекс Рейнольдс

Відповіді:


12

Можна досягти найгіршого часу роботи.O(nklogk)

Почнемо з простого. Якщо ви дбаєте про просте у виконанні рішення, яке буде ефективним на багатьох вкладах, але не на всіх, ось просте, прагматичне, просто реалізувати рішення, якого на практиці достатньо для багатьох ситуацій. Однак у гіршому випадку вона повертається до квадратичного часу роботи.

Візьміть кожну рядок і збережіть її в хештебі, введеному на першу половину рядка. Потім перейдіть по відра з хешбетом. Для кожної пари рядків в одному відрі перевірте, чи відрізняються вони в 1 символі (тобто перевірте, чи відрізняється їх друга половина на 1 символ).

Потім візьміть кожну рядок і збережіть її в хеш-таблицю, на цей раз клавішуючи на другій половині рядка. Знову перевіряйте кожну пару струн в одному відрі.

Якщо припустити, що струни добре розподілені, час роботи, швидше за все, буде приблизно . Більше того, якщо існує пара рядків, які відрізняються на 1 символ, це буде виявлено під час одного з двох проходів (оскільки вони відрізняються лише 1 символом, що різний символ повинен бути або в першій, або в другій половині рядка, тому друга або перша половина струни повинна бути однаковою). Однак у гіршому випадку (наприклад, якщо всі рядки починаються або закінчуються однаковими k / 2 символами), це погіршує час роботи O ( n 2 k ) , тому його найгірший час роботи не є поліпшенням грубої сили .O(nk)k/2O(n2k)

Оптимізація продуктивності, якщо будь-яке відро має в собі занадто багато рядків, ви можете повторювати той же процес рекурсивно, щоб шукати пару, яка відрізняється одним символом. Рекурсивне виклик буде на рядках довжиною .k/2

Якщо ви дбаєте про найгірший час роботи:

З урахуванням вищеописаної оптимізації продуктивності, я вважаю, що найгіршим часом роботи є .O(nklogk)


3
Якщо рядки поділяють ту саму першу половину, що може дуже статися в реальному житті, то ви не покращили складність. Ω(n)
einpoklum

@einpoklum, точно! Ось чому я написав у своєму другому реченні заяву про те, що воно в гіршому випадку повертається до квадратичного часу роботи, а також у моєму останньому реченні, що описує, як досягти найгіршої складності якщо вам все одно про найгірший випадок. Але я думаю, можливо, я не висловив це дуже чітко - тому я відповідно відредагував свою відповідь. Чи краще зараз? O(nklogk)
DW

15

Моє рішення подібне до j_random_hacker, але використовує лише один хеш-набір.

Я створив би хеш-набір рядків. Для кожного рядка на вході додайте до набору рядків. У кожному з цих рядків замініть одну з літер спеціальним символом, який не зустрічається ні в одному з рядків. Поки ви додаєте їх, переконайтеся, що вони ще не встановлені. Якщо вони є, то у вас є два рядки, які відрізняються (максимум) одним символом.k

Приклад із рядками 'abc', 'adc'

Для abc додаємо '* bc', 'a * c' та 'ab *'

Для adc ми додаємо "* dc", "a * c" та "ad *"

Коли ми додаємо "a * c" вдруге, ми помічаємо, що це вже в наборі, тому ми знаємо, що є два рядки, які відрізняються лише однією літерою.

Загальний час роботи цього алгоритму дорівнює . Це тому, що ми створюємо k нових рядків для всіх n рядків на вході. Для кожного з цих рядків нам потрібно обчислити хеш, який зазвичай займає O ( k ) час.O(nk2)knO(k)

Зберігання всіх рядків займає місця.O(nk2)

Подальші вдосконалення

Ми можемо додатково вдосконалити алгоритм, не зберігаючи модифіковані рядки безпосередньо, а замість цього зберігаючи об’єкт із посиланням на початковий рядок та індекс символу, який маскується. Таким чином нам не потрібно створювати всі рядки, і нам потрібен лише простір для зберігання всіх об'єктів.O(nk)

Вам потрібно буде реалізувати спеціальну функцію хешу для об'єктів. Ми можемо взяти реалізацію Java як приклад, подивитися документацію на Java . Java-хеш-код помножує значення unicode кожного символу на k довжиною рядка та i на основі одного індексу символу. Зауважте, що кожна змінена рядок відрізняється лише одним символом від оригіналу. Ми можемо легко обчислити внесок цього символу в хеш-код. Ми можемо відняти це і замість цього додати наш маскуючий символ. Для обчислення потрібен O ( 1 ) . Це дозволяє нам звести загальний час роботи до O ( n31kikiO(1)O(nk)


4
@JollyJoker Так, космос викликає занепокоєння з цим методом. Ви можете зменшити простір, не зберігаючи змінені рядки, а натомість зберігаючи об’єкт із посиланням на рядок та маскований індекс. Це повинно залишити вам простір O (nk).
Саймон Принс

Для обчислення хешів для кожного рядка за час O ( k ) , я думаю, вам знадобиться спеціальна домашня хеш-функція (наприклад, обчислити хеш вихідної рядки за час O ( k ) , а потім XOR - з кожним видаленим символів в O ( 1 ) раз кожен (хоча це, мабуть, досить погана хеш-функція іншими способами)). BTW, це досить схоже на моє рішення, але з одним хештелем замість k окремих, і заміною символу на "*" замість видалення. kO(k)O(k)O(1)k
j_random_hacker

@SimonPrins З користувацькими equalsта hashCodeметодами, які могли б працювати. Просто створення рядка в стилі * b в цих методах повинно зробити його бездоганним; Я підозрюю, що деякі інші відповіді тут матимуть хеш-проблеми зіткнення.
JollyJoker

1
@DW Я змінив свій пост, щоб відобразити той факт, що обчислення хесів займає час і додав рішення, щоб повернути загальний час роботи до O ( n k ) . O(k)O(nk)
Саймон Принс

1
@SimonPrins Найгіршим випадком може бути nk ^ 2 через перевірку рівності рядків у hashset.contains, коли хеші стикаються. Звичайно, в гіршому випадку, коли кожен рядок має точно такий же хеш, який зажадає досить багато ручного набору рядків, особливо щоб отримати той же хеш *bc, a*c, ab*. Цікаво, чи це може бути неможливо?
JollyJoker

7

Я зробив би хештелів H 1 , ... , H k , кожен з яких має в якості ключа рядок довжини ( k - 1 ) і список номерів (ідентифікаторів рядків) як значення. Хештеб H i буде містити всі оброблені рядки досі, але з символом у позиції i видаленим . Наприклад, якщо k = 6 , то H 3 [ A B D E F ] буде містити перелік усіх рядків, що бачились дотепер, що мають візерунок AkH1,,Hk(k1)Hiik=6H3[ABDEF] , де означає «будь-який символ». Потім обробити j -й рядок введення s j :ABDEFjsj

  1. Для кожного в діапазоні від 1 до k : ik
    • Формуйте рядок , видаливши i -й символ з s j .sjisj
    • Подивіться . Кожен ідентифікатор рядка тут ідентифікує оригінальну рядок, яка дорівнює або s , або відрізняється лише у позиції i . Виведіть їх у відповідність для рядка s j . (Якщо ви хочете виключити точні дублікати, зробіть тип значення хештелів пара (ідентифікатор рядка, видалений символ), щоб ви могли перевірити ті, які видалили той самий символ, який ми видалили з s j .)Hi[sj]sisjsj
    • Вставте в H i, щоб майбутні запити використовувати.jHi

Якщо ми чітко зберігаємо кожен хеш-ключ, тоді ми повинні використовувати простір і, таким чином, мати складність у часі принаймні. Але, як описав Саймон Принс , можна представляти ряд модифікацій рядка (у його випадку описується як зміна одиночних символів на , у моєму як видалення) неявно таким чином, що всі k хеш-ключі для певної рядки потрібно просто O ( k ) простір, що веде до загального простору O ( n k ) і відкриває можливість O ( n k )O(nk2)*kO(k)O(nk)O(nk)час теж. Щоб досягти цієї часової складності, нам потрібен спосіб обчислити хеші для всіх варіацій рядка довжини- k за час O ( k ) : наприклад, це можна зробити за допомогою полінових хешів, як це запропонував DW (і це ймовірно, набагато краще, ніж просто XORing видаленого символу з хешем для вихідної рядки).kkO(k)

Неявна хитрість представлення Саймона Принса також означає, що "видалення" кожного символу насправді не виконується, тому ми можемо використовувати звичайне представлення рядка на основі масиву без покарання за ефективність (а не пов'язані списки, як я спочатку пропонував).


2
Приємне рішення. Прикладом підходящої функції хеш-функції може бути поліном хеша.
DW

Дякую @DW Чи не могли б ви трохи уточнити, що ви маєте на увазі під "многочленним хешем"? Гугл термін не дав мені нічого, що здавалося б остаточним. (Будь ласка, не соромтесь редагувати мою публікацію безпосередньо, якщо хочете.)
j_random_hacker

1
Просто прочитайте рядок як базове число за модулем p , де p є простим розміром, ніж ваш розмір хешмапу, а q - примітивний корінь p , а q більший за розміром алфавіту. Це називається "многочлен хеша", тому що це як оцінка полінома, коефіцієнти якого задаються рядком у q . Я залишу це як вправу, щоб зрозуміти, як обчислити всі бажані хеши за О ( к ) час. Зауважте, що цей підхід не захищений противником, якщо ви не випадково виберете обидва p , q, що задовольняють бажаним умовам.qppqpqqO(k)p,q
користувач21820

1
Я думаю, що це рішення можна вдосконалити, спостерігаючи, що лише одна з k хеш-таблиць повинна існувати в будь-який час, зменшуючи, таким чином, потребу в пам'яті.
Майкл Кей

1
@MichaelKay: Це не спрацює, якщо ви хочете обчислити хеш можливих змін рядка за час O ( k ) . Їх ще потрібно десь зберігати. Отже, якщо ви перевіряєте лише одну позицію за один раз, ви займете k разів, як якщо б ви перевіряли всі позиції разом, використовуючи k разів більше записів хештибу. kO(k)kk
користувач21820

2

Ось більш надійний хеш-підхід, ніж метод многочлен-хеш. Спочатку генеруємо випадкових натуральних чисел r 1 .. k, які є одночасно величиною M хештибу . А саме 0 r i < M . Потім хеш кожного рядка х 1 .. K для ( Σ K я = 1 х я г I ) по модулю М . Суперник майже нічого не може зробити, щоб викликати дуже нерівномірні зіткнення, оскільки ви генеруєте r 1 .. k під час виконання та так, як kkr1..kM0ri<Mx1..k(i=1kxiri)modMr1..kkзбільшує максимальну ймовірність зіткнення будь-якої пари різних рядків швидко переходить в . Очевидно також, як обчислити за O ( k ) час усі можливі хеші для кожної рядки з одним символом.1/MO(k)

Якщо ви дійсно хочете гарантувати рівномірне хешування, ви можете генерувати одне випадкове натуральне число менше М для кожної пари ( i , c ) для i від 1 до k та для кожного символу c , а потім хешувати кожну рядок x 1 .. k до ( k i = 1 r ( i , x i ) ) mod Mr(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)

Хитрість полягає у використанні , який є компаратором між двома значеннями a і b, що повертає істину, якщо a < b (лексикографічно), не зважаючи на k- й символ. Тоді алгоритм такий.Ck(a,b)aba<bk

По-перше, просто регулярно сортуйте рядки і виконайте лінійну перевірку, щоб видалити дублікати.

Потім для кожного :k

  1. Сортуйте рядки за допомогою у порівнянні.Ck

  2. Рядки, які відрізняються лише , тепер суміжні і можуть бути виявлені при лінійному скануванні.k


1

Два рядки довжиною k , що відрізняються одним символом, мають префікс довжиною l і суфіксом довжини m таким, що k = l + m + 1 .

Відповідь на Simon Прінса кодує це шляхом збереження всіх префікса / суфікса комбінації в явному вигляді, тобто 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, при цьому кожен вузол до k / 2 глибини буде родоначальником 100 000 листових вузлів. Як результат, дерево суфіксів буде використано до (k / 2-1) глибини, що добре, оскільки рядки повинні відрізнятися за суфіксами, враховуючи, що вони мають спільні префікси.

[редагувати] Як оптимізація, як тільки ви визначили найкоротший унікальний префікс рядка, ви знаєте, що якщо є один інший символ, він повинен бути останнім символом префікса, і ви знайшли б майже дублікат, коли перевірка префікса, який був на один коротший. Отже, якщо "abcde" має найкоротший унікальний префікс "abc", це означає, що є й інші рядки, які починаються з "ab?" але не з "abc". Тобто, якби вони відрізнялися лише одним символом, це був би той третій символ. Вам більше не потрібно перевіряти "abc? E".

За тією ж логікою, якщо ви виявите, що "cde" є унікальним найкоротшим суфіксом, то ви знаєте, що вам потрібно перевірити лише префікс довжини-2 "ab", а не довжину 1 або 3 префікса.

Зауважте, що цей метод працює лише для рівно однієї різниці символів і не узагальнює до 2-х відмінностей символів, він покладається на один один символ як розділення між однаковими префіксами та однаковими суфіксами.


Ви припускаєте, що для кожного рядка і кожного 1 i k ми знаходимо вузол P [ s 1 , , s i - 1 ], що відповідає префіксу довжини ( 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

The optimization idea is clever and interesting. Did you have in mind a particular way to do the check for mtaches? If the "abcde" has the shortest unique prefix "abc", that means we should check for some other string of the form "ab?de". Did you have in mind a particular way to do that, that will be efficient? What's the resulting running time?
D.W.

@D.W.: The idea is that to find strings in the form "ab?de", you check the prefix tree how many leaf nodes exist below "ab" and in the suffix tree how many nodes exist under "de", then choose the smallest of the two to enumerate. When all strings begin and end with the same k/4 characters; that means the first k/4 nodes in both trees have one child each. And yes, every time you need those trees, those have to be traversed which is an O(n*k) step.
MSalters

To check for a string of the form "ab?de" in the prefix trie, it suffices to get to the node for "ab", then for each of its children v, check whether the path "de" exists below v. That is, don't bother enumerating any other nodes in these subtries. This takes O(ah) time, where a is the alphabet size and h is the height of the initial node in the trie. h is O(k), so if the alphabet size is O(n) then it is indeed O(nk) time overall, but smaller alphabets are common. The number of children (not descendants) is important, as well as the height.
j_random_hacker

1

Storing strings in buckets is a good way (there are already different answers outlining this).

An alternative solution could be to store strings in a sorted list. The trick is to sort by a locality-sensitive hashing algorithm. This is a hash algorithm which yields similar results when the input is similar[1].

Each time you want to investigate a string, you could calculate its hash and lookup the position of that hash in your sorted list (taking O(log(n)) for arrays or O(n) for linked lists). If you find that the neighbours (considering all close neighbours, not only those with an index of +/- 1) of that position are similar (off by one character) you found your match. If there are no similar strings, you can insert the new string at the position you found (which takes O(1) for linked lists and O(n) for arrays).

One possible locality-sensitive hashing algorithm could be Nilsimsa (with open source implementation available for example in python).

[1]: Note that often hash algorithms, like SHA1, are designed for the opposite: producing greatly differing hashes for similar, but not equal inputs.

Disclaimer: To be honest, I would personally implement one of the nested/tree-organized bucket-solutions for a production application. However, the sorted list idea struck me as an interesting alternative. Note that this algorithm highly depends on the choosen hash algorithm. Nilsimsa is one algorithm I found - there are many more though (for example TLSH, Ssdeep and Sdhash). I haven't verified that Nilsimsa works with my outlined algorithm.


1
Interesting idea, but I think we would need to have some bounds on how far apart two hash values can be when their inputs differ by just 1 character -- then scan everything within that range of hash values, instead of just neighbours. (It's impossible to have a hash function that produces adjacent hash values for all possible pairs of strings that differ by 1 character. Consider the length-2 strings in a binary alphabet: 00, 01, 10 and 11. If h(00) is adjacent to both h(10) and h(01) then it must be between them, in which case h(11) can't be adjacent to them both, and vice versa.)
j_random_hacker

Looking at neighbors isn't sufficient. Consider the list abcd, acef, agcd. There exists a matching pair, but your procedure will not find it, as abcd is not a neighbor of 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... similar items map to the same “buckets” with high probability" - since it's probability algorithm, result isn't guaranteed. So it depends on TS whether he needs 100% solution or 99.9% is enough.
Bulat

1

One could achieve the solution in O(nk+n2) time and O(nk) 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

Apologies but I could not understand your query. The above approach is O(nk+n2) and not O(kn2). Also, it is virtually alphabet-size independent. It could be used in conjunction with the hash-table approach -- Once two strings are found to have the same hashes, they could be tested if they contain a single mismatch in O(1) time.
Ritu Kundu

My point is that k=20..40 for the question author and comparing such small strings require only a few CPU cycles, so practical difference between brute force and your approach probably doesn't exist.
Bulat

1

One improvement to all the solutions proposed. They all require O(nk) memory in the worst case. You can reduce it by computing hashes of strings with * instead each character, i.e. *bcde, a*cde... and processing at each pass only variants with hash value in certain integer range. F.e. with even hash values in the first pass, and odd hash values in the second one.

You can also use this approach to split the work among multiple CPU/GPU cores.


Clever suggestion! In this case, the original question says n=100,000 and k40, so O(nk) memory doesn't seem likely to be an issue (that might be something like 4MB). Still a good idea worth knowing if one needs to scale this up, though!
D.W.

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)

Thanks. Please also mention the k copies of exact duplicates, and I'll +1. (Hmm, just noticed I made the same claim about O(nk) time in my own answer... Better fix that...)
j_random_hacker

@j_random_hacker I don't know what exactly the OP wants reported, so I left step 3 vague but I think it is trivial with some extra work to report either (a) a binary any duplicate/no duplicates result or (b) a list of pairs of strings that differ in at most one position, without duplicates. If we take the OP literally ("...to see if any two strings..."), then (a) seems to be desired. Also, if (b) were desired then of course simply creating a list of pairs may take O(n2) if all strings are equal
Bananach

0

Ось мій пошук 2+ пошуку невідповідностей. Зауважте, що в цій публікації я розглядаю кожну рядок як кругову, fe підрядок довжиною 2 в індексі k-1складається з символу, str[k-1]за яким слід str[0]. І підрядка довжиною 2 в індексі -1однакова!

Якщо ми маємо Mневідповідність між двома рядками довжини k, вони мають відповідні підрядки з довжиною щонайменшемлен(к,М)=к/М-1оскільки, в гіршому випадку, невідповідні символи розділяють (круглі) рядки на Mрівні за розміром сегменти. Фе з k=20і M=4"найгірший" матч може мати закономірність abcd*efgh*ijkl*mnop*.

Тепер алгоритм пошуку всіх невідповідностей до Mсимволів серед рядків kсимволів:

  • для кожного i від 0 до k-1
    • розділити всі рядки на групи по str[i..i+L-1], куди L = mlen(k,M). Якщо у L=4вас є алфавіт лише 4 символи (з ДНК), це складе 256 груп.
    • Групи менше ~ 100 рядків можна перевірити алгоритмом грубої сили
    • Для більших груп ми повинні виконати вторинний поділ:
      • Видаліть з кожного рядка з Lсимволів групи, які ми вже відповідали
      • для кожного j від i-L + 1 до kL-1
        • розділити всі рядки на групи по str[i..i+L1-1], куди L1 = mlen(k-L,M). Fe , якщо k=20, M=4, alphabet of 4 symbols, так L=4і L1=3, це зробить 64 груп.
        • решта залишається як вправа для читача: D

Чому ми не починаємо jз 0? Оскільки ми вже створили ці групи з однаковим значенням i, тому робота з j<=i-Lбуде точно рівнозначна роботі із значеннями i та j заміненими.

Подальші оптимізації:

  • У кожному положенні також враховуйте рядки str[i..i+L-2] & str[i+L]. Це лише подвоює кількість створених робочих місць, але дозволяє збільшити Lна 1 (якщо моя математика правильна). Отже, fe замість 256 груп, ви розділите дані на 1024 групи.
  • Якщо якісь L[i]стає занадто мало, ми завжди можемо використовувати *трюк: для кожного i in in 0..k-1видаліть i'th символ із кожної строки та створити роботу, що шукає M-1невідповідності в цих рядках довжини 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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.