З огляду на дві послідовності, знайдіть максимальне перекриття між закінченням одного та початком іншого


11

Мені потрібно знайти ефективний (псевдо) код для вирішення наступної проблеми:

З огляду на дві послідовності (не обов'язково різних) цілих чисел (a[1], a[2], ..., a[n])і (b[1], b[2], ..., b[n]), знайти максимальне dтаке , що a[n-d+1] == b[1], a[n-d+2] == b[2]..., і a[n] == b[d].

Це не домашнє завдання, я насправді придумав це, коли намагався стиснути два тензори на якомога більше розмірів. Я підозрюю, що дієвий алгоритм існує (можливо O(n)?), Але я не можу придумати щось таке O(n^2). O(n^2)Підхід був би очевидною петля на , dа потім внутрішній цикл по пунктам, щоб перевірити необхідний стан до удару максимуму d. Але я підозрюю, що щось краще, ніж це можливо.


Якщо хеш-прокат може бути обчислений для групи об'єктів у вашому масиві, я думаю, що це можна зробити більш ефективно. Обчисліть хеш для елементів, b[1] to b[d]а потім перейдіть до масиву aобчислити хеш для, a[1] to a[d]якщо це відповідає, то це ваша відповідь, якщо не обчислити хеш для a[2] to a[d+1]повторного використання хешу, обчисленого для a[1] to a[d]. Але я не знаю, чи об'єкти в масиві придатні для обчислення хешируемого хешу.
SomeDude

2
@becko Вибачте, я думаю, що нарешті я розумію, що ви намагаєтеся досягти. Що полягає у знаходженні максимального перекриття між кінцем та aпочатком b. Як це .
користувач3386109

1
Мені здається, що проблема є варіацією зіставлення рядків, яку можна вирішити варіацією алгоритму Knuth – Morris – Pratt . Час роботи буде O (m + n), де mкількість елементів у a, і nкількість елементів у b. На жаль, я не маю достатнього досвіду роботи з KMP, щоб розповісти, як його адаптувати.
користувач3386109

1
@ user3386109 моє рішення також є варіантом алгоритму відповідності рядків під назвою Рабін-Карп , використовуючи метод Хорнера як хеш-функцію.
Даніель

1
@Daniel Ах, я знав, що десь бачив прокатку хешу, але не міг пригадати де :)
user3386109

Відповіді:


5

Ви можете використовувати алгоритм z, алгоритм лінійного часу ( O (n) ), який:

Враховуючи рядок S довжиною n, алгоритм Z створює масив Z, де Z [i] - довжина найдовшої підрядки, починаючи з S [i], яка також є префіксом S

Вам потрібно об'єднати масиви ( b + a ) і запустити алгоритм на отриманому побудованому масиві до першого i такого, що Z [i] + i == m + n .

Наприклад, для a = [1, 2, 3, 6, 2, 3] & b = [2, 3, 6, 2, 1, 0], конкатенація була б [2, 3, 6, 2, 1 , 0, 1, 2, 3, 6, 2, 3], що дасть Z [10] = 2, що виконує Z [i] + i = 12 = m + n .


Гарний! Дякую.
бекко

3

Для складності O (n) часу / простору хитрість полягає в оцінці хешей для кожної послідовності. Розглянемо масив b:

[b1 b2 b3 ... bn]

Використовуючи метод Хорнера , ви можете оцінити всі можливі хеші для кожної послідовності. Виберіть базове значення B(більше, ніж будь-яке значення в обох масивах):

from b1 to b1 = b1 * B^1
from b1 to b2 = b1 * B^1 + b2 * B^2
from b1 to b3 = b1 * B^1 + b2 * B^2 + b3 * B^3
...
from b1 to bn = b1 * B^1 + b2 * B^2 + b3 * B^3 + ... + bn * B^n

Зауважте, що ви можете оцінити кожну послідовність за O (1) час, використовуючи результат попередньої послідовності, отже, всі завдання коштують O (n).

Тепер у вас є масив Hb = [h(b1), h(b2), ... , h(bn)], звідки Hb[i]хеш b1до тих пір bi.

Зробіть те ж саме для масиву a, але з невеликою хитрістю:

from an to an   =  (an   * B^1)
from an-1 to an =  (an-1 * B^1) + (an * B^2)
from an-2 to an =  (an-2 * B^1) + (an-1 * B^2) + (an * B^3)
...
from a1 to an   =  (a1   * B^1) + (a2 * B^2)   + (a3 * B^3) + ... + (an * B^n)

Ви повинні зауважити, що, переходячи від однієї послідовності до іншої, ви множите всю попередню послідовність на B і додаєте нове значення, помножене на B. Наприклад:

