经典考题8 - Palindrome回文系列

415 阅读6分钟

Palindrome直接暴力解

一般采用 双指针 或者 reverse做比较,来判断是否为palindrome

680. 验证回文字符串 Ⅱ(Easy)

image.png

Solu:

因为最多只能删除一个,排除掉左右可以构成palindrome的部分后,内部剩下的部分分别删除头/尾,判断是否能构成palindrome

image.png

Code:

class Solution:
    def validPalindrome(self, s: str) -> bool:
        def is_palindrome(l, r):
            while l < r:
                if s[l] != s[r]:
                    return False
                l += 1
                r -= 1
            return True
        
        l, r = 0, len(s) - 1
        while l < r and s[l] == s[r]:
            l += 1
            r -= 1
        if l >= r:
            return True
        return is_palindrome(l + 1, r) or is_palindrome(l, r - 1)


9. 回文数(Easy)

image.png

Solu 1:双指针

  • x转换成str后,双指针判断

Code 1:

class Solution:
    def isPalindrome(self, x: int) -> bool:
        if x < 0:
            return False
        x = str(x)
        l, r = 0, len(x) - 1
        while l < r:
            if x[l] != x[r]:
                return False
            l += 1
            r -= 1
        return True

Solu 2:

把整个数字x做翻转得到rev,并把revx做对比

Code 2:

class Solution:
    def isPalindrome(self, x: int) -> bool:
        if x < 0 or (x % 10 == 0 and x != 0):
            return False
        rev, temp = 0, x
        while temp > 0:
            rev = rev * 10 + temp % 10
            temp //= 10
        return rev == x


234. 回文链表(Easy)

image.png

Solu:双指针

  • 三指针prev, slow, fast
    • fast:每次走两步,根据最后fast是停在ListNode/None来判断整串链表的length的奇偶性
      • 停在node上 -> len = odd
      • 停在None上 -> len = even
    • prevslow找链表中点的同时,翻转链表的前半段
      • 中点找到时,链表前半段同时完成翻转。逐一对比prev.valslow.val

Code:

class Solution:
    def isPalindrome(self, head: ListNode) -> bool:
        prev, slow, fast = None, head, head
        while fast and fast.next:
            fast = fast.next.next
            temp = slow.next
            slow.next = prev
            prev = slow
            slow = temp
        if fast:  # odd length
            slow = slow.next
        while slow:
            if slow.val != prev.val:
                return False
            slow = slow.next
            prev = prev.next
        return True


Palindrome Substring(连续) - DP or 中心扩散法

5. 最长回文子串(Medium)

image.png

Solu 1:DP

  • dp[i][j] = s[i:j+1]是否为palindrome
  • dp[i][j] = (s[i]==s[j]) and dp[i+1][j-1]

Code 1:

class Solution:
    def longestPalindrome(self, s: str) -> str:
        # dp[i][j] = True if s[i:j+1] is palindrome
        # dp[i][j] = dp[i+1][j-1] if s[i] == s[j]
        dp = [[False] * len(s) for _ in range(len(s))]
        max_len, l, r = 1, 0, 0
        for i in range(len(s)):
            dp[i][i] = True  # init
        for i in range(len(s) - 2, -1, -1):
            for j in range(i + 1, len(s)):
                if s[i] == s[j] and (j - i == 1 or dp[i + 1][j - 1]):
                    dp[i][j] = True
                    if j - i + 1 > max_len:
                        max_len = j - i + 1
                        l, r = i, j
        return s[l:r + 1]

Solu 2:中心扩散法(双指针)

  • 每个idx,分别以l=r=idx(len=odd)和 l=idx, r=idx+1(len=even)向两边扩散,寻找以s[l]s[r]为中心点的最长的回文子串

Code 2:

class Solution:
    def __init__(self):
        self.max_len = 0
        self.l = 0
        self.r = 0
    
    def longestPalindrome(self, s: str) -> str:
        def extend(l, r):
            while l >= 0 and r < len(s) and s[l] == s[r]:
                l -= 1
                r += 1
            if self.max_len < r - l - 1:
                self.max_len = r - l - 1
                self.l = l + 1
                self.r = r - 1
        
        for i in range(len(s)):
            extend(i, i)
            extend(i, i + 1)
        return s[self.l:self.r + 1]


