diff --git a/Dynamic-Programming/LongestCommonSubsequence.js b/Dynamic-Programming/LongestCommonSubsequence.js index abee50df64..23f32c43fb 100644 --- a/Dynamic-Programming/LongestCommonSubsequence.js +++ b/Dynamic-Programming/LongestCommonSubsequence.js @@ -1,32 +1,58 @@ /* - * Given two sequences, find the length of longest subsequence present in both of them. - * A subsequence is a sequence that appears in the same relative order, but not necessarily contiguous. - * For example, “abc”, “abg”, “bdf”, “aeg”, ‘”acefg”, .. etc are subsequences of “abcdefg” +Problem: +Given two sequences, find the length of longest subsequence present in both of them. +A subsequence is a sequence that appears in the same relative order, but not necessarily contiguous. +For example, “abc”, “abg”, “bdf”, “aeg”, ‘”acefg”, .. etc are subsequences of “abcdefg” + +Our Solution: +We use recursion with tabular memoization. +Time complexity: O(M x N) +Solving each subproblem has a cost of O(1). Again, there are MxN subproblems, +and so we get a total time complexity of O(MxN). +Space complexity: O(M x N) +We need to store the answer for each of the MxN subproblems. + +Improvement: +It's possible to optimize space complexity to O(min(M, N)) or time to O((N + r)log(N)) +where r is the number of matches between the two sequences. Try to figure out how. + +References: +[wikipedia](https://en.wikipedia.org/wiki/Longest_common_subsequence_problem) +[leetcode](https://leetcode.com/problems/longest-common-subsequence/) */ -function longestCommonSubsequence (x, y, str1, str2, dp) { - if (x === -1 || y === -1) { - return 0 - } else { - if (dp[x][y] !== 0) { - return dp[x][y] +/** + * Finds length of the longest common subsequence among the two input string + * @param {string} str1 Input string #1 + * @param {string} str2 Input string #2 + * @returns {number} Length of the longest common subsequence + */ +function longestCommonSubsequence (str1, str2) { + const memo = new Array(str1.length + 1).fill(null) + .map(() => new Array(str2.length + 1).fill(null)) + + function recursive (end1, end2) { + if (end1 === -1 || end2 === -1) { + return 0 + } + + if (memo[end1][end2] !== null) { + return memo[end1][end2] + } + + if (str1[end1] === str2[end2]) { + memo[end1][end2] = 1 + recursive(end1 - 1, end2 - 1) + return memo[end1][end2] } else { - if (str1[x] === str2[y]) { - dp[x][y] = 1 + longestCommonSubsequence(x - 1, y - 1, str1, str2, dp) - return dp[x][y] - } else { - dp[x][y] = Math.max(longestCommonSubsequence(x - 1, y, str1, str2, dp), longestCommonSubsequence(x, y - 1, str1, str2, dp)) - return dp[x][y] - } + memo[end1][end2] = Math.max( + recursive(end1 - 1, end2), + recursive(end1, end2 - 1) + ) + return memo[end1][end2] } } -} -// Example - -// const str1 = 'ABCDGH' -// const str2 = 'AEDFHR' -// const dp = new Array(str1.length + 1).fill(0).map(x => new Array(str2.length + 1).fill(0)) -// const res = longestCommonSubsequence(str1.length - 1, str2.length - 1, str1, str2, dp) + return recursive(str1.length - 1, str2.length - 1) +} export { longestCommonSubsequence } diff --git a/Dynamic-Programming/tests/LongestCommonSubsequence.test.js b/Dynamic-Programming/tests/LongestCommonSubsequence.test.js new file mode 100644 index 0000000000..3e5c214d56 --- /dev/null +++ b/Dynamic-Programming/tests/LongestCommonSubsequence.test.js @@ -0,0 +1,32 @@ +import { longestCommonSubsequence } from '../LongestCommonSubsequence' + +describe('LongestCommonSubsequence', () => { + it('expects to return an empty string for empty inputs', () => { + expect(longestCommonSubsequence('', '')).toEqual(''.length) + expect(longestCommonSubsequence('aaa', '')).toEqual(''.length) + expect(longestCommonSubsequence('', 'bbb')).toEqual(''.length) + }) + + it('expects to return an empty string for inputs without a common subsequence', () => { + expect(longestCommonSubsequence('abc', 'deffgf')).toEqual(''.length) + expect(longestCommonSubsequence('de', 'ghm')).toEqual(''.length) + expect(longestCommonSubsequence('aupj', 'xyz')).toEqual(''.length) + }) + + it('expects to return the longest common subsequence, short inputs', () => { + expect(longestCommonSubsequence('abc', 'abc')).toEqual('abc'.length) + expect(longestCommonSubsequence('abc', 'abcd')).toEqual('abc'.length) + expect(longestCommonSubsequence('abc', 'ab')).toEqual('ab'.length) + expect(longestCommonSubsequence('abc', 'a')).toEqual('a'.length) + expect(longestCommonSubsequence('abc', 'b')).toEqual('b'.length) + expect(longestCommonSubsequence('abc', 'c')).toEqual('c'.length) + expect(longestCommonSubsequence('abd', 'abcd')).toEqual('abd'.length) + expect(longestCommonSubsequence('abd', 'ab')).toEqual('ab'.length) + expect(longestCommonSubsequence('abc', 'abd')).toEqual('ab'.length) + }) + + it('expects to return the longest common subsequence, medium-length inputs', () => { + expect(longestCommonSubsequence('bsbininm', 'jmjkbkjkv')).toEqual('b'.length) + expect(longestCommonSubsequence('oxcpqrsvwf', 'shmtulqrypy')).toEqual('qr'.length) + }) +})