diff --git a/README.md b/README.md index f997bc418..1d84d63cb 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,7 @@ $ java -cp classes com.williamfiset.algorithms.search.BinarySearch ### Tiling problems - [:movie_camera:](https://youtu.be/yn2jnmlepY8) [Tiling Dominoes](https://github.com/williamfiset/Algorithms/blob/master/src/main/java/com/williamfiset/algorithms/dp/examples/tilingdominoes/TilingDominoes.java) -- [:movie_camera:](https://www.youtube.com/watch?v=CecjOo4Zo-g) [Tiling Dominoes and Trominoes](src/main/java/com/williamfiset/algorithms/dp/examples/domino-and-tromino-tiling) +- [:movie_camera:](https://www.youtube.com/watch?v=CecjOo4Zo-g) [Tiling Dominoes and Trominoes](src/main/java/com/williamfiset/algorithms/dp/examples/dominoandtrominotiling) - [:movie_camera:](https://youtu.be/pPgBZqY_Xh0) [Mountain Scenes](https://github.com/williamfiset/Algorithms/blob/master/src/main/java/com/williamfiset/algorithms/dp/examples/scenes/Scenes.java) # Geometry diff --git a/src/main/java/com/williamfiset/algorithms/dp/BUILD b/src/main/java/com/williamfiset/algorithms/dp/BUILD index 7c8e53809..823352ea9 100644 --- a/src/main/java/com/williamfiset/algorithms/dp/BUILD +++ b/src/main/java/com/williamfiset/algorithms/dp/BUILD @@ -69,13 +69,6 @@ java_binary( runtime_deps = [":dp"], ) -# bazel run //src/main/java/com/williamfiset/algorithms/dp:LongestCommonSubstring -java_binary( - name = "LongestCommonSubstring", - main_class = "com.williamfiset.algorithms.dp.LongestCommonSubstring", - runtime_deps = [":dp"], -) - # bazel run //src/main/java/com/williamfiset/algorithms/dp:LongestIncreasingSubsequence java_binary( name = "LongestIncreasingSubsequence", diff --git a/src/main/java/com/williamfiset/algorithms/dp/EditDistanceIterative.java b/src/main/java/com/williamfiset/algorithms/dp/EditDistanceIterative.java index 25dfeea42..6d09fd1f9 100644 --- a/src/main/java/com/williamfiset/algorithms/dp/EditDistanceIterative.java +++ b/src/main/java/com/williamfiset/algorithms/dp/EditDistanceIterative.java @@ -1,74 +1,88 @@ +package com.williamfiset.algorithms.dp; + /** - * An implementation of the edit distance algorithm + * Edit Distance (Levenshtein Distance) — Iterative Bottom-Up DP + * + * Computes the minimum cost to transform string `a` into string `b` using + * three operations, each with a configurable cost: + * + * - Insert a character into `a` (cost: insertionCost) + * - Delete a character from `a` (cost: deletionCost) + * - Substitute a character in `a` (cost: substitutionCost, 0 if chars match) + * + * The DP table dp[i][j] represents the cost of converting the first i + * characters of `a` into the first j characters of `b`. Each cell is + * computed from three neighbors: diagonal (substitute/match), above (delete), + * and left (insert). + * + * See also: EditDistanceRecursive for a top-down memoized approach. * - *

