Skip to content

Commit 636a36e

Browse files
committed
✨ feat(lcs): implement myers algorithm
1 parent daec2a8 commit 636a36e

File tree

12 files changed

+825
-285
lines changed

12 files changed

+825
-285
lines changed

jest.config.mjs

+3
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ const coverageMap = {
4747
'@algorithm.ts/gomoku': {
4848
global: { branches: 92, lines: 99, statements: 99 },
4949
},
50+
'@algorithm.ts/lcs': {
51+
global: { branches: 95, lines: 97, statements: 97 },
52+
},
5053
'@algorithm.ts/kth': {
5154
global: { branches: 98 },
5255
},

packages/lcs/README.md

+29-3
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,31 @@
5151

5252
A typescript implementation to find the **lcs** (Longest Common Subsequence).
5353

54+
This package provide three different implementation of lcs algorithm. To measure the complexity of
55+
these algorithms, let the $N_1$ and $N_2$ be the subsequences length of two sequences respectively.
56+
And let $L$ be the length of the longest common subsequences, then the $D = 2L - N1 - N2$.
57+
58+
1. `myers_lcs(N1: number, N2: number, equals: (x: number, y: number) => boolean): [x: number, y: number][]`:
59+
The vanilla algorithm introduced by this paper
60+
[_An O(ND) Difference Algorithm and Its Variations_](https://mailserver.org/diff2.pdf).
61+
62+
- Time complexity: `O((N1 + N2) * D)`
63+
- Memory complexity: `O(N1 * N2)`
64+
65+
2. `myers_lcs_linear_space(N1: number, N2: number, equals: (x: number, y: number) => boolean): [x: number, y: number][]`:
66+
The linear space refinement algorithm from
67+
[_An O(ND) Difference Algorithm and Its Variations_](https://mailserver.org/diff2.pdf).
68+
69+
- Time complexity: `O((N1 + N2) * D)`
70+
- Memory complexity: `O(N1 + N2)`
71+
72+
3. `lcs_dp(N1: number, N2: number, equals: (x: number, y: number) => boolean): [x: number, y: number][]`
73+
This implementation is based on dynamic programming, and can find the minimal lexicographical
74+
lcs.
75+
76+
- Time complexity: `O(N1 * N2)`
77+
- Memory complexity: `O(N1 * N2)`
78+
5479
The following definition is quoted from Wikipedia
5580
(https://en.wikipedia.org/wiki/Longest_common_subsequence_problem):
5681

@@ -82,14 +107,14 @@ The following definition is quoted from Wikipedia
82107
- Basic
83108

84109
```typescript
85-
import { findLengthOfLCS, findMinLexicographicalLCS } from '@algorithm.ts/lcs'
110+
import { lcs_dp, lcs_myers_size } from '@algorithm.ts/lcs'
86111

87112
const s1: number[] = [1, 2, 3, 4, 6, 6, 7, 8, 6]
88113
const s2: number[] = [2, 3, 4, 7, 9, 8, 2, 3, 5, 2]
89114

90-
findLengthOfLCS(s1.length, s2.length, (x, y) => s1[x] === s2[y]) // => 5
115+
lcs_myers_size(s1.length, s2.length, (x, y) => s1[x] === s2[y]) // => 5
91116

92-
findMinLexicographicalLCS(s1.length, s2.length, (x, y) => s1[x] === s2[y])
117+
lcs_dp(s1.length, s2.length, (x, y) => s1[x] === s2[y])
93118
// => [ [1, 0], [2, 1], [3, 2], [6, 3], [7, 5] ]
94119
//
95120
// Here is why:
@@ -112,6 +137,7 @@ The following definition is quoted from Wikipedia
112137

113138
## Related
114139

140+
- [An O(ND) Difference Algorithm and Its Variations](https://mailserver.org/diff2.pdf).
115141
- [最长公共子序列(LCS) | 光和尘][lcs]
116142
- [Longest common subsequence problem | Wikipedia][wikipedia-lcs]
117143

packages/lcs/__test__/lcs.spec.ts

+205-84
Original file line numberDiff line numberDiff line change
@@ -1,122 +1,243 @@
1-
import { findLCSOfEveryRightPrefix, findLengthOfLCS, findMinLexicographicalLCS } from '../src'
1+
import {
2+
findLCSOfEveryRightPrefix,
3+
lcs_dp,
4+
lcs_myers,
5+
lcs_myers_linear_space,
6+
lcs_size_dp,
7+
lcs_size_myers,
8+
lcs_size_myers_linear_space,
9+
} from '../src'
210

3-
describe('basic', () => {
4-
it('findLengthOfLCS', function () {
5-
const lenOf = (s1: string | number[], s2: string | number[]): number =>
6-
findLengthOfLCS(s1.length, s2.length, (x, y) => s1[x] === s2[y])
11+
type ILcsSize = (N1: number, N2: number, equals: (x: number, y: number) => boolean) => number
12+
type ILcs = (
13+
N1: number,
14+
N2: number,
15+
equals: (x: number, y: number) => boolean,
16+
) => Array<[number, number]>
717

8-
expect(
9-
lenOf('f8d1d155-d14e-433f-88e1-07b54f184740', 'a00322f7-256e-46fe-ae91-8de835c57778'),
10-
).toEqual(12)
11-
expect(lenOf('abcde', 'ace')).toEqual(3)
12-
expect(lenOf('ace', 'abcde')).toEqual(3)
13-
expect(lenOf('abc', 'abc')).toEqual(3)
14-
expect(lenOf('abc', 'abce')).toEqual(3)
15-
expect(lenOf('', 'abce')).toEqual(0)
16-
expect(lenOf('abce', '')).toEqual(0)
17-
expect(lenOf('', '')).toEqual(0)
18-
expect(lenOf([1, 2, 3, 4, 6, 6, 7, 8, 6], [2, 3, 4, 7, 9, 8, 2, 3, 5, 2])).toEqual(5)
19-
expect(lenOf('abeep boop', 'beep boob blah')).toEqual(8)
20-
})
18+
function test_lcs_size(lcs_size: ILcsSize): void {
19+
const test_case = <T extends string | unknown[]>(s1: T, s2: T, expected: number): void => {
20+
const actual: number = lcs_size(s1.length, s2.length, (x, y) => s1[x] === s2[y])
21+
expect(actual).toEqual(expected)
22+
}
2123

22-
it('findMinLexicographicalLCS', function () {
23-
const lcsOf = (s1: string | number[], s2: string | number[]): Array<[number, number]> =>
24-
findMinLexicographicalLCS(s1.length, s2.length, (x, y) => s1[x] === s2[y])
24+
test_case('f8d1d155-d14e-433f-88e1-07b54f184740', 'a00322f7-256e-46fe-ae91-8de835c57778', 12)
25+
test_case('abcde', 'ace', 3)
26+
test_case('ace', 'abcde', 3)
27+
test_case('abc', 'abc', 3)
28+
test_case('abc', 'abce', 3)
29+
test_case('', 'abce', 0)
30+
test_case('abce', '', 0)
31+
test_case('', '', 0)
32+
test_case([1, 2, 3, 4, 6, 6, 7, 8, 6], [2, 3, 4, 7, 9, 8, 2, 3, 5, 2], 5)
33+
test_case('abeep boop', 'beep boob blah', 8)
34+
}
2535

26-
expect(
27-
lcsOf('f8d1d155-d14e-433f-88e1-07b54f184740', 'a00322f7-256e-46fe-ae91-8de835c57778'),
28-
).toEqual([
29-
[0, 6],
30-
[6, 10],
31-
[12, 12],
32-
[13, 13],
33-
[14, 14],
34-
[17, 16],
35-
[18, 18],
36-
[21, 20],
37-
[22, 22],
38-
[23, 23],
39-
[25, 32],
40-
[31, 35],
41-
])
36+
function test_lcs(lcs: ILcs): void {
37+
const test_case = <T extends string | unknown[]>(s1: T, s2: T, expected: number): void => {
38+
const result = lcs(s1.length, s2.length, (x, y) => s1[x] === s2[y]).map(([i]) => s1[i])
39+
const actual: T = typeof s1 === 'string' ? (result.join('') as T) : (result as T)
40+
expect(actual.length).toEqual(expected)
4241

43-
expect(lcsOf('abcde', 'ace')).toEqual([
44-
[0, 0],
45-
[2, 1],
46-
[4, 2],
47-
])
48-
expect(lcsOf('ace', 'abcde')).toEqual([
49-
[0, 0],
50-
[1, 2],
51-
[2, 4],
52-
])
53-
expect(lcsOf('abc', 'abc')).toEqual([
54-
[0, 0],
55-
[1, 1],
56-
[2, 2],
57-
])
58-
expect(lcsOf('abc', 'abce')).toEqual([
59-
[0, 0],
60-
[1, 1],
61-
[2, 2],
62-
])
63-
expect(lcsOf('', 'abce')).toEqual([])
64-
expect(lcsOf('abce', '')).toEqual([])
65-
expect(lcsOf('', '')).toEqual([])
66-
expect(lcsOf([1, 2, 3, 4, 6, 6, 7, 8, 6], [2, 3, 4, 7, 9, 8, 2, 3, 5, 2])).toEqual([
42+
let k: number = 0
43+
for (let i: number = 0; i < s1.length && k < actual.length; ++i) if (s1[i] === actual[k]) k += 1
44+
expect(k).toEqual(actual.length)
45+
46+
k = 0
47+
for (let i: number = 0; i < s2.length && k < actual.length; ++i) if (s2[i] === actual[k]) k += 1
48+
expect(k).toEqual(actual.length)
49+
}
50+
51+
test_case('f8d1d155-d14e-433f-88e1-07b54f184740', 'a00322f7-256e-46fe-ae91-8de835c57778', 12)
52+
test_case('abcde', 'ace', 3)
53+
test_case('ace', 'abcde', 3)
54+
test_case('abc', 'abc', 3)
55+
test_case('abc', 'abce', 3)
56+
test_case('', 'abce', 0)
57+
test_case('abce', '', 0)
58+
test_case('', '', 0)
59+
test_case([1, 2, 3, 4, 6, 6, 7, 8, 6], [2, 3, 4, 7, 9, 8, 2, 3, 5, 2], 5)
60+
test_case('abeep boop', 'beep boob blah', 8)
61+
test_case([1, 2, 3, 4], [2, 1, 2, 3, 4], 4)
62+
test_case([1, 2, 3, 4], [2, 3, 4, 1, 4], 3)
63+
test_case([1, 2, 3, 4], [2, 3, 1, 2, 3, 4], 4)
64+
test_case([1, 2, 3, 4], [2, 3, 1, 3, 1, 2, 3, 4], 4)
65+
test_case([1, 2, 3, 4], [2, 3, 1, 3, 1, 2, 3, 4, 3, 4], 4)
66+
test_case([2, 3, 1, 3, 1, 2, 3, 4], [1, 2, 3, 4], 4)
67+
}
68+
69+
function test_lcs_minimal_lexicographical(lcs: ILcs): void {
70+
const test_case = <T extends string | unknown[]>(
71+
s1: T,
72+
s2: T,
73+
expected: Array<[number, number]>,
74+
): void => {
75+
const actual: Array<[number, number]> = lcs(s1.length, s2.length, (x, y) => s1[x] === s2[y])
76+
expect(actual).toEqual(expected)
77+
}
78+
79+
test_case('f8d1d155-d14e-433f-88e1-07b54f184740', 'a00322f7-256e-46fe-ae91-8de835c57778', [
80+
[0, 6],
81+
[6, 10],
82+
[12, 12],
83+
[13, 13],
84+
[14, 14],
85+
[17, 16],
86+
[18, 18],
87+
[21, 20],
88+
[22, 22],
89+
[23, 23],
90+
[25, 32],
91+
[31, 35],
92+
])
93+
test_case('abcde', 'ace', [
94+
[0, 0],
95+
[2, 1],
96+
[4, 2],
97+
])
98+
test_case('ace', 'abcde', [
99+
[0, 0],
100+
[1, 2],
101+
[2, 4],
102+
])
103+
test_case('abc', 'abc', [
104+
[0, 0],
105+
[1, 1],
106+
[2, 2],
107+
])
108+
test_case('abc', 'abce', [
109+
[0, 0],
110+
[1, 1],
111+
[2, 2],
112+
])
113+
test_case('', 'abce', [])
114+
test_case('abce', '', [])
115+
test_case('', '', [])
116+
test_case(
117+
[1, 2, 3, 4, 6, 6, 7, 8, 6],
118+
[2, 3, 4, 7, 9, 8, 2, 3, 5, 2],
119+
[
67120
[1, 0],
68121
[2, 1],
69122
[3, 2],
70123
[6, 3],
71124
[7, 5],
72-
])
73-
expect(lcsOf('abeep boop', 'beep boob blah')).toEqual([
74-
[1, 0],
75-
[2, 1],
76-
[3, 2],
77-
[4, 3],
78-
[5, 4],
79-
[6, 5],
80-
[7, 6],
81-
[8, 7],
82-
])
83-
expect(lcsOf([1, 2, 3, 4], [2, 1, 2, 3, 4])).toEqual([
125+
],
126+
)
127+
test_case('abeep boop', 'beep boob blah', [
128+
[1, 0],
129+
[2, 1],
130+
[3, 2],
131+
[4, 3],
132+
[5, 4],
133+
[6, 5],
134+
[7, 6],
135+
[8, 7],
136+
])
137+
test_case(
138+
[1, 2, 3, 4],
139+
[2, 1, 2, 3, 4],
140+
[
84141
[0, 1],
85142
[1, 2],
86143
[2, 3],
87144
[3, 4],
88-
])
89-
expect(lcsOf([1, 2, 3, 4], [2, 3, 4, 1, 4])).toEqual([
145+
],
146+
)
147+
test_case(
148+
[1, 2, 3, 4],
149+
[2, 3, 4, 1, 4],
150+
[
90151
[1, 0],
91152
[2, 1],
92153
[3, 2],
93-
])
94-
expect(lcsOf([1, 2, 3, 4], [2, 3, 1, 2, 3, 4])).toEqual([
154+
],
155+
)
156+
test_case(
157+
[1, 2, 3, 4],
158+
[2, 3, 1, 2, 3, 4],
159+
[
95160
[0, 2],
96161
[1, 3],
97162
[2, 4],
98163
[3, 5],
99-
])
100-
expect(lcsOf([1, 2, 3, 4], [2, 3, 1, 3, 1, 2, 3, 4])).toEqual([
164+
],
165+
)
166+
test_case(
167+
[1, 2, 3, 4],
168+
[2, 3, 1, 3, 1, 2, 3, 4],
169+
[
101170
[0, 2],
102171
[1, 5],
103172
[2, 6],
104173
[3, 7],
105-
])
106-
expect(lcsOf([1, 2, 3, 4], [2, 3, 1, 3, 1, 2, 3, 4, 3, 4])).toEqual([
174+
],
175+
)
176+
test_case(
177+
[1, 2, 3, 4],
178+
[2, 3, 1, 3, 1, 2, 3, 4, 3, 4],
179+
[
107180
[0, 2],
108181
[1, 5],
109182
[2, 6],
110183
[3, 7],
111-
])
112-
expect(lcsOf([2, 3, 1, 3, 1, 2, 3, 4], [1, 2, 3, 4])).toEqual([
184+
],
185+
)
186+
test_case(
187+
[2, 3, 1, 3, 1, 2, 3, 4],
188+
[1, 2, 3, 4],
189+
[
113190
[2, 0],
114191
[5, 1],
115192
[6, 2],
116193
[7, 3],
117-
])
194+
],
195+
)
196+
}
197+
198+
describe('lcs size', function () {
199+
it('dp', () => {
200+
test_lcs_size(lcs_size_dp)
201+
})
202+
203+
it('myers', () => {
204+
test_lcs_size(lcs_size_myers)
118205
})
119206

207+
it('myers (linear space)', () => {
208+
test_lcs_size(lcs_size_myers_linear_space)
209+
})
210+
})
211+
212+
describe('lcs', function () {
213+
it('dp', () => {
214+
test_lcs(lcs_dp)
215+
})
216+
217+
it('myers', () => {
218+
test_lcs(lcs_myers)
219+
})
220+
221+
it('myers (linear space)', () => {
222+
test_lcs(lcs_myers_linear_space)
223+
})
224+
})
225+
226+
describe('lcs (minimal lexicographical)', () => {
227+
it('dp', () => {
228+
test_lcs_minimal_lexicographical(lcs_dp)
229+
})
230+
231+
it('myers', () => {
232+
// test_lcs_minimal_lexicographical(lcs_myers)
233+
})
234+
235+
it('myers (linear space)', () => {
236+
// test_lcs_minimal_lexicographical(lcs_myers_linear_space)
237+
})
238+
})
239+
240+
describe('others', () => {
120241
it('findLCSOfEveryRightPrefix', function () {
121242
const dpOf = (s1: string | number[], s2: string | number[]): number[] | null =>
122243
findLCSOfEveryRightPrefix(s1.length, s2.length, (x, y) => s1[x] === s2[y])

0 commit comments

Comments
 (0)