From 1e1f50798b397f50967e46ee9f42aa31718a1fa7 Mon Sep 17 00:00:00 2001 From: Simran Shrestha Date: Fri, 18 Oct 2024 18:19:18 +0545 Subject: [PATCH] Implement Manacher's Algorithm and Unit Tests for Longest Palindromic Substring --- algorithms/strings/__init__.py | 3 ++- algorithms/strings/manacher.py | 39 ++++++++++++++++++++++++++++++++++ tests/test_strings.py | 31 ++++++++++++++++++++++++++- 3 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 algorithms/strings/manacher.py diff --git a/algorithms/strings/__init__.py b/algorithms/strings/__init__.py index 496b1ebe5..e187257f5 100644 --- a/algorithms/strings/__init__.py +++ b/algorithms/strings/__init__.py @@ -38,4 +38,5 @@ from .atbash_cipher import * from .longest_palindromic_substring import * from .knuth_morris_pratt import * -from .panagram import * \ No newline at end of file +from .panagram import * +from .manacher import * \ No newline at end of file diff --git a/algorithms/strings/manacher.py b/algorithms/strings/manacher.py new file mode 100644 index 000000000..279d3b94a --- /dev/null +++ b/algorithms/strings/manacher.py @@ -0,0 +1,39 @@ +""" +Manacher's Algorithm to find the longest palindromic substring in linear time (O(n)). + +The algorithm transforms the string by inserting special characters (#) between +characters to handle both odd and even length palindromes uniformly. Then it expands +around each possible center to find the longest palindromic substring. +""" + +def manacher(s): + # Transform the string to insert special characters between each character and at the start and end + t = '#' + '#'.join(s) + '#' + n = len(t) + p = [0] * n # Array to store the length of the palindrome centered at each character + c = 0 # Current center + r = 0 # Right boundary of the current longest palindrome + + for i in range(n): + mirror = 2 * c - i # Mirror position of i with respect to center c + + if i < r: + p[i] = min(r - i, p[mirror]) # Use the previously calculated palindrome length if possible + + # Attempt to expand the palindrome centered at i + while i + p[i] + 1 < n and i - p[i] - 1 >= 0 and t[i + p[i] + 1] == t[i - p[i] - 1]: + p[i] += 1 + + # Update the center and right boundary if we've expanded beyond the current right boundary + if i + p[i] > r: + c = i + r = i + p[i] + + # Find the maximum length palindrome + max_len = max(p) + center_index = p.index(max_len) + + # Extract the original palindrome from the transformed string + start = (center_index - max_len) // 2 + return s[start:start + max_len] + diff --git a/tests/test_strings.py b/tests/test_strings.py index e7a68302a..c30924f86 100644 --- a/tests/test_strings.py +++ b/tests/test_strings.py @@ -42,7 +42,8 @@ longest_palindrome, knuth_morris_pratt, panagram, - fizzbuzz + fizzbuzz, + manacher ) import unittest @@ -728,5 +729,33 @@ def test_fizzbuzz(self): self.assertEqual(result, expected) +class TestManacherAlgorithm(unittest.TestCase): + """Unit tests for the Manacher's Algorithm""" + + def test_single_character(self): + self.assertEqual(manacher("a"), "a") + + def test_even_length_palindrome(self): + self.assertEqual(manacher("abba"), "abba") + + def test_odd_length_palindrome(self): + self.assertEqual(manacher("racecar"), "racecar") + + def test_mixed_palindrome(self): + self.assertIn(manacher("babad"), ["aba", "bab"]) # There can be "bab" or "aba" + + def test_no_palindrome(self): + self.assertEqual(manacher("abcde"), "a") # The longest palindrome is just one character + + def test_empty_string(self): + self.assertEqual(manacher(""), "") + + def test_full_string_palindrome(self): + self.assertEqual(manacher("aaaa"), "aaaa") + + def test_palindrome_with_special_characters(self): + self.assertEqual(manacher("a!b!a"), "a!b!a") + + if __name__ == "__main__": unittest.main()