由于dict中可能存在多个前缀相同的单词,也就使得s可能有多种拆分方案,比如例题中的“nowcoderis”可以被拆分为"now coder is"和“now coderis"。如何才能计算出所有方案?
很直观的想法就是记录下每个分割点的位置,最后利用这些分割点的信息还原成一个语句即可。例如,“nowcoderis”有两个分割点,一个是分割点是'i',另外一个是'c',这两个分割点的索引分别为8,3,记录下这两个分割点,然后自底向上还原。(为什么是自底向上?因为在记录分割点的时候是自上而下的,每个位置记录的都是它之前的分割点,所以只能自底向上才能还原完整的索引信息)
具体的,创建一个二维数组dp,数组dp[i][j]表示如果s[0,i-1]可以被分割,分割的位置,这个位置可能有多个,也可能没有。然后利用dp数组中记录的信息计算最终结果,这个过程是自底向上的。
上面解释可能比较难懂,对例题来说,首先计算得到dp数组(省略了空行):
dp[0] = {0}(这种情况对应字符串为空)
dp[3] = {0}("now"可以拆分一个"now"出来)
dp[8] = {0}("nowcoder"可以拆分一个"nowcoder"出来)
dp[10] = {3, 8} (s[3] == 'c', s[8] == 'i',“nowcoderis”可以拆分一个"is"出来,或者拆分一个"coderis")
dp[14] = {10} (s[10] == 'b',"nowcoderisbest"可以拆分一个"best"出来)
然后从dp[14]开始(自底向上)还原语句,可以得到两条完整拆分信息,14->10->3->0,14->10->8->0,这个过程向上回溯即可。
时间复杂度O(n^2)
vector<string> res; string path; void dfs(string& s, vector<vector<int>>& dp, int i) { if(i == 0) { path.pop_back(); //去掉末尾的空格 res.push_back(path); return; } for(int j=0; j<dp[i].size(); ++j) { string temp = path; path = s.substr(dp[i][j], i-dp[i][j]) + " " + path; //由于是自底向上还原,这里的顺序需要调整 dfs(s, dp, dp[i][j]); path = temp; } } vector<string> wordBreak(string s, unordered_set<string> &dict) { int len = s.size(); vector<vector<int>> dp(len+1); dp[0].push_back(0); for(int i=1; i<=len; ++i) { for(int j=0; j<i; ++j) { //dp[j].size()>0表示s[0,j-1]可以被拆分,dict.count(s.substr(j,i-j)表示dict中有s[j,i-1]这个单词 //也就是说如果s[0,i-1]可以被拆分为s[0,j-1], s[j,i-1] if(dp[j].size()>0 && dict.count(s.substr(j,i-j))) { dp[i].push_back(j); } } } dfs(s, dp, len); return res; }