Наибольшая общая подпоследовательность
Описание
Задача нахождения наибольшей общей подпоследовательности (НОП) (longest common subsequence, LCS) — задача поиска последовательности, которая является подпоследовательностью нескольких последовательностей (обычно двух). Это классическая задача информатики, которая имеет приложения, в частности, в задаче сравнения текстовых файлов (утилита diff
), а также в биоинформатике. Она также широко используется системами контроля версий, такими как Git
, для согласования изменений в коллекции файлов.
Подпоследовательность отличается от подстроки. Например, если есть исходная последовательность ABCDEF
, то ACE
будет подпоследовательностью, но не подстрокой, а ABC
будет как подпоследовательностью, так и подстрокой.
Примеры
- НОП для последовательностей
ABCDGH
иAEDFHR
-ADH
длиной 3 - НОП для последовательностей
AGGTAB
иGXTXAYB
-GTAB
длиной 4
Реализация
НОП можно реализовать, как минимум, двумя способами.
Рекурсивный подход
// algorithms/sets/longest-common-subsequence/recursive.js
export default function lcsRecursive(a, b) {
const lcs = (a, b, memo = {}) => {
if (!a || !b) return ''
if (memo[`${a},${b}`]) {
return memo[`${a},${b}`]
}
if (a[0] === b[0]) {
return a[0] + lcs(a.slice(1), b.slice(1), memo)
}
const next1 = lcs(a.slice(1), b, memo)
const next2 = lcs(a, b.slice(1), memo)
const nextLongest = next1.length >= next2.length ? next1 : next2
memo[`${a},${b}`] = nextLongest
return nextLongest
}
return lcs(a, b)
}
Тестирование
// algorithms/sets/longest-common-subsequence/__tests__/recursive.test.js
import lcsRecursive from '../recursive'
describe('lcsRecursive', () => {
it('должен рекурсивно найти НОП двух строк', () => {
expect(lcsRecursive('', '')).toBe('')
expect(lcsRecursive('ABC', '')).toBe('')
expect(lcsRecursive('', 'ABC')).toBe('')
expect(lcsRecursive('ABABC', 'BABCA')).toBe('BABC')
expect(lcsRecursive('BABCA', 'ABCBA')).toBe('ABCA')
expect(lcsRecursive('sea', 'eat')).toBe('ea')
expect(lcsRecursive('algorithms', 'rithm')).toBe('rithm')
expect(
lcsRecursive(
'Algorithms and data structures implemented in JavaScript',
'Here you may find Algorithms and data structures that are implemented in JavaScript',
),
).toBe('Algorithms and data structures implemented in JavaScript')
})
})
Динамическое программирование
Этот подход предполагает использование матрицы (см. ролик на ютубе по ссылке в начале раздела).
// algorithms/sets/longest-common-subsequence/matrix.js
export default function lcs(a, b) {
// Инициализируем матрицу LCS
const matrix = new Array(b.length + 1)
.fill(null)
.map(() => new Array(a.length + 1).fill(null))
// Заполняем `0` первую строку
for (let i = 0; i <= a.length; i++) {
matrix[0][i] = 0
}
// Заполняем `0` первую колонку
for (let i = 0; i <= b.length; i++) {
matrix[i][0] = 0
}
// Заполняем матрицу LCS
for (let i = 1; i <= b.length; i++) {
for (let j = 1; j <= a.length; j++) {
if (b[i - 1] === a[j - 1]) {
matrix[i][j] = matrix[i - 1][j - 1] + 1
} else {
matrix[i][j] = Math.max(matrix[i - 1][j], matrix[i][j - 1])
}
}
}
// Вычисляем LCS на основе матрицы
if (!matrix[b.length][a.length]) {
return ['']
}
const lcs = []
let i = a.length
let j = b.length
while (i > 0 && j > 0) {
if (a[i - 1] === b[j - 1]) {
// Двигаемся по диагонали "влево-верх"
lcs.unshift(a[i - 1])
i--
j--
} else if (matrix[j][i] === matrix[j][i - 1]) {
// Двигаемся влево
i--
} else {
// Двигаемся вверх
j--
}
}
return lcs
}
Тестирование
// algorithms/sets/longest-common-subsequence/__tests__/matrix.test.js
import lcs from '../matrix'
describe('lcs', () => {
it('должен найти НОП двух множеств', () => {
expect(lcs([''], [''])).toEqual([''])
expect(lcs([''], ['A', 'B', 'C'])).toEqual([''])
expect(lcs(['A', 'B', 'C'], [''])).toEqual([''])
expect(lcs(['A', 'B', 'C'], ['D', 'E', 'F', 'G'])).toEqual([''])
expect(
lcs(['A', 'B', 'C', 'D', 'G', 'H'], ['A', 'E', 'D', 'F', 'H', 'R']),
).toEqual(['A', 'D', 'H'])
expect(
lcs(['A', 'G', 'G', 'T', 'A', 'B'], ['G', 'X', 'T', 'X', 'A', 'Y', 'B']),
).toEqual(['G', 'T', 'A', 'B'])
expect(
lcs(['A', 'B', 'C', 'D', 'A', 'F'], ['A', 'C', 'B', 'C', 'F']),
).toEqual(['A', 'B', 'C', 'F'])
})
})
Запускаем тесты:
npm run test ./algorithms/sets/longest-common-subsequence
