Word Break:深度理解 DP 前缀结束点的核心思想

题目回顾:Word Break 是在问什么?

题目给定:

  • 一个字符串 s
  • 一个字符串数组 wordDict,表示字典。

要求:

判断 s 能不能被拆分成若干个单词,这些单词都必须来自 wordDict,并且可以重复使用。leetcode

例如:

  • s = "leetcode", wordDict = ["leet", "code"],答案是 true,因为可以拆成 "leet code"。leetcode
  • s = "applepenapple", wordDict = ["apple", "pen"],答案是 true,可以拆成 "apple pen apple"。leetcode
  • s = "catsandog", wordDict = ["cats","dog","sand","and","cat"],答案是 false,怎么拆都不行。leetcode

本题约束:
1 ≤ s.length ≤ 3001 ≤ wordDict.length ≤ 1000leetcode

初始思路:dp[i] 表示"前 i 个字母能否被拼出"

一开始的直觉非常自然:

  • 定义 dp[i] 表示 前 i 个字符 能不能由字典里的单词拼出来。
  • 用 C 字符串的说法,就是判断 s[0..i-1] 这一段是不是可拆分。

这是一个非常典型、也是正确的状态定义:

  • dp[0] 对应"前 0 个字符",也就是空串;
  • dp[4] 对应 s[0..3],比如 "leet";
  • dp[n] 对应整个字符串 s[0..n-1]

但是在这个定义下,有两个关键点一开始容易踩坑:

  1. dp[0] 应该是 true 还是 false
  2. 匹配一个单词时,要不要把"单词中间的 dp 位置"也标记为 true

下面按这些疑问,一点点展开。

核心概念:前缀和"前缀结束点"

理解这题 DP 的关键,是把 i 看成一个"前缀结束点"。

什么是前缀

对字符串 s = "applepen"

  • "a"、"ap"、"apple"、"applep"......都叫 s 的前缀。
  • 一般说"长度为 i 的前缀",指的是 s[0..i-1]

什么是前缀结束点

从下标的角度看:

  • 字符的下标是 0..n-1
  • 切割位置n+1 个:开头的 0,字符之间的位置 1..n-1,和最后结尾的 n

dp[i] 定义成:

dp[i] = true 表示:在切割位置 i 把字符串切一刀,左边 s[0..i-1] 这一整段,已经可以完全由字典单词拼出来。

换句话说,i 就是一个"可行前缀的结束点"

这有两个重要含义:

  1. dp[i] == true 的下标 i,才有资格作为"下一个单词的起点";
  2. 单词中间的那些位置,比如 "leet" 的 1、2、3,不能作为"前面那段已经完整拼完"的位置,所以 dp[1], dp[2], dp[3] 不应该被设为 true

为什么 dp[0] 必须是 true

dp[0] 表示:前 0 个字符(空串)能不能被"若干个字典单词"拼出来。

这里有一个常见约定:

  • 空串可以看作由"0 个单词"拼出来。
  • 也就是:不选任何单词,结果就是空串。

这样做的好处是:

  • 把下标 0 当作第一个"可行前缀结束点";
  • 为后面的第一个单词提供一个起点。

例如在 "leetcode" 里:

  • 如果 dp[0] = true,并且 s[0..3] == "leet",就可以把 dp[4] 设为 true,这表示 "leet" 可以被拼出来。
  • 如果 dp[0] = false,那从 0 开始永远扩展不出任何 true 状态,整个 DP 直接挂掉。

顺带一提:

  • 本题的约束保证 s.length >= 1,不会出现空串输入。leetcode
  • 如果从数学上扩展这道题,让 s = "",绝大多数人会认为答案应为 true(空串可以由 0 个单词组成),也和 dp[0] = true 的建模是一致的。

注意:

这里不是说"字典里有一个空串单词",而是说"空前缀是一种已经完成的状态"。

转移逻辑:只从 true 的位置向后扩展

在状态和 dp[0] 搞清楚之后,转移的思想可以归结成一句话:

只从那些 dp[i]true 的位置出发,尝试往后贴单词,更新新的前缀结束点。

更具体地说:

  1. 状态:
    dp[i] 表示 s[0..i-1] 是否可拆分。

  2. 初始化:
    dp[0] = 1true),其余为 0(false)。leetcode

  3. 转移:

    外层遍历 i,从 0 到 str_len - 1

    • 如果 dp[i] == 0,说明前缀 s[0..i-1] 还拼不出来,直接 continue,不能从这里起步。leetcode
    • 如果 dp[i] == 1,说明这里是一个"可行前缀结束点",可以尝试从这里往后贴单词。

    内层遍历字典中的每个 word

    • word_len = strlen(word)leetcode
    • 如果 i + word_len > str_len,说明这个单词贴过去会越界,跳过。leetcode
    • 否则比较子串:如果 s[i..i+word_len-1] 恰好等于这个 word(例如用 strncmp(s + i, word, word_len) == 0),
      说明从 i 出发,接上这个单词,可以安全到达位置 i + word_len
      所以将 dp[i + word_len] = 1leetcode
  4. 结束:

    整个字符串长度为 str_len

    如果 dp[str_len] == 1,表示前 str_len 个字符(也就是整个 s)可以被完全拆分,返回 true;否则返回 falseleetcode