Time Complexity: O(nm) + * Tested against: https://leetcode.com/problems/edit-distance + * + * Time: O(n*m) where n = a.length(), m = b.length() + * Space: O(n*m) * * @author Micah Stairs */ -package com.williamfiset.algorithms.dp; - public class EditDistanceIterative { - // Computes the cost to convert a string 'a' into a string 'b' using dynamic - // programming given the insertionCost, deletionCost and substitutionCost, O(nm) + /** + * Computes the minimum cost to convert string `a` into string `b`. + * + * @param a the source string + * @param b the target string + * @param insertionCost cost of inserting one character + * @param deletionCost cost of deleting one character + * @param substitutionCost cost of substituting one character (0 cost if chars already match) + * @return the minimum edit distance + * + * Time: O(n*m) + * Space: O(n*m) + */ public static int editDistance( String a, String b, int insertionCost, int deletionCost, int substitutionCost) { - - final int AL = a.length(), BL = b.length(); - int[][] dp = new int[AL + 1][BL + 1]; - - for (int i = 0; i <= AL; i++) { - for (int j = (i == 0 ? 1 : 0); j <= BL; j++) { - - int min = Integer.MAX_VALUE; - - // Substitution - if (i > 0 && j > 0) - min = dp[i - 1][j - 1] + (a.charAt(i - 1) == b.charAt(j - 1) ? 0 : substitutionCost); - - // Deletion - if (i > 0) min = Math.min(min, dp[i - 1][j] + deletionCost); - - // Insertion - if (j > 0) min = Math.min(min, dp[i][j - 1] + insertionCost); - - dp[i][j] = min; + if (a == null || b == null) throw new IllegalArgumentException("Input strings must not be null"); + + final int n = a.length(), m = b.length(); + int[][] dp = new int[n + 1][m + 1]; + + // Base cases: transforming a prefix of `a` into empty string (deletions only) + for (int i = 1; i <= n; i++) + dp[i][0] = i * deletionCost; + + // Base cases: transforming empty string into a prefix of `b` (insertions only) + for (int j = 1; j <= m; j++) + dp[0][j] = j * insertionCost; + + // Fill the DP table + for (int i = 1; i <= n; i++) { + for (int j = 1; j <= m; j++) { + // If characters match, no substitution cost; otherwise pay substitutionCost + int substitute = dp[i - 1][j - 1] + + (a.charAt(i - 1) == b.charAt(j - 1) ? 0 : substitutionCost); + int delete = dp[i - 1][j] + deletionCost; + int insert = dp[i][j - 1] + insertionCost; + dp[i][j] = Math.min(substitute, Math.min(delete, insert)); } } - return dp[AL][BL]; + return dp[n][m]; } public static void main(String[] args) { + // Identical strings — cost is 0 + System.out.println(editDistance("abcdefg", "abcdefg", 10, 10, 10)); // 0 - String a = "abcdefg"; - String b = "abcdefg"; - - // The strings are the same so the cost is zero - System.out.println(EditDistanceIterative.editDistance(a, b, 10, 10, 10)); - - a = "aaa"; - b = "aaabbb"; - - // 10*3 = 30 because of three insertions - System.out.println(EditDistanceIterative.editDistance(a, b, 10, 2, 3)); - - a = "1023"; - b = "10101010"; - - // Outputs 2*2 + 4*5 = 24 for 2 substitutions and 4 insertions - System.out.println(EditDistanceIterative.editDistance(a, b, 5, 7, 2)); - - a = "923456789"; - b = "12345"; + // 3 insertions at cost 10 each = 30 + System.out.println(editDistance("aaa", "aaabbb", 10, 2, 3)); // 30 - // Outputs 4*4 + 1 = 16 because we need to delete 4 - // characters and perform one substitution - System.out.println(EditDistanceIterative.editDistance(a, b, 2, 4, 1)); + // 2 substitutions (cost 2) + 4 insertions (cost 5) = 24 + System.out.println(editDistance("1023", "10101010", 5, 7, 2)); // 24 - a = "aaaaa"; - b = "aabaa"; + // 1 substitution (cost 1) + 4 deletions (cost 4) = 17 + System.out.println(editDistance("923456789", "12345", 2, 4, 1)); // 17 - System.out.println(EditDistanceIterative.editDistance(a, b, 2, 3, 10)); + // Insert 'b' then delete 'a' is cheaper than substituting 'a'->'b' + System.out.println(editDistance("aaaaa", "aabaa", 2, 3, 10)); // 5 } } diff --git a/src/main/java/com/williamfiset/algorithms/dp/EditDistanceRecursive.java b/src/main/java/com/williamfiset/algorithms/dp/EditDistanceRecursive.java index bb79e33b5..554f19d52 100644 --- a/src/main/java/com/williamfiset/algorithms/dp/EditDistanceRecursive.java +++ b/src/main/java/com/williamfiset/algorithms/dp/EditDistanceRecursive.java @@ -1,21 +1,47 @@ +package com.williamfiset.algorithms.dp; + /** - * A solution to the edit distance problem + * Edit Distance (Levenshtein Distance) — Top-Down Recursive with Memoization + * + * Computes the minimum cost to transform string `a` into string `b` using + * three operations, each with a configurable cost: + * + * - Insert a character into `a` (cost: insertionCost) + * - Delete a character from `a` (cost: deletionCost) + * - Substitute a character in `a` (cost: substitutionCost, 0 if chars match) + * + * The recursive function f(i, j) returns the cost of converting a[i..] into + * b[j..]. At each step it considers three choices — substitute/match, delete, + * insert — and memoizes results in a 2D table. + * + * Compared to EditDistanceIterative, the recursive approach only visits + * reachable states, which can be faster when many states are unreachable. + * + * Tested against: https://leetcode.com/problems/edit-distance * - *