647. 回文子串(Medium)

image.png

Solu 1:DP

LC 5思路一致:一旦dp[i][j] = true,则increment palindrome_count

Code 1:

class Solution:
    def countSubstrings(self, s: str) -> int:
        # dp[i][j] = True if s[i:j+1] is palindrome
        # dp[i][j] = dp[i+1][j-1] and s[i] == s[j]
        dp = [[False] * len(s) for _ in range(len(s))]
        count = 0
        for i in range(len(s)):
            dp[i][i] = True  # init, single char is palindrome
            count += 1
        for i in range(len(s) - 2, -1, -1):
            for j in range(i + 1, len(s)):
                if s[i] == s[j] and (j - i < 3 or dp[i + 1][j - 1]):
                    dp[i][j] = True
                    count += 1
        return count

Solu 2:中心扩散法(双指针)

LC 5思路一致:中心扩散时,一旦s[l] == s[r](即:s[l:r+1]为palindrome),则increment palindrome_count

Code 2:

class Solution:
    def __init__(self):
        self.count = 0
    
    def countSubstrings(self, s: str) -> int:
        def expand(left, right):
            while left >= 0 and right < len(s) and s[left] == s[right]:
                self.count += 1
                left -= 1
                right += 1
        
        for i in range(len(s)):
            expand(i, i)  # odd length
            expand(i, i + 1)  # even length
        return self.count


Palindrome Subsequence(不一定连续)- DP

因为“中心扩散法”无法跳过某个char,所以子序列(不一定连续)直接用DP解

516. 最长回文子序列(Medium)

image.png

Solu 1:DP

  • dp[i][j] = s[i:j+1]中最长回文子序列的长度
  • p[i][j] = max(dp[i][j-1], dp[i+1][j]) if s[i] != s[j] else dp[i+1][j-1] + 2

Code 1:

class Solution:
    def longestPalindromeSubseq(self, s: str) -> int:
        # dp[i][j] = s[i:j+1]中最长回文子序列的长度
        # dp[i][j] = max(dp[i][j-1], dp[i+1][j]) if s[i] != s[j] else dp[i+1][j-1] + 2
        dp = [[0] * len(s) for _ in range(len(s))]
        for i in range(len(s) - 1, -1, -1):
            dp[i][i] = 1  # init
            for j in range(i + 1, len(s)):
                if s[i] == s[j]:
                    dp[i][j] = dp[i + 1][j - 1] + 2
                else:
                    dp[i][j] = max(dp[i][j - 1], dp[i + 1][j])
        return dp[0][len(s) - 1]

Solu 2:记忆化DFS

  • dfs(l, r) = 在s[l:r+1]中能产生的最长回文子序列的长度

Code 2:

class Solution:
    def __init__(self):
        self.memo = {}
    
    def longestPalindromeSubseq(self, s: str) -> int:
        def dfs(l, r):
            if l > r:
                return 0
            if l == r:
                return 1
            key = (l, r)
            if key in self.memo:
                return self.memo[key]
            self.memo[key] = max(dfs(l + 1, r), dfs(l, r - 1)) if s[l] != s[r] else dfs(l + 1, r - 1) + 2
            return self.memo[key]
        
        return dfs(0, len(s) - 1)


1216. 验证回文字符串 III(Hard)

image.png

Solu:DP

