LeetCode 139 单词拆分

2,152 阅读4分钟

1. 问题描述

给定一个非空字符串 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

注意你可以重复使用字典中的单词。

2. 递归法解题思路

这一题稍一分析感觉不算太难,只需要从头开始,拿出每一个字典里的单词进行匹配,当前不匹配就切换下一个单词。匹配的话就把原串中匹配的部分去除,剩下的字符串递归地调用函数进行匹配即可。直到整个字符串全部都匹配就返回true。
于是就有了以下的代码:

class Solution {
public:
	bool wordBreak(string s, vector<string>& wordDict) {
		int len1 = s.length();
		bool result = false;
		for (auto str : wordDict) {
			int len2 = str.length();
			if (len2 > len1)
				continue;
			bool flag = true;
			for (int i = 0; i < len2; i++) {
				if (s[i] != str[i]) {
					flag = false;
					break;
				}
			}
			if (flag == false)
				continue;
			if (len1 == len2)
				return true;
			string sSon = s.substr(len2, len1 - len2);
			if (wordBreak(sSon, wordDict) == true) {
				result = true;
				break;
			}
		}
		return result;
	}
};

但是这种方法提交后有些输入的情况下会超时,leetcode上提醒超时的用例为:
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab" ["a","aa","aaa","aaaa","aaaaa","aaaaaa","aaaaaaa","aaaaaaaa","aaaaaaaaa","aaaaaaaaaa"]

很显然这个例子的递归深度会很深,同时字典中每个单词在每个循环中都会递归地调用函数,并且适配只出现在递归到最末尾。显然复杂度是很高的。

3. 动态规划解题思路

既然可以用递归来解决问题,说明是存在相同的子问题的,那么这个问题就是有可能用动态规划来解决的。动态规划通过将子问题的结果记录下来,可以降低递归法的复杂度。就类似于fabnacci数列存下前两项的值,而不用每次通过递归去求前两项的值。
由于输入是一个字符串,所有用dp来解属于1维dp问题。举个简单的例子:

输入:“catsandog”,wordDict = ["cats", "dog", "sand", "and", "cat"]
输出:“false”

一维dp首先申请一个一维dp数组。初始化最简单的情况显然是dp[0] = 0,索引0表示为空串,里面有0个字符。dp[1],则表示字符串只有第一个字符“c”,在字典中查不到,所以dp[1] = 0。dp[2]则表示子串“ca”也不能在字典中找到,dp[2] = 0。dp[3]为“cat”,所以dp[3] = 1。依次类推,当计算到dp[4]的时候,需要判断两种情况:(1)子串“cats”能否在字典中找到,如果能找到则dp[4] = 1(2)因为dp[3] = 1,子串“s”如果能在字典中找到,那么dp[4] = 1。依次往后:
dp[0] = 0;
dp[1] = 0;
dp[2] = 0;
dp[3] = 1;  //查找到cat
dp[4] = 1;  //查找到cats
dp[5] = 0;  //查不到catsa,且查找不到sa和a
dp[6] = 0;  //查不到catsan,san,an
dp[7] = 1;  //dp[3] = 1查到sand或dp[4] = 1查找到and
dp[8] = 0;
dp[9] = 0;
最后返回dp[9],即该字符串无法找到。
在这个查找过程中,最好记录下来字典中所有可使用的单词的长度,一旦dp的第i项长度超过了最大长度,其本身就不能在字典中找到,只能是查找其往前一个单词长度的dp[i - len]处是否是1,如果满足且从i-len到i - 1位置的子串能在字典中找到,那么dp[i]就能被拆分。如果不记录单词的长度的话,就只能盲目找到之前所有dp为1的项,浪费资源。
最终代码如下:

class Solution {
public:
	bool wordBreak(string s, vector<string>& wordDict) {
		int length = s.length();
		vector<bool> dp(length + 1, 0);
		//set用来存字典中所有word的长度
		int maxLen = 0;
		set<int> storeLen;
		for (auto word : wordDict) {
			int len = word.length();
			if (len > maxLen)
				maxLen = len;
			storeLen.insert(len);
		}
		//初始化
		dp[0] = 0;
		for (int i = 1; i <= length; i++) {
			//只有i的长度比maxLen小才需要判断从0到i的字符串是否能找到
			if (i <= maxLen) {
				string tmp = s.substr(0, i);
				for (auto word : wordDict) {
					if (tmp == word) {
						dp[i] = 1;
						break;
					}
				}
			}
			//如果已经找到就可以计算dp的下一项
			if (dp[i] == 1)
				continue;
			//否则就查找往前一个单词长度处的dp是否为1,为1且这个单词匹配则前i个字符匹配
			for (auto len : storeLen) {
				if (i - len <= 0)
					continue;
				if (dp[i - len] == 1) {
					string tmp = s.substr(i - len, len);
					for (auto word : wordDict) {
						if (tmp == word && dp[i - len]) {
							dp[i] = 1;
							break;
						}
					}
					if (dp[i] == 1)
						break;
				}
			}
		}
		return dp[length];
	}
};