Tested against: https://leetcode.com/problems/edit-distance + * Time: O(n*m) where n = a.length(), m = b.length() + * Space: O(n*m) * * @author William Fiset, william.alexandre.fiset@gmail.com */ -package com.williamfiset.algorithms.dp; - public class EditDistanceRecursive { - final char[] a, b; - final int insertionCost, deletionCost, substitutionCost; + private final char[] a, b; + private final int insertionCost, deletionCost, substitutionCost; + /** + * Creates an edit distance solver for the given strings and operation costs. + * + * @param a the source string + * @param b the target string + * @param insertionCost cost of inserting one character + * @param deletionCost cost of deleting one character + * @param substitutionCost cost of substituting one character (0 cost if chars already match) + */ public EditDistanceRecursive( String a, String b, int insertionCost, int deletionCost, int substitutionCost) { if (a == null || b == null) { - throw new IllegalArgumentException("Input string must not be null"); + throw new IllegalArgumentException("Input strings must not be null"); } this.a = a.toCharArray(); this.b = b.toCharArray(); @@ -24,70 +50,63 @@ public EditDistanceRecursive( this.substitutionCost = substitutionCost; } - private static int min(int... values) { - int m = Integer.MAX_VALUE; - for (int v : values) { - if (v < m) { - m = v; - } - } - return m; - } - - // Returns the Levenshtein distance to transform string `a` into string `b`. + /** + * Computes and returns the minimum edit distance from `a` to `b`. + * + * Time: O(n*m) + * Space: O(n*m) + */ public int editDistance() { Integer[][] dp = new Integer[a.length + 1][b.length + 1]; return f(dp, 0, 0); } + /** + * Recursive helper: returns the min cost to convert a[i..] into b[j..]. + */ private int f(Integer[][] dp, int i, int j) { - if (i == a.length && j == b.length) { - return 0; - } - if (i == a.length) { - return (b.length - j) * insertionCost; - } - if (j == b.length) { - return (a.length - i) * deletionCost; - } - if (dp[i][j] != null) { - return dp[i][j]; - } - int substituteOrSkip = f(dp, i + 1, j + 1) + (a[i] == b[j] ? 0 : substitutionCost); + // Both strings fully consumed — nothing left to do + if (i == a.length && j == b.length) return 0; + + // Remaining characters in `b` must be inserted + if (i == a.length) return (b.length - j) * insertionCost; + + // Remaining characters in `a` must be deleted + if (j == b.length) return (a.length - i) * deletionCost; + + if (dp[i][j] != null) return dp[i][j]; + + // Match (free) or substitute, then advance both pointers + int substitute = f(dp, i + 1, j + 1) + (a[i] == b[j] ? 0 : substitutionCost); + + // Delete a[i], advance i only int delete = f(dp, i + 1, j) + deletionCost; + + // Insert b[j] into a, advance j only int insert = f(dp, i, j + 1) + insertionCost; - return dp[i][j] = min(substituteOrSkip, delete, insert); + + return dp[i][j] = Math.min(substitute, Math.min(delete, insert)); } public static void main(String[] args) { - String a = "923456789"; - String b = "12345"; - EditDistanceRecursive solver = new EditDistanceRecursive(a, b, 100, 4, 2); - System.out.println(solver.editDistance()); - - a = "12345"; - b = "923456789"; - solver = new EditDistanceRecursive(a, b, 100, 4, 2); - System.out.println(solver.editDistance()); - - a = "aaa"; - b = "aaabbb"; - solver = new EditDistanceRecursive(a, b, 10, 2, 3); - System.out.println(solver.editDistance()); - - a = "1023"; - b = "10101010"; - solver = new EditDistanceRecursive(a, b, 5, 7, 2); - System.out.println(solver.editDistance()); - - a = "923456789"; - b = "12345"; - EditDistanceRecursive solver2 = new EditDistanceRecursive(a, b, 100, 4, 2); - System.out.println(solver2.editDistance()); - - a = "aaaaa"; - b = "aabaa"; - solver = new EditDistanceRecursive(a, b, 2, 3, 10); - System.out.println(solver.editDistance()); + // 1 substitution (cost 2) + 4 deletions (cost 4) = 18 + System.out.println( + new EditDistanceRecursive("923456789", "12345", 100, 4, 2).editDistance()); // 18 + + // Reverse direction: 1 substitution (cost 2) + 4 insertions (cost 100) = 402 + System.out.println( + new EditDistanceRecursive("12345", "923456789", 100, 4, 2).editDistance()); // 402 + + // 3 insertions at cost 10 each = 30 + System.out.println( + new EditDistanceRecursive("aaa", "aaabbb", 10, 2, 3).editDistance()); // 30 + + // 2 substitutions (cost 2) + 4 insertions (cost 5) = 24 + System.out.println( + new EditDistanceRecursive("1023", "10101010", 5, 7, 2).editDistance()); // 24 + + // Insert 'b' then delete 'a' is cheaper than substituting 'a'->'b' + System.out.println( + new EditDistanceRecursive("aaaaa", "aabaa", 2, 3, 10).editDistance()); // 5 } } diff --git a/src/main/java/com/williamfiset/algorithms/dp/LongestCommonSubsequence.java b/src/main/java/com/williamfiset/algorithms/dp/LongestCommonSubsequence.java index 0477fab94..72cd602e7 100644 --- a/src/main/java/com/williamfiset/algorithms/dp/LongestCommonSubsequence.java +++ b/src/main/java/com/williamfiset/algorithms/dp/LongestCommonSubsequence.java @@ -1,75 +1,101 @@ +package com.williamfiset.algorithms.dp; + /** - * This file contains an implementation of finding the Longest Common Subsequence (LCS) between two - * strings using dynamic programming. + * Longest Common Subsequence (LCS) + * + * Given two strings A and B, find the longest subsequence present in both. + * A subsequence is a sequence that appears in the same relative order but + * not necessarily contiguously (unlike a substring). + * + * Builds an (n+1) x (m+1) DP table where dp[i][j] = length of the LCS of + * A[0..i-1] and B[0..j-1], then backtracks to recover one LCS string. + * + * Tested against: https://leetcode.com/problems/longest-common-subsequence * - *

