SWE / MLE · Coding

Meta 最多删K字符成回文:双指针分支与区间DP

将最多删1个、2个和K个字符合并为同题变体,说明固定小K与一般K的复杂度区别。

题目重点:将最多删1个、2个和K个字符合并为同题变体,说明固定小K与一般K的复杂度区别。

题意与假设

这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。

  • 字符大小写敏感,删除不改变剩余字符顺序,允许少于K次;空串为回文,K非负。只返回是否可行,不要求输出删除位置。
  • 这里是删除字符而非交换、替换或删除连续块。示例Python按Unicode码点,若输入要求grapheme需先分词。
  • 不需删除的标准验证变体可先忽略非ASCII字母数字并大小写不敏感,用is_alnum_palindrome_ascii;这不等于删除预算中的原字符比较,预处理合同不同。

解题路线

  1. 双指针从两端跳过相同字符;遇到冲突时任何回文结果必须删除左端或右端至少一个。分别递归剩余区间并减少budget,两支取OR;budget0时冲突立即失败。
  2. K=1时只需检查删除左端或右端后的两个子串,K=2至多再分两层。不能贪心总删左端,例如不同输入可能只有删右端可行。缓存(left,right,budget)避免同状态重复,但不能据此凭空说一般K必为O(nK)。
  3. 一般K用最少删除区间DP:相等取内区间,不等取1+min(删左,删右)。长度1为0,空区间为0;最终minDeletes≤K。
  4. 滚动一行dp时left倒序、right正序,旧dp[right]代表下一行,dp[right-1]是本行,diagonal保存旧内区间。覆盖顺序错误会把同一行结果当旧行。
  5. 若需输出删除位置,保存二维选择或进行带检查点的重构;一行长度值本身不能直接恢复完整路径。大输入递归可能遇Python栈深限制,一般K优先迭代DP。
  6. 标准忽略标点的验证以两指针跳过非法字符,比较ASCII字符lower;只求合法性O(N)时间/O(1)状态,不生成过滤副本。删除K变体仍对原text大小写敏感,不共用未声明normalize。

手动走一遍

abca最多删1可行:删b或c。abc最多删1不行,最多删2可行。abcbda的最少删除是1(删d得到abcba);在每次冲突固定删左端不能证明正确,应枚举两端候选。 标准验证A man, a plan, a canal: Panama为True,但大小写敏感且预算0的原字符删除合同不能当作同一答案。

Python 参考实现

下面的代码按上文假设实现,适合本地运行并追加自己的测试用例。

代码区可横向滚动;使用键盘时,先聚焦代码区再按方向键查看。

from functools import lru_cache


def can_make_palindrome(text, k):
    """At most k deleted characters, case sensitive. Memoized branching baseline."""
    if k < 0: raise ValueError('nonnegative budget')
    @lru_cache(None)
    def solve(left, right, budget):
        while left < right and text[left] == text[right]:
            left += 1; right -= 1
        if left >= right: return True
        if budget == 0: return False
        return solve(left+1, right, budget-1) or solve(left, right-1, budget-1)
    return solve(0, len(text)-1, k)


def minimum_deletions(text):
    """O(n^2) time, O(n) auxiliary space rolling interval DP."""
    n=len(text);dp=[0]*n
    for left in range(n-2,-1,-1):
        diagonal=0
        for right in range(left+1,n):
            old=dp[right]
            dp[right]=diagonal if text[left]==text[right] else 1+min(dp[right],dp[right-1])
            diagonal=old
    return dp[-1] if n else 0


def is_alnum_palindrome_ascii(text):
    """No deletions budget; ignore non-ASCII-alphanumeric, compare case-insensitive."""
    def allowed(ch):return 'a'<=ch<='z' or 'A'<=ch<='Z' or '0'<=ch<='9'
    left=0;right=len(text)-1
    while left<right:
        if not allowed(text[left]):left+=1;continue
        if not allowed(text[right]):right-=1;continue
        if text[left].lower()!=text[right].lower():return False
        left+=1;right-=1
    return True

复杂度

固定小K的分支基线保守O(n·2^K)时间、O(K)递归深度加缓存;K=1或2可视为O(n)。一般DP O(n²)时间/O(n)辅助空间;不能把任意K的双指针递归直接标O(nK)。

易错点与追问

容易失分的边界

  • 最多K不等于必须正好K,已经回文可以不删。
  • 用最长回文子串长度判断删除数错误:删除保留的是子序列。

继续深挖

  • 若返回一个最少删除后的回文,怎样记录tie政策与重构选择?
  • K非常小而n很大时,为什么指数只依赖K的分支可能比全二维DP合适?
✓已复制微信号!