Description

給定字串 s,找出兩個不相交的回文子序列,使它們長度的乘積最大。回傳該最大乘積。(兩個子序列不相交意味著它們不共用任何索引。)

Example:

Input: s = “leetcodecom” Output: 9 (選擇 “ete” 和 “coc”,長度乘積 3 × 3 = 9)

Intuition#

核心思路:字串長度最多 12,可以用 Bitmask 枚舉所有子序列(2^12 = 4096),預計算每個 mask 是否為回文及其長度,再枚舉不相交的兩個 mask 取最大乘積。

  • 約束 s.length <= 12 暗示 Bitmask 暴力列舉可行
  • 第一步:枚舉所有 2^n 個 mask,檢查對應子序列是否為回文,記錄其長度
  • 第二步:枚舉所有 mask 對 (m1, m2),滿足 m1 & m2 == 0(不相交),取最大乘積

Approaches#

1: Bitmask Brute Force#

  • 概念: 枚舉所有 2^n 子集,過濾回文子序列,再雙重迴圈找不相交的最大乘積
  • 時間複雜度: O(2^n * n + 4^n) - 預處理 O(2^n * n),枚舉配對 O(4^n)(最壞)
  • 空間複雜度: O(2^n) - 儲存每個 mask 的回文長度
class Solution {
    fun maxProduct(s: String): Int {
        val n = s.length
        val total = 1 shl n
        val palindromeLen = IntArray(total)

        // 預計算每個 mask 對應子序列是否回文及長度
        for (mask in 1 until total) {
            val subseq = StringBuilder()
            for (i in 0 until n) {
                if (mask and (1 shl i) != 0) subseq.append(s[i])
            }
            if (isPalindrome(subseq.toString())) {
                palindromeLen[mask] = subseq.length
            }
        }

        var maxProd = 0
        for (m1 in 1 until total) {
            if (palindromeLen[m1] == 0) continue
            for (m2 in m1 + 1 until total) {
                if (palindromeLen[m2] == 0) continue
                if (m1 and m2 == 0) {
                    maxProd = maxOf(maxProd, palindromeLen[m1] * palindromeLen[m2])
                }
            }
        }
        return maxProd
    }

    private fun isPalindrome(s: String): Boolean {
        var l = 0; var r = s.length - 1
        while (l < r) {
            if (s[l] != s[r]) return false
            l++; r--
        }
        return true
    }
}

⭐2: Bitmask + Subset Enumeration 優化#

  • 概念: 對每個回文 mask m1,只枚舉其補集 complement 的子集作為 m2,減少無效配對
  • 時間複雜度: O(3^n) - 枚舉子集的複雜度(每個 bit 有三種狀態:屬於 m1、屬於 m2、不屬於任一)
  • 空間複雜度: O(2^n) - 儲存每個 mask 的回文長度
class Solution {
    fun maxProduct(s: String): Int {
        val n = s.length
        val total = 1 shl n
        val palindromeLen = IntArray(total)

        for (mask in 1 until total) {
            val subseq = StringBuilder()
            for (i in 0 until n) {
                if (mask and (1 shl i) != 0) subseq.append(s[i])
            }
            val str = subseq.toString()
            var l = 0; var r = str.length - 1
            var isPalin = true
            while (l < r) {
                if (str[l] != str[r]) { isPalin = false; break }
                l++; r--
            }
            if (isPalin) palindromeLen[mask] = str.length
        }

        var maxProd = 0
        for (m1 in 1 until total) {
            if (palindromeLen[m1] == 0) continue
            // 枚舉 m1 補集的所有子集
            val complement = (total - 1) xor m1
            var m2 = complement
            while (m2 > 0) {
                if (palindromeLen[m2] > 0) {
                    maxProd = maxOf(maxProd, palindromeLen[m1] * palindromeLen[m2])
                }
                m2 = (m2 - 1) and complement
            }
        }
        return maxProd
    }
}

子集枚舉技巧 m2 = (m2 - 1) & complement 可以高效地遍歷 complement 的所有非空子集。這是 Bitmask DP 中常見的優化手法。

🔑 Takeaways#

  • Pattern: 小規模字串(n <= 12~20)-> Bitmask 枚舉所有子集
  • 關鍵技巧: 補集子集枚舉 m2 = (m2-1) & complement 將 O(4^n) 優化到 O(3^n);Bitmask 常用於需要枚舉所有子集組合的問題