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等算法思想。

相关推荐
汀、人工智能10 小时前
[特殊字符] 第21课:最长有效括号
数据结构·算法·数据库架构·图论·bfs·最长有效括号
Boop_wu11 小时前
[Java 算法] 字符串
linux·运维·服务器·数据结构·算法·leetcode
故事和你9111 小时前
洛谷-算法1-2-排序2
开发语言·数据结构·c++·算法·动态规划·图论
Fcy64811 小时前
算法基础详解(三)前缀和与差分算法
算法·前缀和·差分
kvo7f2JTy12 小时前
基于机器学习算法的web入侵检测系统设计与实现
前端·算法·机器学习
List<String> error_P12 小时前
蓝桥杯最后几天冲刺:暴力大法(一)
算法·职场和发展·蓝桥杯
迈巴赫车主13 小时前
蓝桥杯3500阶乘求和java
java·开发语言·数据结构·职场和发展·蓝桥杯
流云鹤13 小时前
Codeforces Round 1090 (Div. 4)
c++·算法
wljy113 小时前
第十三届蓝桥杯大赛软件赛省赛C/C++ 大学 B 组(个人见解,已完结)
c语言·c++·算法·蓝桥杯·stl
清空mega14 小时前
C++中关于数学的一些语法回忆(2)
开发语言·c++·算法