Учитывая две последовательности, найдите максимальное перекрытие между окончанием одного и началом другого - PullRequest
11 голосов
/ 26 февраля 2020

Мне нужно найти эффективный (псевдо) код для решения следующей проблемы:

Учитывая две последовательности (не обязательно отличных) целых чисел (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) был бы очевидным l oop на d, а затем внутренним l oop на предметах для проверки требуемого условия до достижения максимального значения d. Но я подозреваю, что лучше, чем это возможно.

Ответы [ 3 ]

5 голосов
/ 27 февраля 2020

Вы можете использовать алгоритм 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 голосов
/ 26 февраля 2020

Для 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] - это га sh с 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] - га sh с ai до an.

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


ВНИМАНИЕ : это метод ha sh, значения могут быть большими, и вам, возможно, придется использовать метод быстрого возведения в степень и модульную арифметику , которая может (вряд ли) дает вам столкновений , что делает этот метод не совсем безопасным. Хорошей практикой является выбор базы B в качестве действительно большого простого числа (по крайней мере, больше, чем наибольшее значение в ваших массивах). Вы также должны быть осторожны, так как ограничения чисел могут переполняться на каждом шаге, поэтому вам придется использовать (по модулю K) в каждой операции (где K может быть простое число больше B).

Это означает, что две разные последовательности могут иметь одинаковое значение ha sh, но две равные последовательности будут всегда иметь одинаковые значения ha sh.

2 голосов
/ 27 февраля 2020

Это действительно может быть сделано за линейное время, O (n) и O (n) дополнительное пространство. Я предполагаю, что входные массивы являются символьными строками, но это не обязательно.

Наивный метод - после сопоставления k равных символов - находит символ, который не соответствует и go back k-1 единиц в a , сбросьте индекс в b и затем начните процесс сопоставления оттуда. Это ясно представляет O (n²) наихудший случай.

Чтобы избежать этого процесса обратного отслеживания, мы можем заметить, что возврат назад бесполезен, если мы не встретили символ b [0] во время сканирование последних k-1 символов. Если бы мы действительно нашли этот символ, то возврат к этой позиции был бы полезен только в том случае, если бы в подстроке размером k у нас было повторение periodi c.

Например, если мы посмотрим на подстроку "abcab c" где-то в 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", первое несоответствие происходит с последним персонажем. Приведенная выше информация затем сообщает алгоритму go обратно в b для индекса 5, поскольку "acaa c" является распространенным. И затем, изменив только текущий индекс в b , мы можем продолжить сопоставление при текущем индексе a . В этом примере совпадение с последним символом завершается успешно.

С этим мы можем оптимизировать поиск и убедиться, что индекс в a всегда может двигаться вперед.

Вот реализация этой идеи в JavaScript с использованием самого базового c синтаксиса только этого языка:

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

В завершение, здесь вы можете найти тот же код как указано выше, но в интерактивном фрагменте: вы можете ввести свои собственные строки и просмотреть результат в интерактивном режиме:

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

// I/O handling

let [inputA, inputB] = document.querySelectorAll("input");
let output = document.querySelector("pre");

function refresh() {
    let a = inputA.value;
    let b = inputB.value;
    let count = overlapCount(a, b);
    let padding = a.length - count;
    // Apply some HTML formatting to highlight the overlap:
    if (count) {
        a = a.slice(0, -count) + "<b>" + a.slice(-count) + "</b>";
        b = "<b>" + b.slice(0, count) + "</b>" + b.slice(count);
    }
    output.innerHTML = count + " overlapping characters:\n" + 
                         a + "\n" + 
                         " ".repeat(padding) + b;
}

document.addEventListener("input", refresh);
refresh();
body { font-family: monospace }
b { background:yellow }
input { width: 90% }
a: 
b:
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...