From 09661a259ebdaac42430d75bbee30170e95d64da Mon Sep 17 00:00:00 2001 From: Ionut Hodoroaga Date: Mon, 23 Sep 2024 10:52:09 +0300 Subject: [PATCH 1/4] Added Manacher Algorithm --- .../com/thealgorithms/strings/Manacher.java | 87 +++++++++++++++++++ .../thealgorithms/strings/ManacherTest.java | 36 ++++++++ 2 files changed, 123 insertions(+) create mode 100644 src/main/java/com/thealgorithms/strings/Manacher.java create mode 100644 src/test/java/com/thealgorithms/strings/ManacherTest.java diff --git a/src/main/java/com/thealgorithms/strings/Manacher.java b/src/main/java/com/thealgorithms/strings/Manacher.java new file mode 100644 index 000000000000..3f3540990259 --- /dev/null +++ b/src/main/java/com/thealgorithms/strings/Manacher.java @@ -0,0 +1,87 @@ +package com.thealgorithms.strings; + +/** + * Wikipedia: https://en.wikipedia.org/wiki/Longest_palindromic_substring#Manacher's_algorithm + */ +public final class Manacher { + + // Private constructor to prevent instantiation + private Manacher() { + } + + /** + * Test code for Manacher's Algorithm + */ + public static void main(String[] args) { + assert longestPalindrome("babad").equals("bab") || longestPalindrome("babad").equals("aba"); + assert longestPalindrome("cbbd").equals("bb"); + assert longestPalindrome("a").equals("a"); + assert longestPalindrome("ac").equals("a") || longestPalindrome("ac").equals("c"); + } + + /** + * Finds the longest palindromic substring using Manacher's Algorithm + * + * @param s The input string + * @return The longest palindromic substring in {@code s} + */ + public static String longestPalindrome(String s) { + // Preprocess the string to avoid even-length palindrome issues + String processedString = preprocess(s); + int n = processedString.length(); + int[] P = new int[n]; // Array to store the radius of palindromes + int center = 0, rightBoundary = 0; // Current center and right boundary of the palindrome + int maxLen = 0, centerIndex = 0; // To track the longest palindrome + + // Iterate over the preprocessed string to calculate the palindrome radii + for (int i = 1; i < n - 1; i++) { + // Mirror the current index i to find its corresponding mirrored index + int mirror = 2 * center - i; + + // If the current index is within the right boundary, mirror the palindrome radius + if (i < rightBoundary) { + P[i] = Math.min(rightBoundary - i, P[mirror]); + } + + // Try to expand the palindrome centered at i + while (processedString.charAt(i + 1 + P[i]) == processedString.charAt(i - 1 - P[i])) { + P[i]++; + } + + // Update center and right boundary if palindrome expands beyond current right boundary + if (i + P[i] > rightBoundary) { + center = i; + rightBoundary = i + P[i]; + } + + // Track the maximum length and center index of the longest palindrome found so far + if (P[i] > maxLen) { + maxLen = P[i]; + centerIndex = i; + } + } + + // Extract the longest palindrome from the original string + int start = (centerIndex - maxLen) / 2; // Get the starting index in the original string + return s.substring(start, start + maxLen); + } + + /** + * Preprocesses the input string by inserting a special character ('#') between each character + * and adding '^' at the start and '$' at the end to avoid boundary conditions. + * + * @param s The original string + * @return The preprocessed string with additional characters + */ + private static String preprocess(String s) { + if (s.isEmpty()) { + return "^$"; + } + StringBuilder sb = new StringBuilder("^"); + for (char c : s.toCharArray()) { + sb.append('#').append(c); + } + sb.append("#$"); + return sb.toString(); + } +} diff --git a/src/test/java/com/thealgorithms/strings/ManacherTest.java b/src/test/java/com/thealgorithms/strings/ManacherTest.java new file mode 100644 index 000000000000..3394fd60753b --- /dev/null +++ b/src/test/java/com/thealgorithms/strings/ManacherTest.java @@ -0,0 +1,36 @@ +package com.thealgorithms.strings; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; + +public class ManacherTest { + + @Test + public void testLongestPalindrome() { + assertEquals("aabcdefggfedcbaa", Manacher.longestPalindrome("abracadabraabcdefggfedcbaabracadabra")); // Long string with embedded palindrome + assertEquals("racecar", Manacher.longestPalindrome("somelongtextwithracecarmiddletext")); // Longer string with racecar palindrome + assertEquals("ananananananana", Manacher.longestPalindrome("bananananananana")); // Repetitive pattern with palindrome + assertEquals("defgfed", Manacher.longestPalindrome("qwertydefgfedzxcvbnm")); // Palindrome in middle of long string + assertEquals("abcdefghijklmnopqrstuvwxyzzyxwvutsrqponmlkjihgfedcba", Manacher.longestPalindrome("abcdefghijklmnopqrstuvwxyzzyxwvutsrqponmlkjihgfedcba")); // Symmetrical section + } + + @Test + public void testEmptyAndSingle() { + assertEquals("", Manacher.longestPalindrome("")); // Empty string + assertEquals("a", Manacher.longestPalindrome("a")); // Single character + } + + @Test + public void testComplexCases() { + assertEquals("tattarrattat", Manacher.longestPalindrome("abcdefghijklmnopqrstuvwxyzttattarrattatabcdefghijklmnopqrstuvwxyz")); // Long palindrome inside a large string + assertEquals("aaaaabaaaaa", Manacher.longestPalindrome("aaaaabaaaaacbaaaaa")); // Large repetitive character set + assertEquals("abcdefghhgfedcba", Manacher.longestPalindrome("sometextrandomabcdefgabcdefghhgfedcbahijklmnopqrstuvwxyz")); // Large string with clear palindromic section + assertEquals("madaminedenimadam", Manacher.longestPalindrome("therewasasignthatsaidmadaminedenimadamitwasthereallalong")); // Famous palindrome within a long string + } + + @Test + public void testSentencePalindromes() { + assertEquals("lanacanal", Manacher.longestPalindrome("XThisisalongtextbuthiddeninsideisAmanaplanacanalPanamaWhichweknowisfamous")); + assertEquals("everoddoreve", Manacher.longestPalindrome("AverylongstringthatcontainsNeveroddoreveninahiddenmanner")); // Another sentence-like palindrome + } +} From b4ae4aad3e660b80a8efb3ae692a04f1768305ea Mon Sep 17 00:00:00 2001 From: Ionut Hodoroaga Date: Mon, 23 Sep 2024 10:59:10 +0300 Subject: [PATCH 2/4] Formatted ManacherTest.java --- src/test/java/com/thealgorithms/strings/ManacherTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/java/com/thealgorithms/strings/ManacherTest.java b/src/test/java/com/thealgorithms/strings/ManacherTest.java index 3394fd60753b..01564a7fd35a 100644 --- a/src/test/java/com/thealgorithms/strings/ManacherTest.java +++ b/src/test/java/com/thealgorithms/strings/ManacherTest.java @@ -1,6 +1,7 @@ package com.thealgorithms.strings; import static org.junit.jupiter.api.Assertions.assertEquals; + import org.junit.jupiter.api.Test; public class ManacherTest { From 4c11045ff1ad80bfb8acb87a3471bf4b834808bb Mon Sep 17 00:00:00 2001 From: Ionut Hodoroaga Date: Mon, 23 Sep 2024 11:02:23 +0300 Subject: [PATCH 3/4] Formatted Manacher.java --- .../com/thealgorithms/strings/Manacher.java | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/thealgorithms/strings/Manacher.java b/src/main/java/com/thealgorithms/strings/Manacher.java index 3f3540990259..07a07da80c0f 100644 --- a/src/main/java/com/thealgorithms/strings/Manacher.java +++ b/src/main/java/com/thealgorithms/strings/Manacher.java @@ -29,9 +29,13 @@ public static String longestPalindrome(String s) { // Preprocess the string to avoid even-length palindrome issues String processedString = preprocess(s); int n = processedString.length(); - int[] P = new int[n]; // Array to store the radius of palindromes - int center = 0, rightBoundary = 0; // Current center and right boundary of the palindrome - int maxLen = 0, centerIndex = 0; // To track the longest palindrome + int[] p = new int[n]; // Array to store the radius of palindromes + + // Separate variable declarations into individual statements + int center = 0; + int rightBoundary = 0; + int maxLen = 0; + int centerIndex = 0; // Iterate over the preprocessed string to calculate the palindrome radii for (int i = 1; i < n - 1; i++) { @@ -40,23 +44,23 @@ public static String longestPalindrome(String s) { // If the current index is within the right boundary, mirror the palindrome radius if (i < rightBoundary) { - P[i] = Math.min(rightBoundary - i, P[mirror]); + p[i] = Math.min(rightBoundary - i, p[mirror]); } // Try to expand the palindrome centered at i - while (processedString.charAt(i + 1 + P[i]) == processedString.charAt(i - 1 - P[i])) { - P[i]++; + while (processedString.charAt(i + 1 + p[i]) == processedString.charAt(i - 1 - p[i])) { + p[i]++; } // Update center and right boundary if palindrome expands beyond current right boundary - if (i + P[i] > rightBoundary) { + if (i + p[i] > rightBoundary) { center = i; - rightBoundary = i + P[i]; + rightBoundary = i + p[i]; } // Track the maximum length and center index of the longest palindrome found so far - if (P[i] > maxLen) { - maxLen = P[i]; + if (p[i] > maxLen) { + maxLen = p[i]; centerIndex = i; } } From c1eca2624669fceda825c0cea84af4437569ca2d Mon Sep 17 00:00:00 2001 From: Ionut Hodoroaga Date: Tue, 24 Sep 2024 12:31:11 +0300 Subject: [PATCH 4/4] Refactor: Update Manacher's algorithm tests and improve readability - Added parameterized tests for longestPalindrome, empty cases, complex cases, and sentence palindromes. - Removed unnecessary comments for cleaner code. - Renamed variable `p` to `palindromeLengths` for clarity based on code review feedback. --- .../com/thealgorithms/strings/Manacher.java | 44 ++++---------- .../thealgorithms/strings/ManacherTest.java | 60 ++++++++++++------- 2 files changed, 49 insertions(+), 55 deletions(-) diff --git a/src/main/java/com/thealgorithms/strings/Manacher.java b/src/main/java/com/thealgorithms/strings/Manacher.java index 07a07da80c0f..34c303822bee 100644 --- a/src/main/java/com/thealgorithms/strings/Manacher.java +++ b/src/main/java/com/thealgorithms/strings/Manacher.java @@ -5,20 +5,9 @@ */ public final class Manacher { - // Private constructor to prevent instantiation private Manacher() { } - /** - * Test code for Manacher's Algorithm - */ - public static void main(String[] args) { - assert longestPalindrome("babad").equals("bab") || longestPalindrome("babad").equals("aba"); - assert longestPalindrome("cbbd").equals("bb"); - assert longestPalindrome("a").equals("a"); - assert longestPalindrome("ac").equals("a") || longestPalindrome("ac").equals("c"); - } - /** * Finds the longest palindromic substring using Manacher's Algorithm * @@ -26,47 +15,36 @@ public static void main(String[] args) { * @return The longest palindromic substring in {@code s} */ public static String longestPalindrome(String s) { - // Preprocess the string to avoid even-length palindrome issues - String processedString = preprocess(s); - int n = processedString.length(); - int[] p = new int[n]; // Array to store the radius of palindromes - - // Separate variable declarations into individual statements + final String processedString = preprocess(s); + int[] palindromeLengths = new int[processedString.length()]; int center = 0; int rightBoundary = 0; int maxLen = 0; int centerIndex = 0; - // Iterate over the preprocessed string to calculate the palindrome radii - for (int i = 1; i < n - 1; i++) { - // Mirror the current index i to find its corresponding mirrored index + for (int i = 1; i < processedString.length() - 1; i++) { int mirror = 2 * center - i; - // If the current index is within the right boundary, mirror the palindrome radius if (i < rightBoundary) { - p[i] = Math.min(rightBoundary - i, p[mirror]); + palindromeLengths[i] = Math.min(rightBoundary - i, palindromeLengths[mirror]); } - // Try to expand the palindrome centered at i - while (processedString.charAt(i + 1 + p[i]) == processedString.charAt(i - 1 - p[i])) { - p[i]++; + while (processedString.charAt(i + 1 + palindromeLengths[i]) == processedString.charAt(i - 1 - palindromeLengths[i])) { + palindromeLengths[i]++; } - // Update center and right boundary if palindrome expands beyond current right boundary - if (i + p[i] > rightBoundary) { + if (i + palindromeLengths[i] > rightBoundary) { center = i; - rightBoundary = i + p[i]; + rightBoundary = i + palindromeLengths[i]; } - // Track the maximum length and center index of the longest palindrome found so far - if (p[i] > maxLen) { - maxLen = p[i]; + if (palindromeLengths[i] > maxLen) { + maxLen = palindromeLengths[i]; centerIndex = i; } } - // Extract the longest palindrome from the original string - int start = (centerIndex - maxLen) / 2; // Get the starting index in the original string + final int start = (centerIndex - maxLen) / 2; return s.substring(start, start + maxLen); } diff --git a/src/test/java/com/thealgorithms/strings/ManacherTest.java b/src/test/java/com/thealgorithms/strings/ManacherTest.java index 01564a7fd35a..dc74df31b866 100644 --- a/src/test/java/com/thealgorithms/strings/ManacherTest.java +++ b/src/test/java/com/thealgorithms/strings/ManacherTest.java @@ -2,36 +2,52 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -import org.junit.jupiter.api.Test; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; public class ManacherTest { - @Test - public void testLongestPalindrome() { - assertEquals("aabcdefggfedcbaa", Manacher.longestPalindrome("abracadabraabcdefggfedcbaabracadabra")); // Long string with embedded palindrome - assertEquals("racecar", Manacher.longestPalindrome("somelongtextwithracecarmiddletext")); // Longer string with racecar palindrome - assertEquals("ananananananana", Manacher.longestPalindrome("bananananananana")); // Repetitive pattern with palindrome - assertEquals("defgfed", Manacher.longestPalindrome("qwertydefgfedzxcvbnm")); // Palindrome in middle of long string - assertEquals("abcdefghijklmnopqrstuvwxyzzyxwvutsrqponmlkjihgfedcba", Manacher.longestPalindrome("abcdefghijklmnopqrstuvwxyzzyxwvutsrqponmlkjihgfedcba")); // Symmetrical section + @ParameterizedTest + @MethodSource("provideTestCasesForLongestPalindrome") + public void testLongestPalindrome(String input, String expected) { + assertEquals(expected, Manacher.longestPalindrome(input)); } - @Test - public void testEmptyAndSingle() { - assertEquals("", Manacher.longestPalindrome("")); // Empty string - assertEquals("a", Manacher.longestPalindrome("a")); // Single character + private static Stream provideTestCasesForLongestPalindrome() { + return Stream.of(Arguments.of("abracadabraabcdefggfedcbaabracadabra", "aabcdefggfedcbaa"), Arguments.of("somelongtextwithracecarmiddletext", "racecar"), Arguments.of("bananananananana", "ananananananana"), Arguments.of("qwertydefgfedzxcvbnm", "defgfed"), + Arguments.of("abcdefghijklmnopqrstuvwxyzzyxwvutsrqponmlkjihgfedcba", "abcdefghijklmnopqrstuvwxyzzyxwvutsrqponmlkjihgfedcba")); } - @Test - public void testComplexCases() { - assertEquals("tattarrattat", Manacher.longestPalindrome("abcdefghijklmnopqrstuvwxyzttattarrattatabcdefghijklmnopqrstuvwxyz")); // Long palindrome inside a large string - assertEquals("aaaaabaaaaa", Manacher.longestPalindrome("aaaaabaaaaacbaaaaa")); // Large repetitive character set - assertEquals("abcdefghhgfedcba", Manacher.longestPalindrome("sometextrandomabcdefgabcdefghhgfedcbahijklmnopqrstuvwxyz")); // Large string with clear palindromic section - assertEquals("madaminedenimadam", Manacher.longestPalindrome("therewasasignthatsaidmadaminedenimadamitwasthereallalong")); // Famous palindrome within a long string + @ParameterizedTest + @MethodSource("provideTestCasesForEmptyAndSingle") + public void testEmptyAndSingle(String input, String expected) { + assertEquals(expected, Manacher.longestPalindrome(input)); } - @Test - public void testSentencePalindromes() { - assertEquals("lanacanal", Manacher.longestPalindrome("XThisisalongtextbuthiddeninsideisAmanaplanacanalPanamaWhichweknowisfamous")); - assertEquals("everoddoreve", Manacher.longestPalindrome("AverylongstringthatcontainsNeveroddoreveninahiddenmanner")); // Another sentence-like palindrome + private static Stream provideTestCasesForEmptyAndSingle() { + return Stream.of(Arguments.of("", ""), Arguments.of("a", "a")); + } + + @ParameterizedTest + @MethodSource("provideTestCasesForComplexCases") + public void testComplexCases(String input, String expected) { + assertEquals(expected, Manacher.longestPalindrome(input)); + } + + private static Stream provideTestCasesForComplexCases() { + return Stream.of(Arguments.of("abcdefghijklmnopqrstuvwxyzttattarrattatabcdefghijklmnopqrstuvwxyz", "tattarrattat"), Arguments.of("aaaaabaaaaacbaaaaa", "aaaaabaaaaa"), Arguments.of("sometextrandomabcdefgabcdefghhgfedcbahijklmnopqrstuvwxyz", "abcdefghhgfedcba"), + Arguments.of("therewasasignthatsaidmadaminedenimadamitwasthereallalong", "madaminedenimadam")); + } + + @ParameterizedTest + @MethodSource("provideTestCasesForSentencePalindromes") + public void testSentencePalindromes(String input, String expected) { + assertEquals(expected, Manacher.longestPalindrome(input)); + } + + private static Stream provideTestCasesForSentencePalindromes() { + return Stream.of(Arguments.of("XThisisalongtextbuthiddeninsideisAmanaplanacanalPanamaWhichweknowisfamous", "lanacanal"), Arguments.of("AverylongstringthatcontainsNeveroddoreveninahiddenmanner", "everoddoreve")); } }