LC 516的变种:如果对于sk >= min(#最少需要删除的字符) = len(s) - len(最长回文子序列),则是一个「k回文」

Code:

class Solution:
    def isValidPalindrome(self, s: str, k: int) -> bool:
        # dp[i][j] = s[i:j+1]中最长回文subsequence的长度
        # dp[i][j] = max(dp[i][j-1], dp[i+1][j]) if s[i] != s[j] else dp[i+1][j-1] + 2
        dp = [[0] * len(s) for _ in range(len(s))]
        for i in range(len(s) - 1, -1, -1):
            dp[i][i] = 1  # init
            for j in range(i + 1, len(s)):
                if s[i] == s[j]:
                    dp[i][j] = dp[i + 1][j - 1] + 2 if j > i + 1 else 2
                else:
                    dp[i][j] = max(dp[i][j - 1], dp[i + 1][j])
        
        return k >= len(s) - dp[0][-1]


Palindrome + Backtracking

131. 分割回文串(Medium)

Solu:DFS回溯

  • 每次添加s[start : split](如果是回文串的话),这样下次递归DFS时,分割的起始位置就是split
  • 优化:先用dp,对判断任意s[i:j]是否为palindrome做预处理。这样在添加s[start : split]时可以直接从dp table中去取

Code:

class Solution:
    def __init__(self):
        self.ans = []
    
    def partition(self, s: str) -> List[List[str]]:
        def isPalindrome():
            dp = [[False for _ in range(len(s))] for _ in range(len(s))]
            # dp[i][j] = True if s[i:j+1] is palindrome
            # dp[i][j] = True if dp[i+1][j-1] and s[i] == s[j]
            for i in range(len(s)):
                dp[i][i] = True  # init: single char is palindrome
            for i in range(len(s) - 2, -1, -1):
                for j in range(i + 1, len(s)):
                    if s[i] == s[j] and (j - i < 3 or dp[i + 1][j - 1]):
                        dp[i][j] = True
            return dp
        
        def dfs(start, path, dp):
            if start == len(s):
                self.ans.append(path[:])
                return
            for split in range(start + 1, len(s) + 1):
                if dp[start][split - 1]:
                    path.append(s[start:split])
                    dfs(split, path, dp)
                    path.pop()  # backtracking
        
        dp = isPalindrome()  # 预处理,提前判断是否是回文
        dfs(0, [], dp)
        return self.ans


132. 分割回文串 II(Hard)

image.png

Solu 1:记忆化DFS

  • dfs(start) = min{使s[start:]分割出来的每个子串都是palindrome的分割次数}

Code 1:

class Solution:
    def __init__(self):
        self.memo = {}
    
    def minCut(self, s: str) -> int:
        def isPalindrome():
            dp = [[False for _ in range(len(s))] for _ in range(len(s))]
            # dp[i][j] = True if s[i:j+1] is palindrome
            # dp[i][j] = True if dp[i+1][j-1] and s[i] == s[j]
            for i in range(len(s)):
                dp[i][i] = True  # init: single char is palindrome
            for i in range(len(s) - 2, -1, -1):
                for j in range(i + 1, len(s)):
                    if s[i] == s[j] and (j - i < 3 or dp[i + 1][j - 1]):
                        dp[i][j] = True
            return dp
        
        def dfs(start) -> int:
            if start == len(s):
                return 0
            if start in self.memo:
                return self.memo[start]
            res = float('inf')
            for split in range(start + 1, len(s) + 1):
                if dp[start][split - 1]:
                    res = min(res, 1 + dfs(split))
            self.memo[start] = res
            return res
        
        dp = isPalindrome()  # dp预处理
        return dfs(0)

Solu 2:DP

  • dp[i] = min{使s[:i]分割出来的每个子串都是palindrome的分割次数}
  • 状态转移方程:
    • If s[:i] is NOT palindrome, then dp[i] = min{dp[j]} + 1 (if s[j+1 : i] is palindrome)
    • Otherwise, dp[i] = 0

Code 2:

class Solution:
    def minCut(self, s: str) -> int:
        def isPalindrome():
            dp = [[False for _ in range(len(s))] for _ in range(len(s))]
            # dp[i][j] = True if s[i:j+1] is palindrome
            # dp[i][j] = True if dp[i+1][j-1] and s[i] == s[j]
            for i in range(len(s)):
                dp[i][i] = True  # init: single char is palindrome
            for i in range(len(s) - 2, -1, -1):
                for j in range(i + 1, len(s)):
                    if s[i] == s[j] and (j - i < 3 or dp[i + 1][j - 1]):
                        dp[i][j] = True
            return dp
        
        isPalindrome = isPalindrome()
        dp = [len(s) - 1] * len(s)
        for i in range(len(s)):
            if isPalindrome[0][i]:
                dp[i] = 0
            else:
                for j in range(1, i + 1):
                    if isPalindrome[j][i]:
                        dp[i] = min(dp[i], dp[j - 1] + 1)
        return dp[-1]


Reference: