单词拆分(Word Break)题解 | 动态规划解法

单词拆分(Word Break)题解 | 动态规划解法

问题描述

给定一个非空字符串 s 和一个包含若干非空单词的字符串列表 wordDict(字典),判断是否可以将字符串 s 拆分为若干个字典中的单词(字典中的单词可重复使用,无需用完所有单词)。

核心特征分析

本题的核心是字符串拆分的可达性判断,核心特征可归纳为:

  1. 问题本质:判断整个字符串能否被划分为字典中单词的组合,属于"可达性"判定问题(而非求所有拆分方案);
  2. 重叠子问题:判断前 i 个字符能否拆分时,会重复用到前 jj < i)个字符的拆分结果,若暴力计算会重复求解子问题;
  3. 最优子结构:前 i 个字符的拆分结果,可由"前 j 个字符可拆分"且"ji 之间的子串在字典中"两个条件推导得出。

算法选择

可选算法对比

  • 暴力搜索/回溯:可枚举所有可能的拆分方式,但会重复计算大量子问题(如多次判断前 k 个字符能否拆分),时间复杂度呈指数级,效率极低;
  • 动态规划(DP):通过缓存子问题的解(DP数组)避免重复计算,仅需线性空间存储状态,时间复杂度可优化至多项式级别。

最终选择

优先选择动态规划,原因:题目仅需判断"能否拆分"而非"所有拆分方案",DP可通过状态缓存高效解决重叠子问题,是此类可达性问题的最优选择。

解题模式识别(动态规划适用场景)

当遇到以下特征的字符串问题时,可优先考虑动态规划:

  1. 问题目标是"判断能否完成某类组合/分割",而非求所有解或最优解;
  2. 子问题的解可推导原问题的解,且存在明确的最优子结构;
  3. 字符串拆分/分割类问题(如分割回文串、单词拆分等),通常存在重叠子问题。

解题思路

动态规划的核心是定义状态并推导转移方程,具体步骤如下:

  1. 预处理字典 :将 wordDict 转换为哈希集合(unordered_set),将子串查询的时间复杂度从 O(m)m 为字典长度)降至 O(1)
  2. 定义DP状态 :设 dp[i] 表示"字符串 s 的前 i 个字符(即 s[0...i-1])能否被拆分为字典中的单词";
  3. 初始化状态dp[0] = true(空字符串默认可拆分,作为递归/迭代的起始条件);
  4. 状态转移
    • 遍历字符串长度 i(从1到 s 的总长度 n),逐一判断前 i 个字符的可达性;
    • 对每个 i,遍历分割点 j(从0到 i-1),若满足两个条件:
      • dp[j] = true(前 j 个字符可拆分);
      • 子串 s[j...i-1](即 s.substr(j, i-j))存在于字典中;
    • 则说明前 i 个字符可拆分,令 dp[i] = true(找到一种可行方案即可,无需继续遍历 j);
  5. 返回结果 :最终 dp[n] 即为整个字符串 s 的拆分结果(ns 的长度)。

解题代码(带详细注释)

cpp 复制代码
class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        int n = s.size();
        // 1. 字典转哈希集合,优化查询效率
        unordered_set<string> wordSet(wordDict.begin(), wordDict.end());
        // 2. 定义dp数组:dp[i]表示前i个字符能否被拆分
        vector<bool> dp(n + 1, false);
        // 3. 初始状态:空字符串可拆分
        dp[0] = true;

        // 4. 遍历所有可能的字符串长度i(前i个字符)
        for (int i = 1; i <= n; ++i) {
            // 遍历所有可能的分割点j
            for (int j = 0; j < i; ++j) {
                // 核心条件:前j个字符可拆分 + j到i的子串在字典中
                if (dp[j] && wordSet.count(s.substr(j, i - j))) {
                    dp[i] = true;
                    break; // 找到一种方案即可,无需继续遍历j
                }
            }
        }

        // 5. 返回整个字符串的拆分结果
        return dp[n];
    }
};

复杂度分析

时间复杂度

O(n²),其中 n 是字符串 s 的长度:

  • 外层循环遍历 i(1到 n),共 n 次;
  • 内层循环遍历 j(0到 i-1),最坏情况下总遍历次数为 1+2+...+n = n(n+1)/2,近似为 O(n²)
  • 哈希集合的查询操作是 O(1),子串截取 s.substr(j, i-j) 的时间复杂度为 O(i-j),但由于字典中单词长度通常远小于 n,实际时间复杂度接近 O(n²)

空间复杂度

O(n)

  • 主要开销为 dp 数组(长度 n+1),哈希集合的空间取决于字典大小(题目未要求优化字典空间,属于输入相关开销)。

总结

  1. 本题核心是利用动态规划解决字符串拆分的可达性问题 ,通过 dp[i] 缓存前 i 个字符的拆分状态,避免重复计算;
  2. 关键优化点:将字典转为哈希集合,将子串查询效率从 O(m) 降至 O(1)
  3. 状态转移的核心逻辑:dp[i] = dp[j] && (s[j:i] ∈ 字典),找到任意一个可行的 j 即可确定 dp[i] = true
相关推荐
不懒不懒19 小时前
【逻辑回归从原理到实战:正则化、参数调优与过拟合处理】
人工智能·算法·机器学习
一只大袋鼠19 小时前
分布式 ID 生成:雪花算法原理、实现与 MyBatis-Plus 实战
分布式·算法·mybatis
tobias.b19 小时前
408真题解析-2010-27-操作系统-同步互斥/Peterson算法
算法·计算机考研·408真题解析
寄存器漫游者19 小时前
数据结构 二叉树核心概念与特性
数据结构·算法
m0_7066532319 小时前
跨语言调用C++接口
开发语言·c++·算法
皮皮哎哟19 小时前
数据结构:从队列到二叉树基础解析
c语言·数据结构·算法·二叉树·队列
一匹电信狗19 小时前
【高阶数据结构】并查集
c语言·数据结构·c++·算法·leetcode·排序算法·visual studio
愚者游世19 小时前
list Initialization各版本异同
开发语言·c++·学习·程序人生·算法
.小墨迹19 小时前
apollo中车辆的减速绕行,和加速超车实现
c++·学习·算法·ubuntu·机器学习
超级大只老咪19 小时前
DFS算法(回溯搜索)
算法