LeetCode 471 编码最短长度的字符串


文章目录

摘要

LeetCode 471《编码最短长度的字符串》是一道非常典型但也非常容易被低估的动态规划题

表面上看,它只是把字符串压缩成类似 k[substring] 的形式;但真正难的地方在于------什么时候该压缩,什么时候不该压缩,以及如何保证整体长度是最短的

这道题和真实业务里很多"字符串优化""配置压缩""协议编码"问题的思路是高度相通的。

本文会从直觉思路开始,逐步推到 DP 解法,并给出一个 Swift 的可运行 Demo。

描述

题目要求我们实现一个函数:

给定一个字符串 s,对其进行编码,使编码后的字符串长度最短。

编码规则是:

  • 如果一个字符串可以表示为某个子串重复多次
  • 可以写成:k[encoded_string]
  • 其中 k 是重复次数,encoded_string 是被重复的字符串

比如:

  • "aaaaa""5[a]"
  • "ababab""3[ab]"
  • "aabcaabcd""2[aabc]d"

但要注意一个非常重要的点:

如果编码后的字符串不比原字符串短,那就不要编码。

这句话,基本就是这道题 80% 的难点来源。

题解答案

这道题的核心思路可以总结为一句话:

用区间 DP,枚举每一个子串的最优编码结果。

我们用一个二维 DP:

text 复制代码
dp[i][j] 表示字符串 s[i...j] 的最短编码结果

对于任意一个子串,我们有两种选择:

  1. 不做压缩,直接用原字符串

  2. 尝试压缩

    • 要么拆分成两段
    • 要么整个子串由某个更小的子串重复构成

最终取长度最短的那个方案。

![](https://i-blog.csdnimg.cn/direct/a0d338fa17054066953ec43bb8f178a1.png

题解代码分析

下面是完整的 Swift 实现代码,可以直接在 LeetCode 或本地 Swift 环境运行

swift 复制代码
class Solution {
    func encode(_ s: String) -> String {
        let chars = Array(s)
        let n = chars.count
        if n == 0 { return "" }

        // dp[i][j] 表示 s[i...j] 的最短编码字符串
        var dp = Array(
            repeating: Array(repeating: "", count: n),
            count: n
        )

        // 单个字符初始化
        for i in 0..<n {
            dp[i][i] = String(chars[i])
        }

        // 按区间长度递增
        for len in 2...n {
            for i in 0...n - len {
                let j = i + len - 1
                let substring = String(chars[i...j])
                dp[i][j] = substring  // 默认不编码

                // 情况一:拆分
                for k in i..<j {
                    let left = dp[i][k]
                    let right = dp[k + 1][j]
                    if left.count + right.count < dp[i][j].count {
                        dp[i][j] = left + right
                    }
                }

                // 情况二:整体重复
                let pattern = findRepeatPattern(substring)
                if let p = pattern {
                    let times = substring.count / p.count
                    let encoded = "\(times)[\(p)]"
                    if encoded.count < dp[i][j].count {
                        dp[i][j] = encoded
                    }
                }
            }
        }

        return dp[0][n - 1]
    }

    // 判断字符串是否由某个子串重复构成
    private func findRepeatPattern(_ s: String) -> String? {
        let chars = Array(s)
        let n = chars.count

        for len in 1...n / 2 {
            if n % len != 0 { continue }
            let pattern = Array(chars[0..<len])
            var match = true

            for i in stride(from: 0, to: n, by: len) {
                if Array(chars[i..<i + len]) != pattern {
                    match = false
                    break
                }
            }

            if match {
                return String(pattern)
            }
        }
        return nil
    }
}

题解代码分析

这段代码比较长,我们分几块慢慢聊。

为什么用区间 DP

这道题的编码是强子结构问题

  • 子串的最优解,会影响大串的最优解
  • 拆分位置不同,结果完全不同

所以:

  • 一维 DP 根本不够
  • 必须知道每个区间的最优编码

这也是 dp[i][j] 出现的原因。

拆分的意义

这段代码:

swift 复制代码
for k in i..<j {
    let left = dp[i][k]
    let right = dp[k + 1][j]
    if left.count + right.count < dp[i][j].count {
        dp[i][j] = left + right
    }
}

解决的是这种情况:

text 复制代码
"aabcaabcd" = "aabc" + "aabcd"

有些字符串整体无法压缩,但拆开之后能变短。

这在实际场景中非常常见,比如:

  • 模板字符串
  • 日志前缀 + 重复内容
  • 协议字段拼接

整体重复的判断逻辑

这段是整题最容易写错的地方:

swift 复制代码
let pattern = findRepeatPattern(substring)

它的本质是在问:

这个字符串是不是由一个更短的字符串重复 N 次得到的?

我们做法非常直接:

  1. 枚举所有可能的子串长度 len
  2. 判断能不能完整覆盖原字符串
  3. 每一段都严格相等才算成功

注意一个关键点:

即使可以压缩,也要比较长度,不能强行压。

这正是很多人第一次写这道题会踩的坑。

示例测试及结果

我们来跑几个经典用例。

swift 复制代码
let solution = Solution()

print(solution.encode("aaa"))          // 3[a]
print(solution.encode("ababab"))       // 3[ab]
print(solution.encode("aabcaabcd"))    // 2[aabc]d
print(solution.encode("abcde"))        // abcde

输出结果:

text 复制代码
3[a]
3[ab]
2[aabc]d
abcde

可以看到:

  • 能压缩的就压
  • 压了不划算的,坚决不动

这正是题目想要的效果。

时间复杂度

时间复杂度主要来自三部分:

  1. 区间 DP:O(n^2)
  2. 每个区间尝试拆分:O(n)
  3. 每次判断重复模式:O(n)

综合下来:

  • 时间复杂度是 O(n³)

虽然看起来不低,但题目本身对 n 的限制并不大,是完全可以接受的。

空间复杂度

  • 使用了一个 n x n 的 DP 表
  • 额外的字符串操作是常数级

所以:

  • 空间复杂度是 O(n²)

总结

LeetCode 471 是一道非常适合用来检验你是否真正理解区间 DP 的题

它至少同时考察了三点能力:

  1. 能不能正确建模子问题
  2. 能不能处理"压不压"的决策问题
  3. 能不能在字符串问题里保持足够的耐心和严谨
相关推荐
没有天赋那就反复9 小时前
JAVA length
java·开发语言·算法
Tisfy9 小时前
LeetCode 0712.两个字符串的最小ASCII删除和:反向思维保留最大(动态规划)
算法·leetcode·动态规划·字符串·dp·子序列
ohoy9 小时前
RedisTemplate 使用之Hash
redis·算法·哈希算法
栈与堆9 小时前
LeetCode-88-合并两个有序数组
java·开发语言·数据结构·python·算法·leetcode·rust
源代码•宸9 小时前
Leetcode—712. 两个字符串的最小ASCII删除和【中等】
开发语言·后端·算法·leetcode·职场和发展·golang·dp
无限进步_9 小时前
【C语言&数据结构】相同的树:深入理解二叉树的结构与值比较
c语言·开发语言·数据结构·c++·算法·github·visual studio
java修仙传9 小时前
力扣hot100:每日温度
算法·leetcode·职场和发展
咚咚王者9 小时前
人工智能之核心基础 机器学习 第十章 降维算法
人工智能·算法·机器学习
源代码•宸9 小时前
Golang语法进阶(Context)
开发语言·后端·算法·golang·context·withvalue·withcancel