Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 0 additions & 7 deletions src/main/java/com/williamfiset/algorithms/dp/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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
}
}
Original file line number Diff line number Diff line change
@@ -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
*
* <p>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();
Expand All @@ -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
}
}
Loading
Loading