leetcode 139单词拆分

一、完整代码(精简注释)

复制代码
class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        // 哈希集合存储字典中的单词
        unordered_set<string> words;
        for(auto x : wordDict){
            words.insert(x);
        }

        // dp[i]表示前i个字符能否被成功拆分
        vector<bool> dp(s.size()+1, false);
        dp[0] = true; // 空字符串可以拆分

        for(int i=1; i<=s.size(); i++){
            for(int j=0; j<i; j++){
                // 前半部分可拆分,且后半部分在字典中
                if(dp[j]==true && words.count(s.substr(j,i-j))==true){
                    dp[i] = true;
                    break; // 找到合法拆分,退出内层循环
                }
            }
        }

        return dp[s.size()]; // 整个字符串能否拆分
    }
};

二、动态规划五步总结

1. 确定 dp 数组含义

复制代码
vector<bool> dp(s.size()+1, false);

dp[i] 表示字符串 s 的前 i 个字符能否由字典中的单词拼接得到。

例如:

复制代码
s = "leetcode";
wordDict = {"leet", "code"};
状态 对应字符串 结果
dp[0] "" true
dp[1] "l" false
dp[2] "le" false
dp[3] "lee" false
dp[4] "leet" true
dp[5] "leetc" false
dp[8] "leetcode" true

最终目标是求 dp[s.size()]。

2. 确定状态转移方程(核心)

复制代码
if(dp[j]==true && words.count(s.substr(j,i-j))==true){
    dp[i] = true;
    break;
}

核心思想:将前 i 个字符在位置 j 处分成两部分。

例如:i = 8,j = 4

l

0

e

1

e

2

t

3

c

4

o

5

d

6

e

7

前半部分:leet

dp[j]

判断是否已经可以拆分

后半部分:code

s.substr(j,i-j)

判断是否在字典中

条件一:

复制代码
dp[j] == true

表示前 j 个字符已经可以由字典中的单词拼接得到。

条件二:

复制代码
words.count(s.substr(j,i-j)) == true

表示从下标 j 到 i-1 的字符串是字典中的一个完整单词。

注意:

复制代码
s.substr(j,i-j)
  • j:截取的起始下标。

  • i-j:截取的字符数量。

  • 截取范围:[j,i)。

两个条件同时满足,就有:

复制代码
dp[i] = true;

因为只需要找到一种合法拆分方式,所以可以直接 break,跳出内层 j 循环。

3. 初始化 dp 数组

复制代码
vector<bool> dp(s.size()+1, false);
dp[0] = true;
  • s.size()+1:存储从 dp[0] 到 dp[s.size()] 的状态。

  • false:默认当前前缀不能成功拆分。

  • dp[0]=true:空字符串可以看作由零个单词拼接得到。

为什么 dp[0] 必须是 true?

假设:

复制代码
s = "leetcode";

当 i=4,j=0 时:

复制代码
dp[0] == true
s.substr(0,4) == "leet"

由于 "leet" 在字典中,所以:

复制代码
dp[4] = true;

如果 dp[0]=false,就无法通过这个转移识别第一个完整单词。

4. 确定遍历顺序

复制代码
for(int i=1; i<=s.size(); i++){
    for(int j=0; j<i; j++){
        if(dp[j]==true && words.count(s.substr(j,i-j))==true){
            dp[i] = true;
            break;
        }
    }
}

外层循环 i:枚举当前需要判断的字符串前缀长度。

例如:

复制代码
i=1 → "l"
i=2 → "le"
i=3 → "lee"
i=4 → "leet"
...
i=8 → "leetcode"

内层循环 j:枚举当前前缀的分割位置。

例如,当 i=8 时:

j 前半部分 后半部分 s.substr(j,i-j)
0 "" "leetcode"
1 "l" "eetcode"
2 "le" "etcode"
3 "lee" "tcode"
4 "leet" "code"
5 "leetc" "ode"
6 "leetco" "de"
7 "leetcod" "e"

当 j=4 时:

复制代码
dp[4] == true
words.count("code") == 1

因此:

复制代码
dp[8] = true;
break;

这时退出内层循环,继续执行下一个外层 i;如果 i 已经是 s.size(),则循环结束。

5. 确定返回值

复制代码
return dp[s.size()];
  • true:整个字符串可以由字典中的单词拼接得到。

  • false:不存在合法的完整拆分方案。

三、复杂度分析

设 n 为字符串长度。

  • 时间复杂度: 最坏 \(O(n^3)\)。双层循环枚举 i 和 j,substr() 构造子串及哈希查找还可能花费 \(O(n)\)。

  • 空间复杂度: DP 数组 \(O(n)\),临时子串最多 \(O(n)\),此外还需要存储字典的哈希集合。

四、核心思路总结

这道题属于布尔型动态规划,本质是判断是否存在一种合法的字符串拆分方案。

整个过程可以理解为:

  1. 使用 unordered_set 存储字典中的单词,方便快速查找。

  2. 定义 dp[i],表示前 i 个字符能否成功拆分。

  3. 枚举当前前缀长度 i,再枚举分割位置 j。

  4. 如果 dp[j] 为 true,并且 [j,i) 这一段字符串在字典中,那么 dp[i]=true。

  5. 找到一种合法方案后,直接退出内层循环。

  6. 最后返回 dp[s.size()]。

最重要的状态转移逻辑:

\ \\boxed{ dp\[j=true\ \land\ sj:i\in wordDict \ \Longrightarrow\ dpi=true } \]

记住一句话:

i 决定当前判断前多少个字符,j 决定从哪里切开;dp[j] 判断左边,substr(j,i-j) 判断右边,两边都合法,dp[i] 就为 true。

相关推荐
Starry-sky(jing)3 小时前
BUG: unable to handle kernel paging request 完整排查:dmesg 四要素与三路定罪
linux·运维·服务器·内核·排障
mounter6253 小时前
从硬件互连到操作系统变革:CXL 技术演进与 Linux 内核工程挑战
linux·运维·服务器
zhengqweasd4 小时前
机房托管服务器怎么选硬盘,机械盘和 SSD 托管场景差异
运维·服务器·github
xh didida5 小时前
Linux -- 基础IO
linux·服务器·开发语言·c++
心易行者5 小时前
Agent应用+API端点商业化进阶实战:从单体智能体到可付费调用的API全流程
运维·服务器·人工智能·python·apache
科技研学社6 小时前
精度决胜品质:羽绒服缝制工艺标准与自动化精度对标解析
运维·自动化
夜之眷属6 小时前
Core dump 崩溃排查:JVM 宕机后,那份 core 文件怎么用 gdb 还原现场
java·运维·服务器·jvm
Initialize293086 小时前
Redis 内存打满、连接数飙红?运维排查四步法
运维