这就是你最后写出的那份 C 代码的完整逻辑:

  • 分配 dp[strlen(s) + 1]
  • memset(dp, 0, ...) 全部初始化为 0;
  • dp[0] = 1
  • 两层循环做扩展;
  • 返回 dp[str_len]leetcode

"单词中间的 dp 值"为什么保持为 false

一开始你有一个想法:

匹配一个单词以后,从 ii+len 中间的 dp 都设为 true

现在可以清晰地看到,这样做是错的。原因有两个:

1. 语义不对

dp[k] 的语义是:前 k 个字符能否被"完全拆完"

  • 如果从 0 匹配 "leet" 到 4,只能说明前 4 个字符 "leet" 是可拆的,也就是 dp[4] = true
  • 不应该得出"dp[1], dp[2], dp[3] 也都可拆"的结论,因为 "l"、"le"、"lee" 并没有被字典单词完全覆盖。

2. 会制造错误的起点

  • 如果把 dp[1..3] 都设为 true,后续在这些位置继续贴单词,会创建出实际不存在的拆分路径。
  • 例如,在复杂的例子中,这可能会让算法认为某个拆法是可行的,但其实中间断掉了。

正确的做法就是:

  • 只更新单词末尾那个位置 :从 i 匹配长度为 len 的单词,只把 dp[i + len] 置为 true
  • 单词内部的位置保持 false ,这样循环走到这些位置时,看到 dp[k] == 0,就会"自然跳过"。

和常见"背包形式"的关系

你提到过想用常见的"背包 DP 模板"来表达:

背包中常见形式:

复制代码
dp[j] = max(dp[j], dp[j - w[i]] + v[i])

在 Word Break 里,可以做一个类比:

  • j 对应"前缀结束点";
  • w[i] 对应"某个单词的长度 len";
  • 没有价值 v[i],只关心"能否到达"。

逻辑上可以写成:

复制代码
对每个 j(1..n)
  对每个单词 word(长度 len):
    如果 j - len >= 0,dp[j-len] == true,并且 s[j-len..j-1] == word,
      则 dp[j] = dp[j] || dp[j - len];
      实际上就是 dp[j] = true。

和"max 写法"的对比:

  • 把 "max" 换成 "||"(逻辑或),表示"存在一种方案就行";
  • 把 "dp[j - w[i]] + v[i]" 换成了"dp[j-len] 且子串匹配成功"。

不过在实际写代码时,这种"模板形态"的形式不如直接写 if 更直观,所以最后代码通常还是:

如果 dp[i] 为 1 且 s[i..i+word_len-1] == word,就 dp[i + word_len] = 1

完整思路小结

把上面的内容压缩成一个"拿来就能用"的解题框架:

1. 定义状态

dp[i]前 i 个字符 s[0..i-1] 能否被字典单词完全拆分。

2. 初始化

  • dp[0] = true,表示空前缀已经是一个合法拆分;
  • 其他位置初始为 false

3. 状态转移

遍历 i 从 0 到 n-1

  • 如果 dp[i] == false,跳过(这个位置不能作为起点)。
  • 否则枚举字典中的每个单词 word
    • len = word.length(),若 i + len <= ns[i..i+len-1] == word
    • dp[i + len] = true

4. 答案

dp[n] 是否为 truens.length


用一句话概括:

把每个 dp[i] == true 的位置,看成图中的一个点,从这里出发,用字典单词当边,一条条跳到新的位置 i + len(word),最后看能不能跳到位置 n

你提交的那份 C 代码,正是完整实现了这一套逻辑,并在 LeetCode 上通过了所有测试,用时 0ms,说明理解已经非常扎实。leetcode


相关链接

相关推荐
宇钶宇夕21 小时前
CoDeSys入门实战一起学习(十三):函数(FUN)深度解析:自定义、属性与实操案例
运维·算法·自动化·软件工程
l1t21 小时前
对clickhouse给出的二分法求解Advent of Code 2025第10题 电子工厂 第二部分的算法理解
数据库·算法·clickhouse
Tisfy21 小时前
LeetCode 3315.构造最小位运算数组 II:位运算
算法·leetcode·题解·位运算
YuTaoShao21 小时前
【LeetCode 每日一题】1292. 元素和小于等于阈值的正方形的最大边长
算法·leetcode·职场和发展
Remember_99321 小时前
【数据结构】深入理解Map和Set:从搜索树到哈希表的完整解析
java·开发语言·数据结构·算法·leetcode·哈希算法·散列表
浅念-21 小时前
C++第一课
开发语言·c++·经验分享·笔记·学习·算法
charlie11451419121 小时前
现代嵌入式C++教程:对象池(Object Pool)模式
开发语言·c++·学习·算法·嵌入式·现代c++·工程实践
燃于AC之乐21 小时前
我的算法修炼之路--8——预处理、滑窗优化、前缀和哈希同余,线性dp,图+并查集与逆向图
算法·哈希算法·图论·滑动窗口·哈希表·线性dp
2501_930707781 天前
使用C#代码在 Word 文档页面中添加装订线
开发语言·c#·word
格林威1 天前
多相机重叠视场目标关联:解决ID跳变与重复计数的 8 个核心策略,附 OpenCV+Halcon 实战代码!
人工智能·数码相机·opencv·算法·计算机视觉·分类·工业相机