from an to an =    (an   * B^1)

for the next sequence, multiply the previous by B: (an * B^1) * B = (an * B^2)
now sum with the new value multiplied by B: (an-1 * B^1) + (an * B^2) 
hence:

from an-1 to an =  (an-1 * B^1) + (an * B^2)

Тепер у вас є масив Ha = [h(an), h(an-1), ... , h(a1)], звідки Ha[i]хеш aiдо тих пір an.

Тепер ви можете порівняти Ha[d] == Hb[d]для всіх dзначень від n до 1, якщо вони відповідають, у вас є відповідь.


УВАГА : це хеш-метод, значення можуть бути великими, і вам, можливо, доведеться використовувати метод швидкої експоненції та модульну арифметику , що може (навряд чи) спричинити зіткнення , що робить цей метод не зовсім безпечним. Доброю практикою є вибір бази Bяк дійсно великого простого числа (принаймні більше, ніж найбільше значення у ваших масивах). Ви також повинні бути обережними, оскільки обмеження чисел можуть переповнюватись на кожному кроці, тому вам доведеться використовувати (модуль K) у кожній операції (де це Kможе бути більше, ніж B).

Це означає, що дві різні послідовності можуть мати однаковий хеш, але дві рівні послідовності завжди матимуть однаковий хеш.


Чи можете ви почати цю відповідь з оцінки вимог до ресурсів?
сіра борода

2

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

Наївний метод - після зіставлення k рівних знаків k - знайде символ, який не відповідає, і повернеться назад k-1 одиниці в a , скине індекс у b , а потім розпочнеть процес узгодження звідти. Це явно являє собою O (n²) найгірший випадок.

Щоб уникнути цього процесу зворотного відстеження, ми можемо зауважити, що повернення назад не є корисним, якщо ми не стикалися з символом b [0] під час сканування останніх символів k-1 . Якщо ми дійсно виявили , що характер, а потім відкат до цієї позиції буде тільки корисно, якщо в тому , що до SIZED підрядка ми мали періодичне повторення.

Наприклад, якщо ми подивимось на підрядку "abcabc" десь у a , а b - "abcabd", і виявимо, що кінцевий символ b не відповідає, ми повинні врахувати, що успішний матч може розпочатися з другого "a" в підрядку, і нам слід відповідно перемістити наш поточний індекс в b назад, перш ніж продовжувати порівняння.

Тоді ідея полягає в тому, щоб зробити деяку попередню обробку на основі рядка b, щоб увімкнути зворотні посилання в b , корисні для перевірки наявності невідповідності. Так, наприклад, якщо b "acaacaacd", ми могли б ідентифікувати ці 0-базисні зворотні посилання (поставити нижче кожного символу):

index: 0 1 2 3 4 5 6 7 8
b:     a c a a c a a c d
ref:   0 0 0 1 0 0 1 0 5

Наприклад, якщо ми маємо рівні «acaacaaca» перша невідповідність відбувається на останній символ. Наведена вище інформація потім говорить алгоритму повернутися в b до індексу 5, оскільки "acaac" є загальним. І тоді, лише змінюючи поточний індекс в b, ми можемо продовжувати відповідність за поточним індексом a . У цьому прикладі збіг остаточного персонажа вдається.

Завдяки цьому ми можемо оптимізувати пошук і переконатися, що індекс у a завжди може просуватися вперед.

Ось реалізація цієї ідеї в JavaScript, використовуючи лише найпростіший синтаксис цієї мови:

function overlapCount(a, b) {
    // Deal with cases where the strings differ in length
    let startA = 0;
    if (a.length > b.length) startA = a.length - b.length;
    let endB = b.length;
    if (a.length < b.length) endB = a.length;
    // Create a back-reference for each index
    //   that should be followed in case of a mismatch.
    //   We only need B to make these references:
    let map = Array(endB);
    let k = 0; // Index that lags behind j
    map[0] = 0;
    for (let j = 1; j < endB; j++) {
        if (b[j] == b[k]) {
            map[j] = map[k]; // skip over the same character (optional optimisation)
        } else {
            map[j] = k;
        }
        while (k > 0 && b[j] != b[k]) k = map[k]; 
        if (b[j] == b[k]) k++;
    }
    // Phase 2: use these references while iterating over A
    k = 0;
    for (let i = startA; i < a.length; i++) {
        while (k > 0 && a[i] != b[k]) k = map[k];
        if (a[i] == b[k]) k++;
    }
    return k;
}

console.log(overlapCount("ababaaaabaabab", "abaababaaz")); // 7

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

Для завершення тут можна знайти той самий код, що і вище, але в інтерактивному фрагменті: ви можете вводити власні рядки та інтерактивно бачити результат:

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.