diff --git a/src/algorithms/sets/longest-common-subsequence/__test__/longestCommonSubsequenceRecursive.test.js b/src/algorithms/sets/longest-common-subsequence/__test__/longestCommonSubsequenceRecursive.test.js index 3d5c9d802a..1046e3c5d0 100644 --- a/src/algorithms/sets/longest-common-subsequence/__test__/longestCommonSubsequenceRecursive.test.js +++ b/src/algorithms/sets/longest-common-subsequence/__test__/longestCommonSubsequenceRecursive.test.js @@ -1,6 +1,27 @@ import longestCommonSubsequence from '../longestCommonSubsequenceRecursive'; describe('longestCommonSubsequenceRecursive', () => { + it('should distinguish memoized suffixes containing colons', () => { + expect(longestCommonSubsequence('::a:a', 'aa:::')).toBe(':::'); + expect(longestCommonSubsequence('aa:::', '::a:a')).toBe(':::'); + }); + + it('should reuse empty subsequences instead of expanding them repeatedly', () => { + const substring = jest.spyOn(String.prototype, 'substring'); + let result; + let calls; + try { + result = longestCommonSubsequence('a'.repeat(8), 'b'.repeat(8)); + calls = substring.mock.calls.length; + } finally { + substring.mockRestore(); + } + + expect(result).toBe(''); + // There are 8 * 8 non-empty suffix pairs, with two branches per pair. + expect(calls).toBeLessThanOrEqual(2 * 8 * 8); + }); + it('should find longest common substring between two strings', () => { expect(longestCommonSubsequence('', '')).toBe(''); expect(longestCommonSubsequence('ABC', '')).toBe(''); diff --git a/src/algorithms/sets/longest-common-subsequence/longestCommonSubsequenceRecursive.js b/src/algorithms/sets/longest-common-subsequence/longestCommonSubsequenceRecursive.js index 580ba8c72f..5c40fb621c 100644 --- a/src/algorithms/sets/longest-common-subsequence/longestCommonSubsequenceRecursive.js +++ b/src/algorithms/sets/longest-common-subsequence/longestCommonSubsequenceRecursive.js @@ -4,7 +4,7 @@ * * @param {string} string1 * @param {string} string2 - * @return {number} + * @return {string} */ export default function longestCommonSubsequenceRecursive(string1, string2) { /** @@ -16,10 +16,14 @@ export default function longestCommonSubsequenceRecursive(string1, string2) { const lcs = (s1, s2, memo = {}) => { if (!s1 || !s2) return ''; - if (memo[`${s1}:${s2}`]) return memo[`${s1}:${s2}`]; + // Each call uses suffixes of the original inputs. Their lengths uniquely + // identify the pair, even when the input strings contain the separator. + const key = `${s1.length}:${s2.length}`; + if (memo[key] !== undefined) return memo[key]; if (s1[0] === s2[0]) { - return s1[0] + lcs(s1.substring(1), s2.substring(1), memo); + memo[key] = s1[0] + lcs(s1.substring(1), s2.substring(1), memo); + return memo[key]; } const nextLcs1 = lcs(s1.substring(1), s2, memo); @@ -27,7 +31,7 @@ export default function longestCommonSubsequenceRecursive(string1, string2) { const nextLongest = nextLcs1.length >= nextLcs2.length ? nextLcs1 : nextLcs2; - memo[`${s1}:${s2}`] = nextLongest; + memo[key] = nextLongest; return nextLongest; };