Time Complexity: O(nm) + * Time: O(n*m) + * Space: O(n*m) * * @author William Fiset, william.alexandre.fiset@gmail.com */ -package com.williamfiset.algorithms.dp; - public class LongestCommonSubsequence { - // Returns a non unique Longest Common Subsequence - // between the strings str1 and str2 in O(nm) - public static String lcs(char[] A, char[] B) { + /** + * Finds one Longest Common Subsequence between A and B. + * + * @param A - first string + * @param B - second string + * @return one LCS string, or null if either input is null + */ + public static String lcs(String A, String B) { + if (A == null || B == null) return null; + return lcs(A.toCharArray(), B.toCharArray()); + } + /** + * Finds one Longest Common Subsequence between A and B using bottom-up DP. + * + * Builds a table dp[i][j] = length of LCS of A[0..i-1] and B[0..j-1], + * then backtracks through the table to reconstruct the actual subsequence. + * + * @param A - first character array + * @param B - second character array + * @return one LCS string, or null if either input is null + * + * Time: O(n*m) + * Space: O(n*m) + */ + public static String lcs(char[] A, char[] B) { if (A == null || B == null) return null; final int n = A.length; final int m = B.length; - if (n == 0 || m == 0) return null; + if (n == 0 || m == 0) return ""; int[][] dp = new int[n + 1][m + 1]; - // Suppose A = a1a2..an-1an and B = b1b2..bn-1bn + // Fill the DP table for (int i = 1; i <= n; i++) { for (int j = 1; j <= m; j++) { - - // If ends match the LCS(a1a2..an-1an, b1b2..bn-1bn) = LCS(a1a2..an-1, b1b2..bn-1) + 1 - if (A[i - 1] == B[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1; - - // If the ends do not match the LCS of a1a2..an-1an and b1b2..bn-1bn is - // max( LCS(a1a2..an-1, b1b2..bn-1bn), LCS(a1a2..an-1an, b1b2..bn-1) ) - else dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); + // If characters match, extend the LCS from the diagonal + if (A[i - 1] == B[j - 1]) + dp[i][j] = dp[i - 1][j - 1] + 1; + // Otherwise take the best LCS excluding one character from either string + else + dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); } } - int lcsLen = dp[n][m]; - char[] lcs = new char[lcsLen]; - int index = 0; - - // Backtrack to find a LCS. We search for the cells - // where we included an element which are those with - // dp[i][j] != dp[i-1][j] and dp[i][j] != dp[i][j-1]) + // Backtrack from dp[n][m] to reconstruct the LCS string. + // At each cell, if the characters match, that character is part of + // the LCS — take it and move diagonally. Otherwise, move toward + // the neighbor with the larger value (up or left) to stay on the + // path that produced the optimal length. + StringBuilder sb = new StringBuilder(); int i = n, j = m; - while (i >= 1 && j >= 1) { - - int v = dp[i][j]; - // The order of these may output different LCSs - while (i > 1 && dp[i - 1][j] == v) i--; - while (j > 1 && dp[i][j - 1] == v) j--; - - // Make sure there is a match before adding - if (v > 0) lcs[lcsLen - index++ - 1] = A[i - 1]; // or B[j-1]; - - i--; - j--; + while (i > 0 && j > 0) { + if (A[i - 1] == B[j - 1]) { + sb.append(A[i - 1]); + i--; + j--; + } else if (dp[i - 1][j] >= dp[i][j - 1]) { + i--; + } else { + j--; + } } - return new String(lcs, 0, lcsLen); + return sb.reverse().toString(); } - public static void main(String[] args) { + // ==================== Main ==================== - char[] A = {'A', 'X', 'B', 'C', 'Y'}; - char[] B = {'Z', 'A', 'Y', 'W', 'B', 'C'}; - System.out.println(lcs(A, B)); // ABC + public static void main(String[] args) { + // LCS: ABC + System.out.println("LCS: " + lcs("AXBCY", "ZAYWBC")); - A = new char[] {'3', '9', '8', '3', '9', '7', '9', '7', '0'}; - B = new char[] {'3', '3', '9', '9', '9', '1', '7', '2', '0', '6'}; - System.out.println(lcs(A, B)); // 339970 + // LCS: 339970 + System.out.println("LCS: " + lcs("398397970", "3399917206")); } } diff --git a/src/main/java/com/williamfiset/algorithms/dp/LongestCommonSubstring.java b/src/main/java/com/williamfiset/algorithms/dp/LongestCommonSubstring.java deleted file mode 100644 index 52eac3f7a..000000000 --- a/src/main/java/com/williamfiset/algorithms/dp/LongestCommonSubstring.java +++ /dev/null @@ -1,75 +0,0 @@ -/** - * This file contains an implementation of finding the Longest Common Substring (LCS) between two - * strings using dynamic programming. - * - *

Time Complexity: O(nm) - * - * @author William Fiset, william.alexandre.fiset@gmail.com - */ -package com.williamfiset.algorithms.dp; - -public class LongestCommonSubstring { - - // Returns a non unique Longest Common Substring - // between the strings str1 and str2 in O(nm) - public static String lcs(char[] A, char[] B) { - - if (A == null || B == null) return null; - - final int n = A.length; - final int m = B.length; - - if (n == 0 || m == 0) return null; - - int[][] dp = new int[n + 1][m + 1]; - - // Suppose A = a1a2..an-1an and B = b1b2..bn-1bn - for (int i = 1; i <= n; i++) { - for (int j = 1; j <= m; j++) { - - // If ends match the LCS(a1a2..an-1an, b1b2..bn-1bn) = LCS(a1a2..an-1, b1b2..bn-1) + 1 - if (A[i - 1] == B[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1; - - // If the ends do not match the LCS of a1a2..an-1an and b1b2..bn-1bn is - // max( LCS(a1a2..an-1, b1b2..bn-1bn), LCS(a1a2..an-1an, b1b2..bn-1) ) - else dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); - } - } - - int lcsLen = dp[n][m]; - char[] lcs = new char[lcsLen]; - int index = 0; - - // Backtrack to find a LCS. We search for the cells - // where we included an element which are those with - // dp[i][j] != dp[i-1][j] and dp[i][j] != dp[i][j-1]) - int i = n, j = m; - while (i >= 1 && j >= 1) { - - int v = dp[i][j]; - - // The order of these may output different LCSs - while (i > 1 && dp[i - 1][j] == v) i--; - while (j > 1 && dp[i][j - 1] == v) j--; - - // Make sure there is a match before adding - if (v > 0) lcs[lcsLen - index++ - 1] = A[i - 1]; // or B[j-1]; - - i--; - j--; - } - - return new String(lcs, 0, lcsLen); - } - - public static void main(String[] args) { - - char[] A = {'A', 'X', 'B', 'C', 'Y'}; - char[] B = {'Z', 'A', 'Y', 'W', 'B', 'C'}; - System.out.println(lcs(A, B)); // ABC - - A = new char[] {'3', '9', '8', '3', '9', '7', '9', '7', '0'}; - B = new char[] {'3', '3', '9', '9', '9', '1', '7', '2', '0', '6'}; - System.out.println(lcs(A, B)); // 339970 - } -} diff --git a/src/main/java/com/williamfiset/algorithms/dp/MaximumSubarray.java b/src/main/java/com/williamfiset/algorithms/dp/MaximumSubarray.java index cec45d2a0..50bfb2705 100644 --- a/src/main/java/com/williamfiset/algorithms/dp/MaximumSubarray.java +++ b/src/main/java/com/williamfiset/algorithms/dp/MaximumSubarray.java @@ -1,37 +1,64 @@ +package com.williamfiset.algorithms.dp; + /** - * This file shows you how to find the maximal subarray in an integer array Time complexity: O(n) + * Maximum Subarray Problem (Kadane's Algorithm) + * + * Given an integer array, find the contiguous subarray with the largest sum. + * Kadane's algorithm solves this in a single pass by maintaining the best + * sum ending at the current position: + * + * sum[i] = max(arr[i], sum[i-1] + arr[i]) + * + * At each index, we either extend the current subarray or start a new one + * from the current element — whichever gives the larger sum. The global + * maximum across all positions is the answer. + * + * Tested against: https://leetcode.com/problems/maximum-subarray + * + * Time: O(n) + * Space: O(1) * * @author William Fiset, william.alexandre.fiset@gmail.com */ -package com.williamfiset.algorithms.dp; - public class MaximumSubarray { - public static void main(String[] args) { - System.out.println(maximumSubarrayValue(new int[] {-5})); - System.out.println(maximumSubarrayValue(new int[] {-5, -4, -10, -3, -1, -12, -6})); - System.out.println(maximumSubarrayValue(new int[] {1, 2, 1, -7, 2, -1, 40, -89})); - } + /** + * Returns the sum of the maximum contiguous subarray. + * + * @param arr the input array (must be non-null and non-empty) + * @return the maximum subarray sum + * + * Time: O(n) + * Space: O(1) + */ + public static long maximumSubarrayValue(int[] arr) { + if (arr == null || arr.length == 0) + throw new IllegalArgumentException("Array must not be null or empty"); - // Return the value of the maximum subarray in 'ar' - public static long maximumSubarrayValue(int[] ar) { + long maxValue = arr[0]; + long sum = arr[0]; - if (ar == null || ar.length == 0) return 0L; - int n = ar.length, maxValue, sum; + for (int i = 1; i < arr.length; i++) { + // Either start a new subarray at arr[i], or extend the current one + sum = Math.max(arr[i], sum + arr[i]); - maxValue = sum = ar[0]; + if (sum > maxValue) + maxValue = sum; + } - for (int i = 1; i < n; i++) { + return maxValue; + } - // At each step consider continuing the current subarray - // or starting a new one because adding the next element - // doesn't acutally make the subarray sum any better. - if (ar[i] > sum + ar[i]) sum = ar[i]; - else sum = sum + ar[i]; + public static void main(String[] args) { + // Single negative element + System.out.println(maximumSubarrayValue(new int[] {-5})); // -5 - if (sum > maxValue) maxValue = sum; - } + // All negative: best subarray is the largest single element (-1) + System.out.println( + maximumSubarrayValue(new int[] {-5, -4, -10, -3, -1, -12, -6})); // -1 - return maxValue; + // Mixed: subarray [2, -1, 40] has sum 41 + System.out.println( + maximumSubarrayValue(new int[] {1, 2, 1, -7, 2, -1, 40, -89})); // 41 } } diff --git a/src/test/java/com/williamfiset/algorithms/dp/BUILD b/src/test/java/com/williamfiset/algorithms/dp/BUILD index 91a1c716e..8c6048b94 100644 --- a/src/test/java/com/williamfiset/algorithms/dp/BUILD +++ b/src/test/java/com/williamfiset/algorithms/dp/BUILD @@ -50,5 +50,38 @@ java_test( deps = TEST_DEPS, ) +# bazel test //src/test/java/com/williamfiset/algorithms/dp:EditDistanceIterativeTest +java_test( + name = "EditDistanceIterativeTest", + srcs = ["EditDistanceIterativeTest.java"], + main_class = "org.junit.platform.console.ConsoleLauncher", + use_testrunner = False, + args = ["--select-class=com.williamfiset.algorithms.dp.EditDistanceIterativeTest"], + runtime_deps = JUNIT5_RUNTIME_DEPS, + deps = TEST_DEPS, +) + +# bazel test //src/test/java/com/williamfiset/algorithms/dp:MaximumSubarrayTest +java_test( + name = "MaximumSubarrayTest", + srcs = ["MaximumSubarrayTest.java"], + main_class = "org.junit.platform.console.ConsoleLauncher", + use_testrunner = False, + args = ["--select-class=com.williamfiset.algorithms.dp.MaximumSubarrayTest"], + runtime_deps = JUNIT5_RUNTIME_DEPS, + deps = TEST_DEPS, +) + +# bazel test //src/test/java/com/williamfiset/algorithms/dp:LongestCommonSubsequenceTest +java_test( + name = "LongestCommonSubsequenceTest", + srcs = ["LongestCommonSubsequenceTest.java"], + main_class = "org.junit.platform.console.ConsoleLauncher", + use_testrunner = False, + args = ["--select-class=com.williamfiset.algorithms.dp.LongestCommonSubsequenceTest"], + runtime_deps = JUNIT5_RUNTIME_DEPS, + deps = TEST_DEPS, +) + # Run all tests # bazel test //src/test/java/com/williamfiset/algorithms/dp:all diff --git a/src/test/java/com/williamfiset/algorithms/dp/EditDistanceIterativeTest.java b/src/test/java/com/williamfiset/algorithms/dp/EditDistanceIterativeTest.java new file mode 100644 index 000000000..78b8b8269 --- /dev/null +++ b/src/test/java/com/williamfiset/algorithms/dp/EditDistanceIterativeTest.java @@ -0,0 +1,110 @@ +package com.williamfiset.algorithms.dp; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +public class EditDistanceIterativeTest { + + @Test + public void testNullInputA() { + assertThrows( + IllegalArgumentException.class, + () -> EditDistanceIterative.editDistance(null, "abc", 1, 1, 1)); + } + + @Test + public void testNullInputB() { + assertThrows( + IllegalArgumentException.class, + () -> EditDistanceIterative.editDistance("abc", null, 1, 1, 1)); + } + + @Test + public void testIdenticalStrings() { + assertThat(EditDistanceIterative.editDistance("abcdefg", "abcdefg", 10, 10, 10)).isEqualTo(0); + } + + @Test + public void testBothEmpty() { + assertThat(EditDistanceIterative.editDistance("", "", 1, 1, 1)).isEqualTo(0); + } + + @Test + public void testEmptyToNonEmpty() { + // Converting "" to "abc" requires 3 insertions at cost 5 each + assertThat(EditDistanceIterative.editDistance("", "abc", 5, 1, 1)).isEqualTo(15); + } + + @Test + public void testNonEmptyToEmpty() { + // Converting "abc" to "" requires 3 deletions at cost 4 each + assertThat(EditDistanceIterative.editDistance("abc", "", 1, 4, 1)).isEqualTo(12); + } + + @Test + public void testInsertionsOnly() { + // "aaa" -> "aaabbb" requires 3 insertions at cost 10 + assertThat(EditDistanceIterative.editDistance("aaa", "aaabbb", 10, 2, 3)).isEqualTo(30); + } + + @Test + public void testSubstitutionsAndInsertions() { + // "1023" -> "10101010": 2 substitutions (cost 2) + 4 insertions (cost 5) = 24 + assertThat(EditDistanceIterative.editDistance("1023", "10101010", 5, 7, 2)).isEqualTo(24); + } + + @Test + public void testDeletionsAndSubstitution() { + // "923456789" -> "12345": 1 substitution (cost 1) + 4 deletions (cost 4) = 17 + assertThat(EditDistanceIterative.editDistance("923456789", "12345", 2, 4, 1)).isEqualTo(17); + } + + /** When substitution is expensive, insert+delete can be cheaper. */ + @Test + public void testInsertDeleteCheaperThanSubstitute() { + // "aaaaa" -> "aabaa": substituting costs 10, but insert 'b' (2) + delete 'a' (3) = 5 + assertThat(EditDistanceIterative.editDistance("aaaaa", "aabaa", 2, 3, 10)).isEqualTo(5); + } + + @Test + public void testSingleCharSubstitution() { + assertThat(EditDistanceIterative.editDistance("a", "b", 1, 1, 1)).isEqualTo(1); + } + + @Test + public void testSingleCharInsertion() { + assertThat(EditDistanceIterative.editDistance("a", "ab", 3, 1, 1)).isEqualTo(3); + } + + @Test + public void testSingleCharDeletion() { + assertThat(EditDistanceIterative.editDistance("ab", "a", 1, 7, 1)).isEqualTo(7); + } + + /** Verify iterative and recursive solvers agree on the same inputs. */ + @Test + public void testMatchesRecursiveSolver() { + String[][] pairs = { + {"abcdefg", "abcdefg"}, + {"aaa", "aaabbb"}, + {"1023", "10101010"}, + {"923456789", "12345"}, + {"aaaaa", "aabaa"}, + {"kitten", "sitting"}, + {"", "hello"}, + {"world", ""}, + }; + int[][] costs = {{10, 10, 10}, {10, 2, 3}, {5, 7, 2}, {2, 4, 1}, {2, 3, 10}, {1, 1, 1}, {1, 1, 1}, {1, 1, 1}}; + + for (int k = 0; k < pairs.length; k++) { + String a = pairs[k][0], b = pairs[k][1]; + int ins = costs[k][0], del = costs[k][1], sub = costs[k][2]; + + int iterative = EditDistanceIterative.editDistance(a, b, ins, del, sub); + int recursive = new EditDistanceRecursive(a, b, ins, del, sub).editDistance(); + assertThat(iterative).isEqualTo(recursive); + } + } +} diff --git a/src/test/java/com/williamfiset/algorithms/dp/LongestCommonSubsequenceTest.java b/src/test/java/com/williamfiset/algorithms/dp/LongestCommonSubsequenceTest.java new file mode 100644 index 000000000..57f060f23 --- /dev/null +++ b/src/test/java/com/williamfiset/algorithms/dp/LongestCommonSubsequenceTest.java @@ -0,0 +1,85 @@ +package com.williamfiset.algorithms.dp; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.jupiter.api.Test; + +public class LongestCommonSubsequenceTest { + + @Test + public void testNullInputs() { + assertThat(LongestCommonSubsequence.lcs((String) null, "abc")).isNull(); + assertThat(LongestCommonSubsequence.lcs("abc", (String) null)).isNull(); + assertThat(LongestCommonSubsequence.lcs((char[]) null, "abc".toCharArray())).isNull(); + } + + @Test + public void testEmptyInputs() { + assertThat(LongestCommonSubsequence.lcs("", "abc")).isEmpty(); + assertThat(LongestCommonSubsequence.lcs("abc", "")).isEmpty(); + assertThat(LongestCommonSubsequence.lcs("", "")).isEmpty(); + } + + @Test + public void testSingleCharMatch() { + assertThat(LongestCommonSubsequence.lcs("X", "X")).isEqualTo("X"); + } + + @Test + public void testSingleCharNoMatch() { + assertThat(LongestCommonSubsequence.lcs("X", "Y")).isEmpty(); + } + + @Test + public void testBasicLCS() { + assertThat(LongestCommonSubsequence.lcs("AXBCY", "ZAYWBC")).isEqualTo("ABC"); + } + + @Test + public void testCharArrayOverload() { + assertThat(LongestCommonSubsequence.lcs("AXBCY".toCharArray(), "ZAYWBC".toCharArray())) + .isEqualTo("ABC"); + } + + /** The LCS is not unique for this input; just verify the length. */ + @Test + public void testNumericSequence() { + assertThat(LongestCommonSubsequence.lcs("398397970", "3399917206").length()).isEqualTo(6); + } + + @Test + public void testNoCommonSubsequence() { + assertThat(LongestCommonSubsequence.lcs("ABC", "XYZ")).isEmpty(); + } + + @Test + public void testIdenticalStrings() { + assertThat(LongestCommonSubsequence.lcs("ABCDE", "ABCDE")).isEqualTo("ABCDE"); + } + + @Test + public void testOneIsSubsequence() { + assertThat(LongestCommonSubsequence.lcs("abcde", "ace")).isEqualTo("ace"); + } + + @Test + public void testPrefixMatch() { + assertThat(LongestCommonSubsequence.lcs("ABCXYZ", "ABC")).isEqualTo("ABC"); + } + + @Test + public void testSuffixMatch() { + assertThat(LongestCommonSubsequence.lcs("XYZABC", "ABC")).isEqualTo("ABC"); + } + + @Test + public void testRepeatedCharacters() { + assertThat(LongestCommonSubsequence.lcs("AAAA", "AA")).isEqualTo("AA"); + } + + @Test + public void testInterleavedPattern() { + // LCS of "ABAB" and "BABA" is length 3 + assertThat(LongestCommonSubsequence.lcs("ABAB", "BABA").length()).isEqualTo(3); + } +} diff --git a/src/test/java/com/williamfiset/algorithms/dp/MaximumSubarrayTest.java b/src/test/java/com/williamfiset/algorithms/dp/MaximumSubarrayTest.java new file mode 100644 index 000000000..cb9adfeb4 --- /dev/null +++ b/src/test/java/com/williamfiset/algorithms/dp/MaximumSubarrayTest.java @@ -0,0 +1,94 @@ +package com.williamfiset.algorithms.dp; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +public class MaximumSubarrayTest { + + @Test + public void testNullInput() { + assertThrows( + IllegalArgumentException.class, () -> MaximumSubarray.maximumSubarrayValue(null)); + } + + @Test + public void testEmptyInput() { + assertThrows( + IllegalArgumentException.class, () -> MaximumSubarray.maximumSubarrayValue(new int[] {})); + } + + @Test + public void testSinglePositive() { + assertThat(MaximumSubarray.maximumSubarrayValue(new int[] {7})).isEqualTo(7); + } + + @Test + public void testSingleNegative() { + assertThat(MaximumSubarray.maximumSubarrayValue(new int[] {-5})).isEqualTo(-5); + } + + @Test + public void testSingleZero() { + assertThat(MaximumSubarray.maximumSubarrayValue(new int[] {0})).isEqualTo(0); + } + + @Test + public void testAllPositive() { + // Entire array is the max subarray + assertThat(MaximumSubarray.maximumSubarrayValue(new int[] {1, 2, 3, 4})).isEqualTo(10); + } + + /** All negative: the max subarray is the single largest element. */ + @Test + public void testAllNegative() { + assertThat(MaximumSubarray.maximumSubarrayValue( + new int[] {-5, -4, -10, -3, -1, -12, -6})).isEqualTo(-1); + } + + @Test + public void testMixedWithNegativeReset() { + // [2, -1, 40] = 41 + assertThat(MaximumSubarray.maximumSubarrayValue( + new int[] {1, 2, 1, -7, 2, -1, 40, -89})).isEqualTo(41); + } + + @Test + public void testMaxSubarrayAtStart() { + // [5, 4] = 9 + assertThat(MaximumSubarray.maximumSubarrayValue(new int[] {5, 4, -20, 1, 2})).isEqualTo(9); + } + + @Test + public void testMaxSubarrayAtEnd() { + // [3, 7] = 10 + assertThat(MaximumSubarray.maximumSubarrayValue(new int[] {1, -20, 3, 7})).isEqualTo(10); + } + + @Test + public void testMaxSubarrayInMiddle() { + // [4, -1, 5] = 8 + assertThat(MaximumSubarray.maximumSubarrayValue( + new int[] {-3, 4, -1, 5, -10})).isEqualTo(8); + } + + @Test + public void testEntireArrayIsMax() { + assertThat(MaximumSubarray.maximumSubarrayValue( + new int[] {2, -1, 3, -1, 2})).isEqualTo(5); + } + + @Test + public void testAllZeros() { + assertThat(MaximumSubarray.maximumSubarrayValue(new int[] {0, 0, 0})).isEqualTo(0); + } + + @Test + public void testLargeValues() { + // Ensure long return type handles values beyond int range + assertThat(MaximumSubarray.maximumSubarrayValue( + new int[] {Integer.MAX_VALUE, Integer.MAX_VALUE})) + .isEqualTo(2L * Integer.MAX_VALUE); + } +}