From a3e7dd04d9baf16592d6f0567079fb962fe1721c Mon Sep 17 00:00:00 2001 From: Leo Toff Date: Mon, 6 Dec 2021 10:17:46 -0800 Subject: [PATCH 1/5] Refactors, adds tests and comments to longest common subsequence algorithm --- .../LongestCommonSubsequence.js | 57 ++++++++++++------- .../tests/LongestCommonSubsequence.test.js | 32 +++++++++++ 2 files changed, 68 insertions(+), 21 deletions(-) create mode 100644 Dynamic-Programming/tests/LongestCommonSubsequence.test.js diff --git a/Dynamic-Programming/LongestCommonSubsequence.js b/Dynamic-Programming/LongestCommonSubsequence.js index abee50df64..557f8a96f4 100644 --- a/Dynamic-Programming/LongestCommonSubsequence.js +++ b/Dynamic-Programming/LongestCommonSubsequence.js @@ -1,32 +1,47 @@ /* - * 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” + +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. */ -function longestCommonSubsequence (x, y, str1, str2, dp) { - if (x === -1 || y === -1) { - return 0 - } else { + /** + * Finds the length of the longest common subsequence + * @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 dp = new Array(str1.length + 1).fill(0) + .map(() => new Array(str2.length + 1).fill(0)) + + function recursive(x, y) { + if (x === -1 || y === -1) { + return 0 + } + if (dp[x][y] !== 0) { return dp[x][y] + } + + if (str1[x] === str2[y]) { + dp[x][y] = 1 + recursive(x - 1, y - 1) + return dp[x][y] } 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] - } + dp[x][y] = Math.max(recursive(x - 1, y),recursive(x, y - 1)) + return dp[x][y] } } -} -// 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..6efed9ec09 --- /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); + }) +}) From 42ed59e1a9d792001cf2662335dfc56a1723fe5f Mon Sep 17 00:00:00 2001 From: Leo Toff Date: Mon, 6 Dec 2021 10:20:58 -0800 Subject: [PATCH 2/5] Refactor docs for longest common subsequence algorithm --- Dynamic-Programming/LongestCommonSubsequence.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Dynamic-Programming/LongestCommonSubsequence.js b/Dynamic-Programming/LongestCommonSubsequence.js index 557f8a96f4..5600c341a4 100644 --- a/Dynamic-Programming/LongestCommonSubsequence.js +++ b/Dynamic-Programming/LongestCommonSubsequence.js @@ -4,13 +4,16 @@ Given two sequences, find the length of longest subsequence present in both of t 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” -Solution: +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 consumption to O(min(M, N)). Try to figure out how. */ /** From 7532444cfda59b01b42159bd7cd2dede2fcb285b Mon Sep 17 00:00:00 2001 From: Leo Toff Date: Mon, 6 Dec 2021 10:24:53 -0800 Subject: [PATCH 3/5] Add links to wikipedia and leetcode --- Dynamic-Programming/LongestCommonSubsequence.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Dynamic-Programming/LongestCommonSubsequence.js b/Dynamic-Programming/LongestCommonSubsequence.js index 5600c341a4..0eeb9aa17f 100644 --- a/Dynamic-Programming/LongestCommonSubsequence.js +++ b/Dynamic-Programming/LongestCommonSubsequence.js @@ -13,7 +13,12 @@ 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 consumption to O(min(M, N)). Try to figure out how. +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/) */ /** From ac4a0cb7667857a6dfbc1e93d09ea7c307a41c9c Mon Sep 17 00:00:00 2001 From: Leo Toff Date: Mon, 6 Dec 2021 10:30:57 -0800 Subject: [PATCH 4/5] Fix styling --- .../LongestCommonSubsequence.js | 20 +++++++++---------- .../tests/LongestCommonSubsequence.test.js | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Dynamic-Programming/LongestCommonSubsequence.js b/Dynamic-Programming/LongestCommonSubsequence.js index 0eeb9aa17f..ae2f96f42e 100644 --- a/Dynamic-Programming/LongestCommonSubsequence.js +++ b/Dynamic-Programming/LongestCommonSubsequence.js @@ -21,17 +21,17 @@ References: [leetcode](https://leetcode.com/problems/longest-common-subsequence/) */ - /** - * Finds the length of the longest common subsequence - * @param {string} str1 Input string #1 - * @param {string} str2 Input string #2 - * @returns {number} Length of the longest common subsequence - */ -function longestCommonSubsequence(str1, str2) { +/** + * Finds the length of the longest common subsequence + * @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 dp = new Array(str1.length + 1).fill(0) .map(() => new Array(str2.length + 1).fill(0)) - function recursive(x, y) { + function recursive (x, y) { if (x === -1 || y === -1) { return 0 } @@ -44,12 +44,12 @@ function longestCommonSubsequence(str1, str2) { dp[x][y] = 1 + recursive(x - 1, y - 1) return dp[x][y] } else { - dp[x][y] = Math.max(recursive(x - 1, y),recursive(x, y - 1)) + dp[x][y] = Math.max(recursive(x - 1, y), recursive(x, y - 1)) return dp[x][y] } } - return recursive(str1.length - 1, str2.length - 1); + 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 index 6efed9ec09..3e5c214d56 100644 --- a/Dynamic-Programming/tests/LongestCommonSubsequence.test.js +++ b/Dynamic-Programming/tests/LongestCommonSubsequence.test.js @@ -1,4 +1,4 @@ -import { longestCommonSubsequence } from "../LongestCommonSubsequence"; +import { longestCommonSubsequence } from '../LongestCommonSubsequence' describe('LongestCommonSubsequence', () => { it('expects to return an empty string for empty inputs', () => { @@ -27,6 +27,6 @@ describe('LongestCommonSubsequence', () => { 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); + expect(longestCommonSubsequence('oxcpqrsvwf', 'shmtulqrypy')).toEqual('qr'.length) }) }) From b80dee7c1fcc5b1e20849f9571ea9186f3c38ad6 Mon Sep 17 00:00:00 2001 From: Leo Toff Date: Tue, 7 Dec 2021 09:50:43 -0800 Subject: [PATCH 5/5] Refactor variable naming and jsdoc --- .../LongestCommonSubsequence.js | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/Dynamic-Programming/LongestCommonSubsequence.js b/Dynamic-Programming/LongestCommonSubsequence.js index ae2f96f42e..23f32c43fb 100644 --- a/Dynamic-Programming/LongestCommonSubsequence.js +++ b/Dynamic-Programming/LongestCommonSubsequence.js @@ -22,30 +22,33 @@ References: */ /** - * Finds the length of the longest common subsequence + * 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 dp = new Array(str1.length + 1).fill(0) - .map(() => new Array(str2.length + 1).fill(0)) + const memo = new Array(str1.length + 1).fill(null) + .map(() => new Array(str2.length + 1).fill(null)) - function recursive (x, y) { - if (x === -1 || y === -1) { + function recursive (end1, end2) { + if (end1 === -1 || end2 === -1) { return 0 } - if (dp[x][y] !== 0) { - return dp[x][y] + if (memo[end1][end2] !== null) { + return memo[end1][end2] } - if (str1[x] === str2[y]) { - dp[x][y] = 1 + recursive(x - 1, y - 1) - return dp[x][y] + if (str1[end1] === str2[end2]) { + memo[end1][end2] = 1 + recursive(end1 - 1, end2 - 1) + return memo[end1][end2] } else { - dp[x][y] = Math.max(recursive(x - 1, y), recursive(x, y - 1)) - return dp[x][y] + memo[end1][end2] = Math.max( + recursive(end1 - 1, end2), + recursive(end1, end2 - 1) + ) + return memo[end1][end2] } }