diff --git a/Algorithms.Tests/Strings/ManachersAlgorithmTests.cs b/Algorithms.Tests/Strings/ManachersAlgorithmTests.cs new file mode 100644 index 00000000..bc934c10 --- /dev/null +++ b/Algorithms.Tests/Strings/ManachersAlgorithmTests.cs @@ -0,0 +1,445 @@ +using Algorithms.Strings; +using NUnit.Framework; +using System; + +namespace Algorithms.Tests.Strings; + +/// +/// Comprehensive test suite for Manacher's Algorithm implementation. +/// Tests cover various scenarios including: +/// - Odd-length palindromes +/// - Even-length palindromes +/// - Single character strings +/// - Empty strings +/// - Strings with no palindromes longer than 1 character +/// - Strings that are entirely palindromic +/// - Multiple palindromes of the same length +/// - Edge cases (null input, special characters) +/// - Palindrome detection functionality +/// - Detailed palindrome information retrieval. +/// +public static class ManachersAlgorithmTests +{ + [Test] + public static void FindLongestPalindrome_WithOddLengthPalindrome_ReturnsCorrectPalindrome() + { + // Arrange: Classic example with odd-length palindrome "bab" or "aba" + string input = "babad"; + + // Act + string result = ManachersAlgorithm.FindLongestPalindrome(input); + + // Assert: Either "bab" or "aba" is valid (both have length 3) + Assert.That(result.Length, Is.EqualTo(3)); + Assert.That(result == "bab" || result == "aba", Is.True); + } + + [Test] + public static void FindLongestPalindrome_WithEvenLengthPalindrome_ReturnsCorrectPalindrome() + { + // Arrange: String with even-length palindrome "abba" + string input = "cbbd"; + + // Act + string result = ManachersAlgorithm.FindLongestPalindrome(input); + + // Assert: Should find "bb" (length 2) + Assert.That(result, Is.EqualTo("bb")); + } + + [Test] + public static void FindLongestPalindrome_WithSingleCharacter_ReturnsSingleCharacter() + { + // Arrange: Edge case with single character + string input = "a"; + + // Act + string result = ManachersAlgorithm.FindLongestPalindrome(input); + + // Assert: Single character is a palindrome of itself + Assert.That(result, Is.EqualTo("a")); + } + + [Test] + public static void FindLongestPalindrome_WithEmptyString_ReturnsEmptyString() + { + // Arrange: Edge case with empty string + string input = string.Empty; + + // Act + string result = ManachersAlgorithm.FindLongestPalindrome(input); + + // Assert: Empty string should return empty string + Assert.That(result, Is.EqualTo(string.Empty)); + } + + [Test] + public static void FindLongestPalindrome_WithNullString_ThrowsArgumentException() + { + // Arrange: Test defensive programming - null input validation + string? input = null; + + // Act & Assert: Should throw ArgumentException for null input + Assert.Throws(() => ManachersAlgorithm.FindLongestPalindrome(input!)); + } + + [Test] + public static void FindLongestPalindrome_WithEntirePalindrome_ReturnsEntireString() + { + // Arrange: String that is entirely a palindrome + string input = "racecar"; + + // Act + string result = ManachersAlgorithm.FindLongestPalindrome(input); + + // Assert: Should return the entire string + Assert.That(result, Is.EqualTo("racecar")); + } + + [Test] + public static void FindLongestPalindrome_WithNoPalindromes_ReturnsSingleCharacter() + { + // Arrange: String with no palindromes longer than 1 character + string input = "abcdef"; + + // Act + string result = ManachersAlgorithm.FindLongestPalindrome(input); + + // Assert: Should return a single character (any character is a palindrome) + Assert.That(result.Length, Is.EqualTo(1)); + } + + [Test] + public static void FindLongestPalindrome_WithLongPalindrome_ReturnsCorrectPalindrome() + { + // Arrange: String with a longer palindrome + string input = "forgeeksskeegfor"; + + // Act + string result = ManachersAlgorithm.FindLongestPalindrome(input); + + // Assert: Should find "geeksskeeg" (length 10) + Assert.That(result, Is.EqualTo("geeksskeeg")); + } + + [Test] + public static void FindLongestPalindrome_WithRepeatingCharacters_ReturnsCorrectPalindrome() + { + // Arrange: String with repeating characters + string input = "aaaa"; + + // Act + string result = ManachersAlgorithm.FindLongestPalindrome(input); + + // Assert: Entire string is a palindrome + Assert.That(result, Is.EqualTo("aaaa")); + } + + [Test] + public static void FindLongestPalindrome_WithSpecialCharacters_ReturnsCorrectPalindrome() + { + // Arrange: String with special characters and spaces + string input = "A man, a plan, a canal: Panama"; + + // Act + string result = ManachersAlgorithm.FindLongestPalindrome(input); + + // Assert: Should find a palindrome (note: this includes spaces and punctuation) + // The longest palindrome considering all characters is "anama" + Assert.That(result.Length, Is.GreaterThan(0)); + } + + [Test] + public static void FindLongestPalindrome_WithTwoCharacters_ReturnsCorrectResult() + { + // Arrange: Edge case with two identical characters + string input = "aa"; + + // Act + string result = ManachersAlgorithm.FindLongestPalindrome(input); + + // Assert: Should return "aa" + Assert.That(result, Is.EqualTo("aa")); + } + + [Test] + public static void FindLongestPalindrome_WithTwoDifferentCharacters_ReturnsSingleCharacter() + { + // Arrange: Edge case with two different characters + string input = "ab"; + + // Act + string result = ManachersAlgorithm.FindLongestPalindrome(input); + + // Assert: Should return a single character + Assert.That(result.Length, Is.EqualTo(1)); + } + + [Test] + public static void FindLongestPalindromeWithDetails_ReturnsCorrectDetails() + { + // Arrange: Test the detailed version that returns index and length + string input = "babad"; + + // Act + var (palindrome, startIndex, length) = ManachersAlgorithm.FindLongestPalindromeWithDetails(input); + + // Assert: Verify all components + Assert.That(length, Is.EqualTo(3)); + Assert.That(palindrome.Length, Is.EqualTo(length)); + Assert.That(input.Substring(startIndex, length), Is.EqualTo(palindrome)); + Assert.That(palindrome == "bab" || palindrome == "aba", Is.True); + } + + [Test] + public static void FindLongestPalindromeWithDetails_WithEmptyString_ReturnsZeroLength() + { + // Arrange + string input = string.Empty; + + // Act + var (palindrome, startIndex, length) = ManachersAlgorithm.FindLongestPalindromeWithDetails(input); + + // Assert + Assert.That(palindrome, Is.EqualTo(string.Empty)); + Assert.That(startIndex, Is.EqualTo(0)); + Assert.That(length, Is.EqualTo(0)); + } + + [Test] + public static void FindLongestPalindromeWithDetails_WithSingleCharacter_ReturnsCorrectDetails() + { + // Arrange + string input = "x"; + + // Act + var (palindrome, startIndex, length) = ManachersAlgorithm.FindLongestPalindromeWithDetails(input); + + // Assert + Assert.That(palindrome, Is.EqualTo("x")); + Assert.That(startIndex, Is.EqualTo(0)); + Assert.That(length, Is.EqualTo(1)); + } + + [Test] + public static void FindLongestPalindromeWithDetails_WithNullString_ThrowsArgumentException() + { + // Arrange + string? input = null; + + // Act & Assert + Assert.Throws(() => ManachersAlgorithm.FindLongestPalindromeWithDetails(input!)); + } + + [Test] + public static void IsPalindrome_WithPalindromeString_ReturnsTrue() + { + // Arrange: Test palindrome detection with a valid palindrome + string input = "racecar"; + + // Act + bool result = ManachersAlgorithm.IsPalindrome(input); + + // Assert: Should return true + Assert.That(result, Is.True); + } + + [Test] + public static void IsPalindrome_WithNonPalindromeString_ReturnsFalse() + { + // Arrange: Test palindrome detection with a non-palindrome + string input = "hello"; + + // Act + bool result = ManachersAlgorithm.IsPalindrome(input); + + // Assert: Should return false + Assert.That(result, Is.False); + } + + [Test] + public static void IsPalindrome_WithSingleCharacter_ReturnsTrue() + { + // Arrange: Single character is always a palindrome + string input = "a"; + + // Act + bool result = ManachersAlgorithm.IsPalindrome(input); + + // Assert: Should return true + Assert.That(result, Is.True); + } + + [Test] + public static void IsPalindrome_WithEmptyString_ReturnsTrue() + { + // Arrange: Empty string is considered a palindrome + string input = string.Empty; + + // Act + bool result = ManachersAlgorithm.IsPalindrome(input); + + // Assert: Should return true + Assert.That(result, Is.True); + } + + [Test] + public static void IsPalindrome_WithNullString_ThrowsArgumentException() + { + // Arrange + string? input = null; + + // Act & Assert + Assert.Throws(() => ManachersAlgorithm.IsPalindrome(input!)); + } + + [Test] + public static void IsPalindrome_WithEvenLengthPalindrome_ReturnsTrue() + { + // Arrange: Even-length palindrome + string input = "abba"; + + // Act + bool result = ManachersAlgorithm.IsPalindrome(input); + + // Assert: Should return true + Assert.That(result, Is.True); + } + + [Test] + public static void FindLongestPalindrome_WithNumericString_ReturnsCorrectPalindrome() + { + // Arrange: String with numbers + string input = "12321"; + + // Act + string result = ManachersAlgorithm.FindLongestPalindrome(input); + + // Assert: Should return the entire string + Assert.That(result, Is.EqualTo("12321")); + } + + [Test] + public static void FindLongestPalindrome_WithMixedCase_ReturnsCorrectPalindrome() + { + // Arrange: Mixed case string (case-sensitive palindrome check) + string input = "AbcbA"; + + // Act + string result = ManachersAlgorithm.FindLongestPalindrome(input); + + // Assert: Should find "AbcbA" as it's case-sensitive + Assert.That(result, Is.EqualTo("AbcbA")); + } + + [Test] + public static void FindLongestPalindrome_WithPalindromeAtStart_ReturnsCorrectPalindrome() + { + // Arrange: Palindrome at the start of the string + string input = "abaxyz"; + + // Act + string result = ManachersAlgorithm.FindLongestPalindrome(input); + + // Assert: Should find "aba" + Assert.That(result, Is.EqualTo("aba")); + } + + [Test] + public static void FindLongestPalindrome_WithPalindromeAtEnd_ReturnsCorrectPalindrome() + { + // Arrange: Palindrome at the end of the string + string input = "xyzaba"; + + // Act + string result = ManachersAlgorithm.FindLongestPalindrome(input); + + // Assert: Should find "aba" + Assert.That(result, Is.EqualTo("aba")); + } + + [Test] + public static void FindLongestPalindrome_WithMultiplePalindromesOfSameLength_ReturnsOne() + { + // Arrange: Multiple palindromes of the same length + // "aba" at index 0 and "cdc" at index 3 + string input = "abacdc"; + + // Act + string result = ManachersAlgorithm.FindLongestPalindrome(input); + + // Assert: Should return one of them (the first one found) + Assert.That(result.Length, Is.EqualTo(3)); + Assert.That(result == "aba" || result == "cdc", Is.True); + } + + [Test] + public static void FindLongestPalindromeWithDetails_WithLongString_PerformsEfficiently() + { + // Arrange: Test with a longer string to verify O(n) performance + // Create a string with a palindrome in the middle + string input = new string('a', 1000) + "racecar" + new string('b', 1000); + + // Act + var (_, _, length) = ManachersAlgorithm.FindLongestPalindromeWithDetails(input); + + // Assert: Should find the longest palindrome (either the 'a's or 'b's or "racecar") + // The 1000 'a's form a palindrome + Assert.That(length, Is.GreaterThanOrEqualTo(7)); // At least "racecar" + } + + [Test] + public static void FindLongestPalindrome_WithVeryLongPalindrome_HandlesGracefully() + { + // Arrange: Test with a very long palindrome to verify O(n) performance + string input = new string('a', 10000); + + // Act + string result = ManachersAlgorithm.FindLongestPalindrome(input); + + // Assert: Should correctly identify the entire string as a palindrome + Assert.That(result.Length, Is.EqualTo(10000)); + Assert.That(result, Is.EqualTo(input)); + } + + [Test] + public static void IsPalindrome_WithVeryLongPalindrome_HandlesGracefully() + { + // Arrange: Test with a very long palindrome to verify performance + string input = new string('x', 5000); + + // Act + bool result = ManachersAlgorithm.IsPalindrome(input); + + // Assert: Should correctly identify as palindrome in O(n) time + Assert.That(result, Is.True); + } + + [Test] + public static void FindLongestPalindrome_WithAlternatingCharacters_HandlesEdgeCases() + { + // Arrange: Test with alternating characters (minimal palindromes) + string input = "abababababababababababababababab"; + + // Act + string result = ManachersAlgorithm.FindLongestPalindrome(input); + + // Assert: Should find at least a 3-character palindrome like "aba" or "bab" + Assert.That(result.Length, Is.GreaterThanOrEqualTo(3)); + } + + [Test] + public static void FindLongestPalindromeWithDetails_WithSpecialCharacters_HandlesCorrectly() + { + // Arrange: Test with special characters to ensure proper handling + string input = "abc!@#@!xyz"; + + // Act + var (palindrome, startIndex, length) = ManachersAlgorithm.FindLongestPalindromeWithDetails(input); + + // Assert: Should find the palindrome with special characters + Assert.That(palindrome, Is.EqualTo("!@#@!")); + Assert.That(length, Is.EqualTo(5)); + Assert.That(startIndex, Is.EqualTo(3)); + } +} diff --git a/Algorithms/Strings/ManachersAlgorithm.cs b/Algorithms/Strings/ManachersAlgorithm.cs new file mode 100644 index 00000000..5e535c03 --- /dev/null +++ b/Algorithms/Strings/ManachersAlgorithm.cs @@ -0,0 +1,393 @@ +namespace Algorithms.Strings; + +/// +/// Manacher's Algorithm is used to find the longest palindromic substring in linear time O(n). +/// This algorithm is significantly more efficient than the naive O(n²) approach. +/// +/// KEY CONCEPTS: +/// 1. String Transformation: Inserts special characters to handle even/odd palindromes uniformly. +/// 2. Palindrome Radius: For each position, stores how far the palindrome extends. +/// 3. Center Expansion: Expands around each potential center to find palindromes. +/// 4. Symmetry Property: Uses previously computed palindrome information to skip redundant checks. +/// 5. Right Boundary Tracking: Maintains the rightmost boundary of any palindrome found. +/// +/// WHY IT'S FAST: +/// The algorithm achieves O(n) time by ensuring each character is examined at most twice: +/// - Once when it's inside a known palindrome (using mirror property). +/// - Once when expanding beyond the known boundary. +/// +/// Reference: "A New Linear-Time On-Line Algorithm for Finding the Smallest Initial Palindrome of a String" +/// by Glenn Manacher (1975), Journal of the ACM. +/// +public static class ManachersAlgorithm +{ + /// + /// Finds the longest palindromic substring using Manacher's Algorithm. + /// + /// ALGORITHM STEPS: + /// 1. PREPROCESSING: Transform "abc" to "^#a#b#c#$" to handle even/odd palindromes uniformly. + /// - ^ and $ are sentinels that prevent boundary checks. + /// - # characters create uniform spacing. + /// + /// 2. INITIALIZATION: Set up tracking variables. + /// - palindromeRadii[i]: How far palindrome extends from position i. + /// - center: Center of rightmost palindrome found. + /// - rightBoundary: Right edge of rightmost palindrome. + /// + /// 3. MAIN LOOP: For each position i in transformed string. + /// a) If i is within rightBoundary, use mirror property: + /// - mirror = 2 * center - i (symmetric position). + /// - Start with min(palindromeRadii[mirror], rightBoundary - i). + /// b) Expand palindrome by comparing characters on both sides. + /// c) Update center and rightBoundary if palindrome extends further right. + /// d) Track the longest palindrome found. + /// + /// 4. EXTRACTION: Convert transformed coordinates back to original string. + /// + /// Time Complexity: O(n) - Each character examined at most twice. + /// Space Complexity: O(n) - For transformed string and radius array. + /// + /// The input string to search for palindromes. + /// The longest palindromic substring found in the input. + /// Thrown when the input string is null. + /// + /// Input: "babad". + /// Output: "bab" or "aba" (both are valid longest palindromes with length 3). + /// + /// Detailed Example: + /// Input: "abaxyz". + /// Transformed: "^#a#b#a#x#y#z#$". + /// Process finds "aba" at indices 1-3 with radius 3 in transformed string. + /// Maps back to indices 0-2 in original string. + /// Output: "aba". + /// + public static string FindLongestPalindrome(string input) + { + // Validate input + if (input == null) + { + throw new ArgumentException("Input string cannot be null.", nameof(input)); + } + + // Handle edge cases + if (input.Length == 0) + { + return string.Empty; + } + + if (input.Length == 1) + { + return input; + } + + // STEP 1: Transform the string to handle even-length palindromes uniformly + // Example: "abc" becomes "^#a#b#c#$" + // + // WHY THIS WORKS: + // - Original "aba" (odd): Center is 'b' at index 1. + // - Original "abba" (even): No single center character. + // - Transformed "#a#b#b#a#": Both have clear centers (the middle #). + // + // SENTINELS (^ and $): + // - Prevent index out of bounds during expansion. + // - Never match any character, so expansion stops naturally. + string transformed = PreprocessString(input); + int n = transformed.Length; + + // STEP 2: Initialize data structures + + // palindromeRadii[i] = radius of palindrome centered at position i. + // Example: If transformed[i] is center of "#a#b#a#", radius = 3. + // (3 characters on each side match). + int[] palindromeRadii = new int[n]; + + // Track the rightmost palindrome boundary for optimization. + // center: Position of the palindrome that extends furthest right. + // rightBoundary: The rightmost index covered by that palindrome. + // These help us use symmetry to avoid redundant comparisons. + int center = 0; + int rightBoundary = 0; + + // Track the longest palindrome found during the scan. + // maxLength: Radius of the longest palindrome. + // maxCenter: Position where the longest palindrome is centered. + int maxLength = 0; + int maxCenter = 0; + + // STEP 3: Process each position in the transformed string. + // Skip first and last positions (sentinels ^ and $). + for (int i = 1; i < n - 1; i++) + { + // OPTIMIZATION: Use mirror property for positions within known palindrome + // + // If we have a palindrome centered at 'center' extending to 'rightBoundary': + // center - radius ... center ... center + radius + // mirror i + // + // The mirror position reflects i across the center: + // mirror = center - (i - center) = 2 * center - i. + int mirror = 2 * center - i; + + // KEY INSIGHT: If i is within rightBoundary, we can use symmetry. + // The palindrome at position i might be similar to the one at mirror position. + if (i < rightBoundary) + { + // We can safely copy the radius from the mirror position, BUT: + // 1. palindromeRadii[mirror]: What we know from the mirror side. + // 2. rightBoundary - i: We can't assume anything beyond rightBoundary. + // + // Take the minimum because: + // - If mirror's palindrome fits within bounds, we can use it. + // - If it extends beyond, we only know up to rightBoundary. + palindromeRadii[i] = Math.Min(rightBoundary - i, palindromeRadii[mirror]); + } + + // EXPANSION PHASE: Try to extend the palindrome further. + // We start from palindromeRadii[i] (not 0) to avoid redundant checks. + // This is why the algorithm is O(n) - we never re-check characters. + // + // Example: If palindromeRadii[i] = 2, we already know: + // transformed[i-2] == transformed[i+2] and + // transformed[i-1] == transformed[i+1]. + // So we start checking at distance 3. + // + // The sentinels (^ and $) guarantee we never go out of bounds. + // Expansion stops naturally when characters don't match. + while (transformed[i + palindromeRadii[i] + 1] == transformed[i - palindromeRadii[i] - 1]) + { + palindromeRadii[i]++; + } + + // UPDATE TRACKING: If this palindrome extends further right than any before. + // + // WHY THIS MATTERS: + // By tracking the rightmost boundary, we can use the mirror property + // for future positions, avoiding redundant character comparisons. + // + // Example: If we find a large palindrome early, all positions within it + // can benefit from the symmetry property. + if (i + palindromeRadii[i] > rightBoundary) + { + center = i; // This position is now our reference center. + rightBoundary = i + palindromeRadii[i]; // Update the rightmost boundary. + } + + // TRACK MAXIMUM: Remember the longest palindrome found so far. + // We need both the length and position to extract it later. + if (palindromeRadii[i] > maxLength) + { + maxLength = palindromeRadii[i]; // Radius of longest palindrome. + maxCenter = i; // Where it's centered in transformed string. + } + } + + // STEP 4: Extract the longest palindrome from the original string. + // + // COORDINATE MAPPING: + // Transformed string has format: ^#a#b#c#$. + // Position in transformed -> Position in original: (pos - 1) / 2. + // + // Example: "aba" -> "^#a#b#a#$". + // - maxCenter = 4 (the middle 'b' in transformed). + // - maxLength = 3 (radius). + // - Original center = (4 - 1) / 2 = 1 (index of 'b' in "aba"). + // - Start = (maxCenter - maxLength) / 2 = (4 - 3) / 2 = 0. + // - Length = maxLength = 3. + // - Result: input.Substring(0, 3) = "aba". + int start = (maxCenter - maxLength) / 2; + return input.Substring(start, maxLength); + } + + /// + /// Finds the longest palindromic substring and returns detailed information. + /// This method provides more detailed information than FindLongestPalindrome, + /// including the exact starting position and length in the original string. + /// + /// USE CASE: + /// When you need to know WHERE the palindrome is located, not just what it is. + /// Useful for highlighting, replacing, or further processing the palindrome. + /// + /// The input string to search for palindromes. + /// + /// A tuple containing: + /// - The longest palindromic substring + /// - The starting index of the longest palindrome in the original string + /// - The length of the longest palindrome. + /// + /// Thrown when the input string is null. + /// + /// Input: "babad". + /// Output: (Palindrome: "bab", StartIndex: 0, Length: 3). + /// + public static (string Palindrome, int StartIndex, int Length) FindLongestPalindromeWithDetails(string input) + { + // Validate input + if (input == null) + { + throw new ArgumentException("Input string cannot be null.", nameof(input)); + } + + // Handle edge cases + if (input.Length == 0) + { + return (string.Empty, 0, 0); + } + + if (input.Length == 1) + { + return (input, 0, 1); + } + + // Apply the same algorithm as FindLongestPalindrome. + // (See detailed comments in that method for step-by-step explanation). + string transformed = PreprocessString(input); + int n = transformed.Length; + int[] palindromeRadii = new int[n]; + int center = 0; + int rightBoundary = 0; + int maxLength = 0; + int maxCenter = 0; + + // Main algorithm loop + for (int i = 1; i < n - 1; i++) + { + // Use mirror property if within known palindrome. + int mirror = 2 * center - i; + + if (i < rightBoundary) + { + palindromeRadii[i] = Math.Min(rightBoundary - i, palindromeRadii[mirror]); + } + + // Expand palindrome. + // Sentinels guarantee no out-of-bounds access. + while (transformed[i + palindromeRadii[i] + 1] == transformed[i - palindromeRadii[i] - 1]) + { + palindromeRadii[i]++; + } + + // Update rightmost boundary. + if (i + palindromeRadii[i] > rightBoundary) + { + center = i; + rightBoundary = i + palindromeRadii[i]; + } + + // Track maximum. + if (palindromeRadii[i] > maxLength) + { + maxLength = palindromeRadii[i]; + maxCenter = i; + } + } + + // Calculate the start index and extract the palindrome. + // Map from transformed coordinates to original string coordinates. + int startIndex = (maxCenter - maxLength) / 2; + string palindrome = input.Substring(startIndex, maxLength); + + // Return all three pieces of information. + return (palindrome, startIndex, maxLength); + } + + /// + /// Checks if the entire string is a palindrome using Manacher's Algorithm. + /// + /// EFFICIENCY: + /// - This approach: O(n) time using Manacher's algorithm. + /// - Naive approach: O(n) time for reversing + O(n) for comparison. + /// - Both are O(n), but this avoids creating a reversed copy. + /// + /// LOGIC: + /// If the longest palindrome in the string equals the string length, + /// then the entire string must be a palindrome. + /// + /// The string to check. + /// True if the entire string is a palindrome, false otherwise. + /// Thrown when the input string is null. + /// + /// Input: "racecar". + /// Output: true. + /// + public static bool IsPalindrome(string input) + { + if (input == null) + { + throw new ArgumentException("Input string cannot be null.", nameof(input)); + } + + // Strings of length 0 or 1 are always palindromes. + if (input.Length <= 1) + { + return true; + } + + // Find the longest palindrome in the string. + // If it spans the entire string, then the string is a palindrome. + var (_, _, length) = FindLongestPalindromeWithDetails(input); + return length == input.Length; + } + + /// + /// Preprocesses the input string by inserting special characters. + /// This transformation is the KEY to making Manacher's algorithm work efficiently. + /// + /// PROBLEM IT SOLVES: + /// - Odd-length palindromes ("aba") have a center character. + /// - Even-length palindromes ("abba") have a center between characters. + /// - Without transformation, we'd need separate logic for each case. + /// + /// SOLUTION: + /// Insert '#' between every character, making all palindromes odd-length. + /// + /// EXAMPLES: + /// - "aba" (odd) -> "^#a#b#a#$" (center is 'b'). + /// - "abba" (even) -> "^#a#b#b#a#$" (center is '#' between the two 'b's). + /// + /// SENTINELS (^ and $): + /// - Placed at start and end. + /// - Never match any character (including each other). + /// - Automatically stop expansion without explicit boundary checks. + /// - Prevent IndexOutOfBoundsException. + /// + /// COORDINATE MAPPING: + /// - Original index i maps to transformed index (2*i + 2). + /// - Transformed index j maps to original index (j - 1) / 2. + /// + /// The original input string. + /// The transformed string with inserted special characters. + private static string PreprocessString(string input) + { + // Calculate the size of transformed string. + // Original: n characters. + // Transformed: ^ + # + (n chars with # between each) + # + $. + // Total: 1 + 1 + n + (n-1) + 1 + 1 = 2n + 3. + int n = input.Length; + char[] transformed = new char[n * 2 + 3]; + + // Place sentinels at boundaries. + transformed[0] = '^'; // Start sentinel (never matches anything). + transformed[n * 2 + 2] = '$'; // End sentinel (never matches anything). + + // Build the transformed string: #a#b#c#. + // For input "abc": + // Position 0: ^ (sentinel). + // Position 1: # (separator). + // Position 2: a (input[0]). + // Position 3: # (separator). + // Position 4: b (input[1]). + // Position 5: # (separator). + // Position 6: c (input[2]). + // Position 7: # (separator). + // Position 8: $ (sentinel). + for (int i = 0; i < n; i++) + { + transformed[2 * i + 1] = '#'; // Separator before character. + transformed[2 * i + 2] = input[i]; // Original character. + } + + transformed[n * 2 + 1] = '#'; // Final separator. + + return new string(transformed); + } +} diff --git a/README.md b/README.md index 8b499683..93ea4e25 100644 --- a/README.md +++ b/README.md @@ -218,6 +218,7 @@ find more than one implementation for the same objective but using different alg * [WildCard Pattern Matching](./Algorithms/Strings/PatternMatching/WildCardMatcher.cs) * [Z-block substring search](./Algorithms/Strings/PatternMatching/ZblockSubstringSearch.cs) * [Longest Consecutive Character](./Algorithms/Strings/GeneralStringAlgorithms.cs) + * [Manacher's Algorithm](./Algorithms/Strings/ManachersAlgorithm.cs) * [Palindrome Checker](./Algorithms/Strings/Palindrome.cs) * [Get all permutations of a string](./Algorithms/Strings/Permutation.cs) * [Other](./Algorithms/Other)