LeetCode Hot100(69/100)—— 139. 单词拆分

文章目录

题目链接

https://leetcode.cn/problems/word-break/description/

题目说明

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s

注意: 不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。

示例

示例 1:

复制代码
输入: s = "leetcode", wordDict = ["leet","code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。

示例 2:

复制代码
输入: s = "applepenapple", wordDict = ["apple","pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。
注意,你可以重复使用字典中的单词。

示例 3:

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

约束条件

  • 1 <= s.length <= 300
  • 1 <= wordDict.length <= 1000
  • 1 <= wordDict[i].length <= 20
  • s 和 wordDict[i] 仅由小写英文字母组成
  • wordDict 中的所有字符串互不相同

解题思路概览

单词拆分问题
方法1: 动态规划
方法2: 记忆化搜索
方法3: BFS广度优先搜索
自底向上构建
状态转移方程
自顶向下递归
缓存中间结果
队列存储位置
层次遍历

方法一:动态规划

原理讲解

动态规划是解决此题的最优方法。核心思想是:如果字符串的前 i 个字符可以被拆分,且从位置 ij 的子串在字典中,那么前 j 个字符也可以被拆分。

定义状态:

  • dp[i] 表示字符串 s 的前 i 个字符是否可以被拆分成字典中的单词

状态转移方程:

复制代码
dp[i] = dp[j] && wordDict.contains(s.substring(j, i))
其中 0 <= j < i

执行流程





开始
初始化dp数组, dp0=true
遍历i从1到n
遍历j从0到i-1
dp[j]==true?
s[j:i]在字典中?
dp[i]=true, break
返回dp[n]

Java代码实现

java 复制代码
class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        // 将字典转换为HashSet,提高查找效率
        Set<String> wordSet = new HashSet<>(wordDict);
        int n = s.length();
        
        // dp[i]表示s的前i个字符是否可以被拆分
        boolean[] dp = new boolean[n + 1];
        dp[0] = true; // 空字符串可以被拆分
        
        // 遍历字符串的每个位置
        for (int i = 1; i <= n; i++) {
            // 尝试所有可能的拆分点
            for (int j = 0; j < i; j++) {
                // 如果前j个字符可以拆分,且j到i的子串在字典中
                if (dp[j] && wordSet.contains(s.substring(j, i))) {
                    dp[i] = true;
                    break; // 找到一种拆分方式即可
                }
            }
        }
        
        return dp[n];
    }
}

复杂度分析

  • 时间复杂度:O(n² × m),其中 n 是字符串长度,m 是字典中最长单词的长度(substring操作)
  • 空间复杂度:O(n),dp数组的空间

方法二:记忆化搜索(DFS + 缓存)

原理讲解

记忆化搜索采用自顶向下的递归思路,从字符串的起始位置开始,尝试匹配字典中的每个单词。为了避免重复计算,使用一个缓存数组记录每个位置的搜索结果。

执行流程

字典 缓存数组 DFS递归 主函数 字典 缓存数组 DFS递归 主函数 dfs(0) 检查memo[0] 未计算 遍历字典单词 匹配"leet" dfs(4) 检查memo[4] 未计算 匹配"code" 到达末尾 memo[4]=true memo[0]=true 返回true

Java代码实现

java 复制代码
class Solution {
    private Boolean[] memo;
    private Set<String> wordSet;
    private String s;
    
    public boolean wordBreak(String s, List<String> wordDict) {
        this.s = s;
        this.wordSet = new HashSet<>(wordDict);
        this.memo = new Boolean[s.length()];
        return dfs(0);
    }
    
    private boolean dfs(int start) {
        // 到达字符串末尾,说明可以完全拆分
        if (start == s.length()) {
            return true;
        }
        
        // 如果已经计算过,直接返回缓存结果
        if (memo[start] != null) {
            return memo[start];
        }
        
        // 尝试从start位置开始匹配字典中的每个单词
        for (String word : wordSet) {
            int end = start + word.length();
            // 检查是否越界以及是否匹配
            if (end <= s.length() && s.substring(start, end).equals(word)) {
                // 递归检查剩余部分
                if (dfs(end)) {
                    memo[start] = true;
                    return true;
                }
            }
        }
        
        // 所有尝试都失败
        memo[start] = false;
        return false;
    }
}

复杂度分析

  • 时间复杂度:O(n × k × m),其中 n 是字符串长度,k 是字典大小,m 是平均单词长度
  • 空间复杂度:O(n),递归栈和缓存数组的空间

方法三:BFS广度优先搜索

原理讲解

将问题看作图的遍历问题,每个位置是一个节点,如果从位置 i 到位置 j 的子串在字典中,则存在一条从 i 到 j 的边。使用BFS从位置0开始,看能否到达位置n。

