diff --git a/wizard-work/beam/manacher.py b/wizard-work/beam/manacher.py new file mode 100644 index 0000000..664064a --- /dev/null +++ b/wizard-work/beam/manacher.py @@ -0,0 +1,49 @@ +"""Manacher's algorithm: longest palindromic substring in O(n).""" +from __future__ import annotations + + +def longest_palindrome(s: str) -> str: + if s == "": + return "" + # Transform: insert sentinels so every palindrome has odd length. + t = "^#" + "#".join(s) + "#$" + n = len(t) + p = [0] * n + c = r = 0 + for i in range(1, n - 1): + mirror = 2 * c - i + if i < r: + p[i] = min(r - i, p[mirror]) + # expand + while t[i + p[i] + 1] == t[i - p[i] - 1]: + p[i] += 1 + if i + p[i] > r: + c = i + r = i + p[i] + # find center with max radius + center = 1 + for i in range(1, n - 1): + if p[i] > p[center]: + center = i + start = (center - p[center]) // 2 + return s[start:start + p[center]] + + +def all_palindromic_substrings(s: str) -> list[str]: + """Return all distinct palindromic substrings (may be large).""" + res = set() + n = len(s) + for i in range(n): + # odd + l = r = i + while l >= 0 and r < n and s[l] == s[r]: + res.add(s[l:r + 1]) + l -= 1 + r += 1 + # even + l, r = i, i + 1 + while l >= 0 and r < n and s[l] == s[r]: + res.add(s[l:r + 1]) + l -= 1 + r += 1 + return sorted(res) diff --git a/wizard-work/beam/test_manacher.py b/wizard-work/beam/test_manacher.py new file mode 100644 index 0000000..aac2f92 --- /dev/null +++ b/wizard-work/beam/test_manacher.py @@ -0,0 +1,46 @@ +import manacher + + +def test_empty(): + assert manacher.longest_palindrome("") == "" + + +def test_single(): + assert manacher.longest_palindrome("a") == "a" + + +def test_known(): + cases = { + "babad": "bab", + "cbbd": "bb", + "a": "a", + "ac": "a", + "racecar": "racecar", + "abacdfgdcaba": "aba", + "aaaa": "aaaa", + "abacaba": "abacaba", + } + for s, exp in cases.items(): + got = manacher.longest_palindrome(s) + # accept any maximal-length palindrome + assert len(got) == len(exp), (s, got, exp) + assert got == got[::-1] + + +def test_all_same(): + s = "x" * 1000 + assert manacher.longest_palindrome(s) == s + + +def test_no_palindrome_longer_than_one(): + s = "abcdef" + assert len(manacher.longest_palindrome(s)) == 1 + + +def test_even_center(): + assert manacher.longest_palindrome("abba") == "abba" + + +def test_distinct_substrings_smoke(): + out = manacher.all_palindromic_substrings("aaa") + assert set(out) == {"a", "aa", "aaa"}