งูต้นฉบับเลื่อนกระจกสองด้านจากจุดกึ่งกลางของแถวลูกปัดเพื่อหาช่วงสมมาตรยาวที่สุด

LeetCode #5 Longest Palindromic Substring: ขยายจากกลาง, DP หรือ Manacher

พี่โชว์Engineering

Longest Palindromic Substring ให้คืน substring สมมาตรที่ยาวที่สุด จุดที่พลาดบ่อยคือ palindrome มีศูนย์กลางได้ทั้งบนตัวอักษรหนึ่งตัวและช่องว่างระหว่างสองตัวครับ

วิธี 1: Expand Around Center (default)

ลองศูนย์กลาง 2n-1 แบบ แล้วขยายซ้าย-ขวาตราบที่ตัวอักษรเท่ากัน เวลาแย่สุด O(n²) แต่ใช้พื้นที่เพิ่ม O(1) โค้ดสั้นและเหมาะกับ constraint ทั่วไป

วิธี 2: Dynamic Programming

ให้ dp[left][right] บอกว่าช่วงนั้นเป็น palindrome เมื่อปลายเท่ากันและช่วงข้างในเป็น palindrome ใช้เวลา O(n²) และพื้นที่ O(n²) เหมาะเมื่อโจทย์ต่อยอดถามหลายช่วง

วิธี 3: Manacher

แปลง odd/even centers ให้อยู่ใน representation เดียว แล้ว reuse รัศมีจาก palindrome ที่ครอบอยู่ ได้เวลา O(n) และพื้นที่ O(n) แต่ invariant ซับซ้อนกว่า ควรใช้เมื่อ input ใหญ่มากหรือ latency บังคับ

from __future__ import annotations

import unittest


def expand_center(s: str) -> str:
    best_left = best_right = 0
    for center in range(2 * len(s) - 1):
        left = center // 2
        right = left + center % 2
        while left >= 0 and right < len(s) and s[left] == s[right]:
            if right - left > best_right - best_left:
                best_left, best_right = left, right
            left -= 1
            right += 1
    return s[best_left:best_right + 1] if s else ""


def dynamic_programming(s: str) -> str:
    n = len(s)
    if n < 2:
        return s
    dp = [[False] * n for _ in range(n)]
    best_left = 0
    best_len = 1
    for right in range(n):
        for left in range(right + 1):
            dp[left][right] = s[left] == s[right] and (
                right - left <= 2 or dp[left + 1][right - 1]
            )
            if dp[left][right] and right - left + 1 > best_len:
                best_left, best_len = left, right - left + 1
    return s[best_left:best_left + best_len]


def manacher(s: str) -> str:
    transformed = "^#" + "#".join(s) + "#$"
    radius = [0] * len(transformed)
    center = right = best_center = best_radius = 0
    for index in range(1, len(transformed) - 1):
        mirror = 2 * center - index
        if index < right:
            radius[index] = min(right - index, radius[mirror])
        while transformed[index + radius[index] + 1] == transformed[index - radius[index] - 1]:
            radius[index] += 1
        if index + radius[index] > right:
            center, right = index, index + radius[index]
        if radius[index] > best_radius:
            best_center, best_radius = index, radius[index]
    start = (best_center - best_radius) // 2
    return s[start:start + best_radius]


class PalindromeTests(unittest.TestCase):
    def test_all_methods(self):
        cases = [("", 0), ("a", 1), ("babad", 3), ("cbbd", 2), ("aaaa", 4)]
        for solve in (expand_center, dynamic_programming, manacher):
            for raw, expected_length in cases:
                result = solve(raw)
                self.assertEqual(len(result), expected_length)
                self.assertEqual(result, result[::-1])
                self.assertIn(result, raw)


if __name__ == "__main__":
    unittest.main()

Decision table

เงื่อนไขExpand centerDPManacher
เวลาO(n²)O(n²)O(n)
พื้นที่เพิ่มO(1)O(n²)O(n)
รองรับ odd/evenใช่ใช่ใช่ ผ่าน transform
คืน substringตรงไปตรงมาตรงไปตรงมาต้องแปลง index กลับ
ความเสี่ยง bugต่ำกลางสูง
เหมาะเป็น defaultดีที่สุดเมื่อ reuse tableเมื่อ n ใหญ่มาก

Recap

  • เทสต์ทั้งศูนย์กลางบนตัวอักษรและระหว่างตัวอักษร
  • อย่าคืน subsequence แทน substring
  • ใช้ expand-around-center เป็น default ที่อ่านง่าย
  • เลือก Manacher ต่อเมื่อ constraint คุ้มกับความซับซ้อน