执行流程

leet
code
le
et
code
位置0
位置4
位置8/结束
位置2
位置4

Java代码实现

java 复制代码
class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        Set<String> wordSet = new HashSet<>(wordDict);
        Queue<Integer> queue = new LinkedList<>();
        boolean[] visited = new boolean[s.length()];
        
        queue.offer(0);
        
        while (!queue.isEmpty()) {
            int start = queue.poll();
            
            // 如果这个位置已经访问过,跳过
            if (visited[start]) {
                continue;
            }
            visited[start] = true;
            
            // 尝试从start位置开始的所有可能单词
            for (int end = start + 1; end <= s.length(); end++) {
                if (wordSet.contains(s.substring(start, end))) {
                    // 如果到达末尾,返回true
                    if (end == s.length()) {
                        return true;
                    }
                    // 将新位置加入队列
                    queue.offer(end);
                }
            }
        }
        
        return false;
    }
}

复杂度分析

  • 时间复杂度:O(n² × m),最坏情况下需要访问所有位置,每个位置需要检查所有可能的子串
  • 空间复杂度:O(n),队列和visited数组的空间

优化技巧

动态规划优化版本

可以通过记录字典中最长单词的长度来减少内层循环次数:

java 复制代码
class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        Set<String> wordSet = new HashSet<>(wordDict);
        int n = s.length();
        
        // 找出字典中最长单词的长度
        int maxLen = 0;
        for (String word : wordDict) {
            maxLen = Math.max(maxLen, word.length());
        }
        
        boolean[] dp = new boolean[n + 1];
        dp[0] = true;
        
        for (int i = 1; i <= n; i++) {
            // 只需要检查最长单词长度范围内的拆分点
            for (int j = Math.max(0, i - maxLen); j < i; j++) {
                if (dp[j] && wordSet.contains(s.substring(j, i))) {
                    dp[i] = true;
                    break;
                }
            }
        }
        
        return dp[n];
    }
}

方法对比分析

方法 时间复杂度 空间复杂度 实现难度 性能表现 适用场景
动态规划 O(n² × m) O(n) 中等 最优 通用场景,推荐使用
动态规划优化 O(n × maxLen × m) O(n) 中等 优秀 字典单词长度差异大时效果显著
记忆化搜索 O(n × k × m) O(n) 中等 良好 字典较小时表现好
BFS O(n² × m) O(n) 简单 一般 理解图论思想的教学场景

性能对比说明

  1. 动态规划方法是最稳定和推荐的解法,代码简洁,性能优秀
  2. 优化后的动态规划在字典单词长度较短时能显著提升性能
  3. 记忆化搜索在字典规模较小时表现良好,但递归调用有额外开销
  4. BFS方法直观易懂,但在实际性能上略逊于动态规划

选择建议

  • 面试场景:优先使用动态规划,代码简洁且易于解释
  • 性能要求高:使用优化版动态规划
  • 学习理解:可以从BFS开始,逐步过渡到动态规划
  • 字典特别小:记忆化搜索也是不错的选择

单词拆分问题是一道经典的动态规划题目,核心在于理解状态定义和转移方程。通过将大问题分解为子问题,利用已计算的结果来避免重复计算,可以高效地解决这个问题。掌握这道题的多种解法,有助于深入理解动态规划、记忆化搜索和BFS等算法思想。

相关推荐
故以往之不谏2 小时前
快慢双指针算法--数组删除目标元素--LeetCode27
开发语言·数据结构·c++·算法·leetcode·学习方法·数组
历程里程碑2 小时前
Linux 38 网络协议:从独立主机到全球互通
java·linux·运维·服务器·网络·c++·职场和发展
AI科技星2 小时前
空间光速螺旋动力学:统一质量、引力、电磁与时空本源的公理化理论与全现象验证
c语言·开发语言·opencv·算法·r语言
zhengzhengwang2 小时前
chrome v8 内存管理机制
jvm·chrome·算法
im_AMBER2 小时前
Leetcode 140 括号生成 | 单词搜索
算法·leetcode
njsgcs2 小时前
空间中最后一条折弯线垂直于第一条折弯线
算法
qq_404265832 小时前
C++中的代理模式实战
开发语言·c++·算法
玛卡巴卡ldf2 小时前
【LeetCode 手撕算法】(滑动窗口) 3-无重复字符的最长子串、438-找到字符串中所有字母异位词
数据结构·算法·leetcode·哈希算法
liuyao_xianhui2 小时前
动态规划_最大子数组和_C++
java·开发语言·数据结构·c++·算法·链表·动态规划