Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Manacher's Algorithm and Unit Tests for Longest Palindromic Substring #931

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion algorithms/strings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,5 @@
from .atbash_cipher import *
from .longest_palindromic_substring import *
from .knuth_morris_pratt import *
from .panagram import *
from .panagram import *
from .manacher import *
39 changes: 39 additions & 0 deletions algorithms/strings/manacher.py
Original file line number Diff line number Diff line change
@@ -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]

31 changes: 30 additions & 1 deletion tests/test_strings.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@
longest_palindrome,
knuth_morris_pratt,
panagram,
fizzbuzz
fizzbuzz,
manacher
)

import unittest
Expand Down Expand Up @@ -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()