最长公共子序列(LCS)
Jun 02, 2021 • 🍻 04min 39s read
🔖 最长公共子序列LCS
前言
¶一些定义:
- 公共子序列:对于给定数列
和 ,若 既是 的子序列,又是 的子序列,则称 是 和 的公共子序列。 - 最长公共子序列:公共子序列中元素个数最多的那个。
最长公共子序列的长度
¶动态规划
¶记
其中:
一个小优化
注意到
- 时间复杂度:
- 额外空间复杂度:
./findLengthOfLCS.1.ts | 26 lines.
1234567891011121314151617181920212223242526export function findLengthOfLCS( N1: number, N2: number, isEqual: (i: number, j: number) => boolean,): number { if (N1 <= 0 || N2 <= 0) return 0
const dp: number[][] = new Array(N1) for (let i = 0; i < N1; ++i) dp[i] = new Array(N2)
dp[0][0] = isEqual(0, 0) ? 1 : 0 for (let i = 1; i < N1; ++i) { dp[i][0] = dp[i - 1][0] | (isEqual(i, 0) ? 1 : 0) } for (let j = 1; j < N2; ++j) { dp[0][j] = dp[0][j - 1] | (isEqual(0, j) ? 1 : 0) }
for (let i = 1; i < N1; ++i) { for (let j = 1; j < N2; ++j) { dp[i][j] = isEqual(i, j) ? dp[i - 1][j - 1] + 1 : Math.max(dp[i][j - 1], dp[i - 1][j]) } }
return dp[N1 - 1][N2 - 1]}
转成最长上升子序列问题
¶TIP
此算法当且仅当
为方便表述,不妨记
用一个 TreeMap 或 HashMap 在
或 的复杂度下求出映射: .calc-map.ts12const f: Map<number, number> = new Map(A.length)for (let i = 0; i < A.length; ++i) f.set(A[i], i)通过映射
将数组 中的元素映射成 ,并过滤掉所有 中未出现的元素,这一步的复杂度和第一步相同,同为 或 .calc-b-prime.ts12345const B2: number[] = []for (const b of B) {const idx = f.get(b)if (idx !== undefined) B2.push(idx)}举个栗子,不妨假设
, ;则可以求出映射 为:接着,可以求出
,其中 未在 中出现,故直接过滤掉。对
求一次最长上升子序列,得到的即为 和 的最长公共子序列,这一步的复杂度为 .do-lcs.ts1return findLengthOfLIS(B2.length, (i, j) => B[i] - B[j])
综上,总复杂度为:
- 时间复杂度:
- 空间复杂度为:
最小字典序最长公共子序列
¶沿着 dp[i][j] === dp[i-1][j-1] + 1
的路径可以找到一个最长公共子序列,如果从
./findMinLexicographicalLCS.ts | 39 lines.
123456789101112131415161718192021222324252627282930313233343536373839/** * Find the least lexicographical order of the longest common subsequences. */export function findMinLexicographicalLCS( N1: number, N2: number, isEqual: (x: number, y: number) => boolean,): number[] { if (N1 <= 0 || N2 <= 0) return []
const dp: number[][] = new Array(N1) for (let i = 0; i < N1; i += 1) dp[i] = new Array(N2)
dp[0][0] = isEqual(0, 0) ? 1 : 0 for (let i = 1; i < N1; i += 1) { dp[i][0] = dp[i - 1][0] | (isEqual(i, 0) ? 1 : 0) } for (let j = 1; j < N2; j += 1) { dp[0][j] = dp[0][j - 1] | (isEqual(0, j) ? 1 : 0) }
for (let i = 1; i < N1; i += 1) { for (let j = 1; j < N2; j += 1) { dp[i][j] = isEqual(i, j) ? dp[i - 1][j - 1] + 1 : Math.max(dp[i][j - 1], dp[i - 1][j]) } }
const paired: number[] = new Array(N2).fill(-1) for (let len = dp[N1 - 1][N2 - 1], i = N1 - 1; len > 0; len -= 1) { for (let j = 0; j < N2; j += 1) { if (dp[i][j] === len) { while (i >= 0 && dp[i][j] === len) i -= 1 paired[j] = i + 1 break } } } return paired}
Text Diff
¶实际上只需要求出一个最小字典序的最长公共子序列,在此基础上将原序列patch上去就可以了。
123456789101112131415161718192021222324252627282930313233// 核心代码import { findMinLexicographicalLCS } from '@algorithm.ts/lcs'
// For simplicity, we can assume that the token is of type stringtype T = stringconst lTokens: T[] = [] // Omit the definition of lTokens.const rTokens: T[] = [] // Omit the definition of rTokens.const isEqual = (x: number, y: number): boolean => lTokens[x] === rTokens[y]
const paired: number[] = findMinLexicographicalLCS(N1, N2, isEqual)const pieces: Array<ITextDiffPiece<T>> = []for (let i = 0, j = 0, k: number; j < N2; ) { for (k = j; k < N2 && paired[k] === -1; ) k += 1 if (k < N2) { for (; i < paired[k]; i += 1) { pieces.push({ type: 'REMOVED', token: lTokens[i] }) } for (; j < k; j += 1) { pieces.push({ type: 'ADDED', token: rTokens[j] }) } pieces.push({ type: 'COMMON', token: rTokens[k] }) i += 1 j += 1 } else { for (; i < N1; i += 1) { pieces.push({ type: 'REMOVED', token: lTokens[i] }) } for (; j < N2; j += 1) { pieces.push({ type: 'ADDED', token: rTokens[j] }) } break }}
之后只需要合并 pieces (相同类型的紧邻 token 合并在一起)就好了。需要注意的是,对于 lTokens 或 rTokens 为空的情况需要做一个特判:
12if (lTokens.length <= 0) return [{ type: 'ADDED', value: rTokens.join('') }]if (rTokens.length <= 0) return [{ type: 'REMOVED', value: rTokens.join('') }]
Related
¶