目录

【LeetCode 热题100】139:单词拆分(动态规划全解析+细节陷阱)(Go语言版)

🚀 LeetCode 热题 139:单词拆分(Word Break)| 动态规划全解析+细节陷阱

📌 题目描述

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请判断 s 是否可以由字典中出现的单词拼接成。
说明:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。

🎯 示例 1:

复制代码
输入:s = "leetcode", wordDict = ["leet", "code"]
输出:true
解释:返回 true 因为 "leetcode" 可以被拆分为 "leet code"

🎯 示例 2:

复制代码
输入:s = "applepenapple", wordDict = ["apple", "pen"]
输出:true
解释:可以拼接成 "apple pen apple",可以重复使用 wordDict 中的单词。

🎯 示例 3:

复制代码
输入:s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
输出:false

💡 解题思路一:动态规划(DP)

我们定义一个布尔类型的一维数组 dp[i] 表示 s[0:i] 这个子串是否可以由字典中的单词组成。

🧱 状态定义:

我们用一个布尔数组 dp[i] 表示:

从字符串的起始位置(下标 0)到位置 i 的子串 s[0:i] 是否可以被成功拆分成一个或多个字典中的单词。
注意:这里的 i 是长度,不是下标(下标是 i-1)。


✅ 状态转移方程:

text 复制代码
dp[i] = true,若存在 j,使得 dp[j] == true 且 s[j:i] 在 wordDict 中

也就是说,如果前面某个位置 j 之前的子串可以被拼出,并且 s[j:i] 在字典中,那 dp[i] 就可以设置为 true

✅ 初始条件:

text 复制代码
dp[0] = true  // 空字符串可视为已完成

💻 Go 实现代码(动态规划)

go 复制代码
func wordBreak(s string, wordDict []string) bool {
    wordSet := make(map[string]bool)
    for _, word := range wordDict {
        wordSet[word] = true
    }

    dp := make([]bool, len(s)+1)
    dp[0] = true

    for i := 1; i <= len(s); i++ {
        for j := 0; j < i; j++ {
            if dp[j] && wordSet[s[j:i]] {
                dp[i] = true
                break
            }
        }
    }
    return dp[len(s)]
}

🔍 注意点 & 边界问题解析

✅ 注意 1:不要和子串的下标混淆

  • dp[i] 代表的是前 i 个字符构成的子串(即 s[0:i]),而不是下标 i 的字符。
  • 所以判断子串是否在字典中时,要写 s[j:i],而不是 s[j:i+1]

✅ 注意 2:字典查找效率

使用 map[string]bool 构造一个哈希集合 wordSet 替代数组,可以让查找从 O(n) 降到 O(1),大幅提升效率:

go 复制代码
wordSet := make(map[string]bool)
for _, word := range wordDict {
    wordSet[word] = true
}

✅ 注意 3:剪枝优化(提前结束)

只要在内层循环找到一个合法切割位置,就可以直接 break,节省无效循环。


✅ 注意 4:空字符串与空字典

  • s = "",返回 true,空字符串默认可以拆分(dp[0]=true)。
  • wordDict = [],返回 false,无单词可用无法拼出。

🌈 图解理解

假设输入:

text 复制代码
s = "applepenapple"
wordDict = ["apple", "pen"]

我们依次维护 dp 状态:

i 子串 s[0:i] 能否拆分 dp[i]
0 "" true
5 "apple" true
8 "applepen" true
13 "applepenapple" true

🧠 更深层的理解(背后的思想)

本题其实可以抽象为:

把一个字符串切成若干段,看这些段是否全都能在字典中找到。

也可以类比为:

一个人从字符串左端起跳,只能跳到在字典中出现的词结尾位置,问能否跳到终点。

所以动态规划是处理"前缀是否可达"这种问题的最优解。


⏳ 复杂度分析

类型 复杂度
时间复杂度 O(n²)
空间复杂度 O(n)
  • n 是字符串长度。
  • 时间复杂度主要来自两层循环和切片操作。
  • 使用哈希集合加速查找操作。

🧠 解题思路二:记忆化搜索(DFS + 记忆化)

从起始位置出发,尝试所有可能的切割位置,只要有一个可行就返回 true

为了防止重复计算,使用 map[int]bool 进行记忆。


💻 Go 实现代码(记忆化 DFS)

go 复制代码
func wordBreak(s string, wordDict []string) bool {
    wordSet := make(map[string]bool)
    for _, word := range wordDict {
        wordSet[word] = true
    }

    memo := make(map[int]bool)

    var dfs func(int) bool
    dfs = func(start int) bool {
        if start == len(s) {
            return true
        }
        if val, ok := memo[start]; ok {
            return val
        }

        for end := start + 1; end <= len(s); end++ {
            if wordSet[s[start:end]] && dfs(end) {
                memo[start] = true
                return true
            }
        }

        memo[start] = false
        return false
    }

    return dfs(0)
}

🔍 两种方法对比

方法 优点 缺点
动态规划 性能稳定,逻辑清晰,适合面试高频 实现上可能略显冗长
记忆化 DFS 更贴近人类思考方式,递归直观 有栈溢出风险,依赖剪枝优化

✅ 总结

  • 本题是经典的字符串 + 动态规划题型。
  • 动态规划和记忆化搜索都值得掌握!
  • 实际编码中推荐使用动态规划,执行效率更高。

🎁 加分思考

  • 如果要求输出所有可能的拆分方式?
  • 如果字典非常大,如何优化查找?(使用 Trie 前缀树)

🌟 更多高频算法题持续更新中...

欢迎点赞 👍、收藏 ⭐、评论 💬、关注 🧠,支持我继续输出优质 LeetCode 题解!💻📘📌


本文是转载文章,点击查看原文
如有侵权,请联系 xyy@jishuzhan.net 删除
相关推荐
林泽毅27 分钟前
UNet脑瘤医学影像分割训练实战(PyTorch 完整代码)
深度学习·算法·机器学习
珊瑚里的鱼1 小时前
【双指针】专题:LeetCode 202题解——快乐数
开发语言·c++·笔记·算法·leetcode·职场和发展
David Bates1 小时前
代码随想录第18天:二叉树
python·算法·二叉树
想成为配环境大佬1 小时前
P8739 [蓝桥杯 2020 国 C] 重复字符串
算法·蓝桥杯·贪心
莫有杯子的龙潭峡谷2 小时前
4.15 代码随想录第四十四天打卡
c++·算法
A懿轩A2 小时前
2025年十六届蓝桥杯Python B组原题及代码解析
python·算法·蓝桥杯·idle·b组
灋✘逞_兇2 小时前
快速幂+公共父节点
数据结构·c++·算法·leetcode
姜行运2 小时前
每日算法(双指针算法)(Day 1)
c++·算法·c#
stoneSkySpace3 小时前
算法——BFS
前端·javascript·算法
明月看潮生3 小时前
青少年编程与数学 02-016 Python数据结构与算法 20课题、几何算法
python·算法·青少年编程·编程与数学