给定字符串 s 和字符串字典 wordDict,判断 s 是否可以被拆分为一个或多个字典中的单词。
特点:
-
字典单词 可以重复使用
-
不要求使用所有字典单词
方法一:暴力 DFS(递归)
思路
从字符串开头开始:
-
枚举前缀
-
如果前缀在字典中
-
递归判断剩余部分
例如:
applepenapple
apple | penapple
pen | apple
如果能拆分到最后返回 true。
C 代码
#include <stdbool.h>
#include <string.h>
bool inDict(char* s, int start, int len, char** wordDict, int wordDictSize)
{
for(int i = 0; i < wordDictSize; i++)
{
if(strlen(wordDict[i]) == len &&
strncmp(s + start, wordDict[i], len) == 0)
return true;
}
return false;
}
bool dfs(char* s, int start, int len, char** wordDict, int wordDictSize)
{
if(start == len) return true;
for(int i = start + 1; i <= len; i++)
{
if(inDict(s, start, i - start, wordDict, wordDictSize) &&
dfs(s, i, len, wordDict, wordDictSize))
return true;
}
return false;
}
bool wordBreak(char* s, char** wordDict, int wordDictSize)
{
return dfs(s, 0, strlen(s), wordDict, wordDictSize);
}
复杂度
时间复杂度:O(2^n)
空间复杂度:O(n)
因为大量重复计算,容易 超时。
方法二:DFS + 记忆化搜索(剪枝)
思路
暴力 DFS 的问题:
重复计算同一个子问题
例如:
applepenapple
apple
我们可以记录:
start 位置是否可以拆分
用数组 memo[] 记录状态:
0 = 未计算
1 = 可以拆分
-1 = 不可以拆分
C 代码
#include <stdbool.h>
#include <string.h>
int memo[305];
bool inDict(char* s, int start, int len, char** wordDict, int wordDictSize)
{
for(int i = 0; i < wordDictSize; i++)
{
if(strlen(wordDict[i]) == len &&
strncmp(s + start, wordDict[i], len) == 0)
return true;
}
return false;
}
bool dfs(char* s, int start, int len, char** wordDict, int wordDictSize)
{
if(start == len) return true;
if(memo[start] != 0)
return memo[start] == 1;
for(int i = start + 1; i <= len; i++)
{
if(inDict(s, start, i - start, wordDict, wordDictSize) &&
dfs(s, i, len, wordDict, wordDictSize))
{
memo[start] = 1;
return true;
}
}
memo[start] = -1;
return false;
}
bool wordBreak(char* s, char** wordDict, int wordDictSize)
{
memset(memo, 0, sizeof(memo));
return dfs(s, 0, strlen(s), wordDict, wordDictSize);
}
复杂度
时间复杂度:O(n² * m)
空间复杂度:O(n)
方法三:动态规划(最经典)
这是 LeetCode 官方最推荐方法。
DP 思路
定义:
dp[i] 表示 s[0..i-1] 是否可以拆分
初始化:
dp[0] = true
状态转移:
dp[i] = dp[j] && s[j..i-1] 在字典中
其中:
0 ≤ j < i
C 代码
#include <stdbool.h>
#include <string.h>
bool inDict(char* s, int start, int len, char** wordDict, int wordDictSize)
{
for(int i = 0; i < wordDictSize; i++)
{
if(strlen(wordDict[i]) == len &&
strncmp(s + start, wordDict[i], len) == 0)
return true;
}
return false;
}
bool wordBreak(char* s, char** wordDict, int wordDictSize)
{
int n = strlen(s);
bool dp[n + 1];
memset(dp, false, sizeof(dp));
dp[0] = true;
for(int i = 1; i <= n; i++)
{
for(int j = 0; j < i; j++)
{
if(dp[j] && inDict(s, j, i - j, wordDict, wordDictSize))
{
dp[i] = true;
break;
}
}
}
return dp[n];
}
复杂度
时间复杂度:O(n² * m)
空间复杂度:O(n)
方法四:DP + 哈希优化(最优解)
优化点:
1️⃣ 使用 HashSet 加速查找
2️⃣ 限制单词最大长度
题目给出:
wordDict[i].length ≤ 20
因此:
j >= i - 20
减少无效判断。
优化版 C 代码
#include <stdbool.h>
#include <string.h>
bool inDict(char* s, int start, int len, char** wordDict, int wordDictSize)
{
for(int i = 0; i < wordDictSize; i++)
{
if(strlen(wordDict[i]) == len &&
strncmp(s + start, wordDict[i], len) == 0)
return true;
}
return false;
}
bool wordBreak(char* s, char** wordDict, int wordDictSize)
{
int n = strlen(s);
bool dp[n + 1];
memset(dp, false, sizeof(dp));
dp[0] = true;
for(int i = 1; i <= n; i++)
{
for(int j = i - 20; j < i; j++)
{
if(j >= 0 && dp[j] &&
inDict(s, j, i - j, wordDict, wordDictSize))
{
dp[i] = true;
break;
}
}
}
return dp[n];
}
DP过程图解
示例:
s = "leetcode"
dict = ["leet","code"]
索引:
0 1 2 3 4 5 6 7 8
l e e t c o d e
DP数组:
T F F F T F F F T
解释:
dp[4] = true (leet)
dp[8] = true (code)
面试总结
最推荐回答顺序:
1 暴力DFS
2 DFS + 记忆化
3 动态规划(重点)
4 DP优化
面试官最希望听到:
DP 状态定义 + 状态转移