RedEnginePress logo
RedEnginePress
AlgorithmsLanguagesPlaygroundAbout

Manacher's Algorithm

A
namespace Algorithms.Strings;

/// <summary>
///     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.
/// </summary>
public static class ManachersAlgorithm
{
    /// <summary>
    ///     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.
    /// </summary>
    /// <param name="input">The input string to search for palindromes.</param>
    /// <returns>The longest palindromic substring found in the input.</returns>
    /// <exception cref="ArgumentException">Thrown when the input string is null.</exception>
    /// <example>
    ///     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".
    /// </example>
    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);
    }

    /// <summary>
    ///     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.
    /// </summary>
    /// <param name="input">The input string to search for palindromes.</param>
    /// <returns>
    ///     A tuple containing:
    ///     - The longest palindromic substring
    ///     - The starting index of the longest palindrome in the original string
    ///     - The length of the longest palindrome.
    /// </returns>
    /// <exception cref="ArgumentException">Thrown when the input string is null.</exception>
    /// <example>
    ///     Input: "babad".
    ///     Output: (Palindrome: "bab", StartIndex: 0, Length: 3).
    /// </example>
    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);
    }

    /// <summary>
    ///     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.
    /// </summary>
    /// <param name="input">The string to check.</param>
    /// <returns>True if the entire string is a palindrome, false otherwise.</returns>
    /// <exception cref="ArgumentException">Thrown when the input string is null.</exception>
    /// <example>
    ///     Input: "racecar".
    ///     Output: true.
    /// </example>
    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;
    }

    /// <summary>
    ///     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.
    /// </summary>
    /// <param name="input">The original input string.</param>
    /// <returns>The transformed string with inserted special characters.</returns>
    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);
    }
}