diff --git a/src/main/java/com/thealgorithms/dynamicprogramming/NeedlemanWunsch.java b/src/main/java/com/thealgorithms/dynamicprogramming/NeedlemanWunsch.java new file mode 100644 index 000000000000..13b640cf0d04 --- /dev/null +++ b/src/main/java/com/thealgorithms/dynamicprogramming/NeedlemanWunsch.java @@ -0,0 +1,60 @@ +package com.thealgorithms.dynamicprogramming; + +/** + * The Needleman–Wunsch algorithm performs global sequence alignment between two strings. + * It computes the optimal alignment score using dynamic programming, + * given a scoring scheme for matches, mismatches, and gaps. + * + * Time Complexity: O(n * m) + * Space Complexity: O(n * m) + */ +public final class NeedlemanWunsch { + + private NeedlemanWunsch() { + // Utility Class + } + + /** + * Computes the Needleman–Wunsch global alignment score between two strings. + * + * @param s1 the first string + * @param s2 the second string + * @param matchScore score for a character match + * @param mismatchPenalty penalty for a mismatch (should be negative) + * @param gapPenalty penalty for inserting a gap (should be negative) + * @return the optimal alignment score + */ + public static int align(String s1, String s2, int matchScore, int mismatchPenalty, int gapPenalty) { + if (s1 == null || s2 == null) { + throw new IllegalArgumentException("Input strings must not be null."); + } + + int n = s1.length(); + int m = s2.length(); + + int[][] dp = new int[n + 1][m + 1]; + + // Initialize gap penalties for first row and column + for (int i = 0; i <= n; i++) { + dp[i][0] = i * gapPenalty; + } + for (int j = 0; j <= m; j++) { + dp[0][j] = j * gapPenalty; + } + + // Fill the DP matrix + for (int i = 1; i <= n; i++) { + for (int j = 1; j <= m; j++) { + int matchOrMismatch = (s1.charAt(i - 1) == s2.charAt(j - 1)) ? matchScore : mismatchPenalty; + + dp[i][j] = Math.max(Math.max(dp[i - 1][j - 1] + matchOrMismatch, // match/mismatch + dp[i - 1][j] + gapPenalty // deletion (gap in s2) + ), + dp[i][j - 1] + gapPenalty // insertion (gap in s1) + ); + } + } + + return dp[n][m]; + } +} diff --git a/src/test/java/com/thealgorithms/dynamicprogramming/NeedlemanWunschTest.java b/src/test/java/com/thealgorithms/dynamicprogramming/NeedlemanWunschTest.java new file mode 100644 index 000000000000..f7ba5eea8bce --- /dev/null +++ b/src/test/java/com/thealgorithms/dynamicprogramming/NeedlemanWunschTest.java @@ -0,0 +1,60 @@ +package com.thealgorithms.dynamicprogramming; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +/** + * Unit Tests for the {@code NeedlemanWunsch} class + */ +class NeedlemanWunschTest { + + @Test + void testIdenticalStrings() { + int score = NeedlemanWunsch.align("GATTACA", "GATTACA", 1, -1, -2); + assertEquals(7, score); // All matches, 7*1 + } + + @Test + void testSimpleMismatch() { + int score = NeedlemanWunsch.align("GATTACA", "GACTATA", 1, -1, -2); + assertEquals(3, score); + } + + @Test + void testInsertion() { + int score = NeedlemanWunsch.align("GATTACA", "GATACA", 1, -1, -2); + // One deletion (gap penalty) + assertEquals(4, score); + } + + @Test + void testEmptyStrings() { + assertEquals(0, NeedlemanWunsch.align("", "", 1, -1, -2)); + } + + @Test + void testOneEmpty() { + assertEquals(-14, NeedlemanWunsch.align("GATTACA", "", 1, -1, -2)); // 7 gaps × -2 + } + + @Test + void testGapHeavyAlignment() { + int score = NeedlemanWunsch.align("AAAA", "AA", 1, -1, -2); + assertEquals(-2, score); // Two matches (2*1) + two gaps (2*-2) + } + + @ParameterizedTest + @CsvSource({"null,ABC", "ABC,null", "null,null"}) + void testNullInputs(String s1, String s2) { + // Interpret "null" literal as Java null + String first = "null".equals(s1) ? null : s1; + String second = "null".equals(s2) ? null : s2; + + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> NeedlemanWunsch.align(first, second, 1, -1, -2)); + assertEquals("Input strings must not be null.", ex.getMessage()); + } +}