Skip to main content

yo_kv/
lcs.rs

1//! `LCS`, the longest common subsequence of two strings.
2//!
3//! This is the one string command that is not a probe and a store. It is a
4//! dynamic program over a table of `(alen + 1) * (blen + 1)` counters, so it
5//! costs the product of the two lengths in both time and memory, and Redis is
6//! open about that in its own documentation. It is here because `LCS` is part of
7//! the string group and 100 percent means 100 percent, not because it is a
8//! command anybody should put on a hot path.
9//!
10//! The backtrack is a faithful port of Redis's, quirks included. There is a
11//! branch in it that cannot be reached, because a range is always emitted at the
12//! mismatch that precedes a non contiguous match, and it is kept anyway: a port
13//! that quietly tidies up the original is a port that answers differently on
14//! some input nobody thought of.
15//!
16//! The table is capped rather than left to take the machine down. Redis's own
17//! guard is a failed allocation, which on a server that has overcommitted is a
18//! kill rather than an error, and `LCS` on two large strings is the easiest
19//! accidental denial of service in the string group.
20
21use yo_common::{Code, Error, Result};
22
23/// The largest table `LCS` will build, in entries.
24///
25/// Four bytes each, so this is 256 MiB of counters, which is two strings of
26/// eight thousand bytes each. Redis has no explicit limit and fails on the
27/// allocation instead. Ours is a number so that the failure is the same failure
28/// on every machine rather than a function of how much memory happened to be
29/// free.
30pub const LCS_MAX_CELLS: usize = 64 * 1024 * 1024;
31
32/// What Redis says when the table will not fit.
33const NO_MEMORY: &str = "Insufficient memory, failed allocating transient memory for LCS";
34
35/// One run of characters common to both strings, as `LCS IDX` reports it.
36///
37/// Both ends of both ranges are inclusive, which is Redis's convention here and
38/// the same one `GETRANGE` uses.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub struct Match {
41    /// Where the run sits in the first string, first and last byte.
42    pub a: (u32, u32),
43    /// Where the run sits in the second string, first and last byte.
44    pub b: (u32, u32),
45    /// How long the run is, which `WITHMATCHLEN` asks for.
46    pub len: u32,
47}
48
49/// The answer to `LCS IDX`.
50#[derive(Debug, Clone, Default, PartialEq, Eq)]
51pub struct Idx {
52    /// The runs, in the order Redis emits them, which is from the end of the
53    /// strings towards the start.
54    pub matches: Vec<Match>,
55    /// The length of the whole subsequence, which is not the sum of the runs
56    /// when `MINMATCHLEN` has filtered some of them out.
57    pub len: usize,
58}
59
60/// The table, and the two strings it was built from.
61struct Table<'a> {
62    cells: Vec<u32>,
63    a: &'a [u8],
64    b: &'a [u8],
65}
66
67impl<'a> Table<'a> {
68    fn build(a: &'a [u8], b: &'a [u8]) -> Result<Table<'a>> {
69        let (alen, blen) = (a.len(), b.len());
70        let cells = alen
71            .checked_add(1)
72            .and_then(|r| blen.checked_add(1).and_then(|c| r.checked_mul(c)))
73            .filter(|&n| n <= LCS_MAX_CELLS)
74            .ok_or_else(|| Error::new(Code::Full, NO_MEMORY))?;
75
76        let stride = blen + 1;
77        let mut cells = vec![0u32; cells];
78        for i in 1..=alen {
79            for j in 1..=blen {
80                let v = if a[i - 1] == b[j - 1] {
81                    cells[(i - 1) * stride + (j - 1)] + 1
82                } else {
83                    cells[(i - 1) * stride + j].max(cells[i * stride + (j - 1)])
84                };
85                cells[i * stride + j] = v;
86            }
87        }
88        Ok(Table { cells, a, b })
89    }
90
91    #[inline]
92    fn at(&self, i: usize, j: usize) -> u32 {
93        self.cells[i * (self.b.len() + 1) + j]
94    }
95
96    /// The length of the subsequence, which is the bottom right corner.
97    #[inline]
98    fn total(&self) -> usize {
99        self.at(self.a.len(), self.b.len()) as usize
100    }
101}
102
103/// The length of the longest common subsequence, which is `LCS ... LEN`.
104///
105/// No backtrack, so this is the table and nothing else.
106pub fn len(a: &[u8], b: &[u8]) -> Result<usize> {
107    Ok(Table::build(a, b)?.total())
108}
109
110/// The longest common subsequence itself, which is plain `LCS`.
111pub fn string(a: &[u8], b: &[u8]) -> Result<Vec<u8>> {
112    let t = Table::build(a, b)?;
113    let mut out = vec![0u8; t.total()];
114    walk(&t, 0, &mut out, &mut Vec::new());
115    Ok(out)
116}
117
118/// Where the two strings agree, which is `LCS ... IDX`.
119///
120/// `minmatchlen` drops any run shorter than it, and zero keeps all of them.
121/// `len` is still the length of the whole subsequence and not the length of what
122/// survived the filter, which is what Redis reports and is worth knowing before
123/// somebody tries to reconcile the two numbers.
124pub fn idx(a: &[u8], b: &[u8], minmatchlen: u32) -> Result<Idx> {
125    let t = Table::build(a, b)?;
126    let mut matches = Vec::new();
127    let mut sink = vec![0u8; t.total()];
128    walk(&t, minmatchlen, &mut sink, &mut matches);
129    Ok(Idx {
130        matches,
131        len: t.total(),
132    })
133}
134
135/// Redis's backtrack, writing the subsequence into `out` and the runs into
136/// `matches`.
137///
138/// Both outputs are filled on every call because the walk that produces one
139/// produces the other for free, and `LCS IDX` and plain `LCS` differ only in
140/// which one the caller looks at.
141fn walk(t: &Table<'_>, minmatchlen: u32, out: &mut [u8], matches: &mut Vec<Match>) {
142    let (alen, blen) = (t.a.len(), t.b.len());
143    let (mut i, mut j) = (alen, blen);
144    let mut idx = t.total();
145
146    // `alen` in the start position is Redis's way of saying no range is open,
147    // since a real start is always below it.
148    let (mut a_start, mut a_end) = (alen, 0usize);
149    let (mut b_start, mut b_end) = (0usize, 0usize);
150
151    while i > 0 && j > 0 {
152        let mut emit = false;
153        if t.a[i - 1] == t.b[j - 1] {
154            out[idx - 1] = t.a[i - 1];
155
156            if a_start == alen {
157                a_start = i - 1;
158                a_end = i - 1;
159                b_start = j - 1;
160                b_end = j - 1;
161            } else if a_start == i && b_start == j {
162                // The run is contiguous, so it grows backwards.
163                a_start -= 1;
164                b_start -= 1;
165            } else {
166                emit = true;
167            }
168            // A run that has reached the front of either string is finished,
169            // and so is the walk.
170            if a_start == 0 || b_start == 0 {
171                emit = true;
172            }
173            idx -= 1;
174            i -= 1;
175            j -= 1;
176        } else {
177            // Go whichever way the table says the subsequence came from.
178            if t.at(i - 1, j) > t.at(i, j - 1) {
179                i -= 1;
180            } else {
181                j -= 1;
182            }
183            if a_start != alen {
184                emit = true;
185            }
186        }
187
188        if emit {
189            let run = (a_end - a_start + 1) as u32;
190            if minmatchlen == 0 || run >= minmatchlen {
191                matches.push(Match {
192                    a: (a_start as u32, a_end as u32),
193                    b: (b_start as u32, b_end as u32),
194                    len: run,
195                });
196            }
197            a_start = alen;
198        }
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205
206    /// The example from Redis's own LCS documentation, which is what everybody
207    /// checks a new implementation against first.
208    #[test]
209    fn the_documented_example_comes_out_the_same() {
210        let a = b"ohmytext";
211        let b = b"mynewtext";
212        assert_eq!(string(a, b).unwrap(), b"mytext");
213        assert_eq!(len(a, b).unwrap(), 6);
214
215        let got = idx(a, b, 0).unwrap();
216        assert_eq!(got.len, 6);
217        assert_eq!(
218            got.matches,
219            vec![
220                Match {
221                    a: (4, 7),
222                    b: (5, 8),
223                    len: 4
224                },
225                Match {
226                    a: (2, 3),
227                    b: (0, 1),
228                    len: 2
229                },
230            ]
231        );
232    }
233
234    #[test]
235    fn minmatchlen_drops_the_short_runs_and_leaves_the_length_alone() {
236        let got = idx(b"ohmytext", b"mynewtext", 4).unwrap();
237        assert_eq!(got.matches.len(), 1);
238        assert_eq!(got.matches[0].len, 4);
239        // The length is the whole subsequence, not the sum of what survived.
240        assert_eq!(got.len, 6);
241    }
242
243    #[test]
244    fn an_empty_string_shares_nothing_with_anything() {
245        assert_eq!(string(b"", b"abc").unwrap(), b"");
246        assert_eq!(string(b"abc", b"").unwrap(), b"");
247        assert_eq!(string(b"", b"").unwrap(), b"");
248        assert_eq!(len(b"", b"abc").unwrap(), 0);
249        assert!(idx(b"", b"abc", 0).unwrap().matches.is_empty());
250    }
251
252    #[test]
253    fn two_identical_strings_are_one_run() {
254        let got = idx(b"hello", b"hello", 0).unwrap();
255        assert_eq!(got.len, 5);
256        assert_eq!(
257            got.matches,
258            vec![Match {
259                a: (0, 4),
260                b: (0, 4),
261                len: 5
262            }]
263        );
264        assert_eq!(string(b"hello", b"hello").unwrap(), b"hello");
265    }
266
267    #[test]
268    fn two_strings_with_nothing_in_common_share_nothing() {
269        assert_eq!(string(b"abc", b"xyz").unwrap(), b"");
270        assert_eq!(len(b"abc", b"xyz").unwrap(), 0);
271        assert!(idx(b"abc", b"xyz", 0).unwrap().matches.is_empty());
272    }
273
274    #[test]
275    fn a_run_of_one_is_still_a_run() {
276        let got = idx(b"abc", b"axc", 0).unwrap();
277        assert_eq!(got.len, 2);
278        assert_eq!(
279            got.matches,
280            vec![
281                Match {
282                    a: (2, 2),
283                    b: (2, 2),
284                    len: 1
285                },
286                Match {
287                    a: (0, 0),
288                    b: (0, 0),
289                    len: 1
290                },
291            ]
292        );
293        assert_eq!(string(b"abc", b"axc").unwrap(), b"ac");
294    }
295
296    #[test]
297    fn every_run_lands_where_it_says_it_does() {
298        // The ranges are the point of IDX, so check them against the strings
299        // rather than against a number somebody wrote down.
300        let a = &b"the quick brown fox"[..];
301        let b = &b"a quick red fox jumps"[..];
302        let got = idx(a, b, 0).unwrap();
303        for m in &got.matches {
304            let (s, e) = (m.a.0 as usize, m.a.1 as usize);
305            let (t, u) = (m.b.0 as usize, m.b.1 as usize);
306            assert_eq!(&a[s..=e], &b[t..=u], "{m:?} does not match");
307            assert_eq!(m.len as usize, e - s + 1, "{m:?} has the wrong length");
308        }
309        // The runs concatenate back into the subsequence, once they are put the
310        // right way round.
311        let mut joined = Vec::new();
312        for m in got.matches.iter().rev() {
313            joined.extend_from_slice(&a[m.a.0 as usize..=m.a.1 as usize]);
314        }
315        assert_eq!(joined, string(a, b).unwrap());
316    }
317
318    #[test]
319    fn a_table_that_will_not_fit_is_an_error_and_not_a_kill() {
320        let big = vec![b'x'; LCS_MAX_CELLS];
321        let e = len(&big, b"y").unwrap_err();
322        assert_eq!(e.code(), Code::Full);
323        assert_eq!(e.message(), NO_MEMORY);
324    }
325}