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..34c303822bee --- /dev/null +++ b/src/main/java/com/thealgorithms/strings/Manacher.java @@ -0,0 +1,69 @@ +package com.thealgorithms.strings; + +/** + * Wikipedia: https://en.wikipedia.org/wiki/Longest_palindromic_substring#Manacher's_algorithm + */ +public final class Manacher { + + private Manacher() { + } + + /** + * 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) { + final String processedString = preprocess(s); + int[] palindromeLengths = new int[processedString.length()]; + int center = 0; + int rightBoundary = 0; + int maxLen = 0; + int centerIndex = 0; + + for (int i = 1; i < processedString.length() - 1; i++) { + int mirror = 2 * center - i; + + if (i < rightBoundary) { + palindromeLengths[i] = Math.min(rightBoundary - i, palindromeLengths[mirror]); + } + + while (processedString.charAt(i + 1 + palindromeLengths[i]) == processedString.charAt(i - 1 - palindromeLengths[i])) { + palindromeLengths[i]++; + } + + if (i + palindromeLengths[i] > rightBoundary) { + center = i; + rightBoundary = i + palindromeLengths[i]; + } + + if (palindromeLengths[i] > maxLen) { + maxLen = palindromeLengths[i]; + centerIndex = i; + } + } + + final int start = (centerIndex - maxLen) / 2; + 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..dc74df31b866 --- /dev/null +++ b/src/test/java/com/thealgorithms/strings/ManacherTest.java @@ -0,0 +1,53 @@ +package com.thealgorithms.strings; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +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 { + + @ParameterizedTest + @MethodSource("provideTestCasesForLongestPalindrome") + public void testLongestPalindrome(String input, String expected) { + assertEquals(expected, Manacher.longestPalindrome(input)); + } + + 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")); + } + + @ParameterizedTest + @MethodSource("provideTestCasesForEmptyAndSingle") + public void testEmptyAndSingle(String input, String expected) { + assertEquals(expected, Manacher.longestPalindrome(input)); + } + + 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")); + } +}