The Word Break problem requires determining if a string can be segmented into
space-separated words from a given dictionary. Here are two approaches:
Approach 1: Dynamic Programming - First, we create a
states array
where
states[i]
indicates if
substring
s[0...i-1]
is
breakable or not. At each position, we
check all dictionary words
that could end there. If we find a word that matches and connects to a previous breakable state, we
update the state
accordingly. Finally,
states[len]
indicates
whether a string of length
'len'
is breakable or
not.
Approach 2: Trie with Memoization - Here, we build a
Trie
from the dictionary
words for efficient
prefix searches
. Further,
we perform
DFS (with memoization)
&
store results
of
subproblems in a
map
to
avoid
recomputation. After each valid word
match,
recursively check
if the
remaining substring can be broken or not.
class Solution { public: bool wordBreak(string s, vector<string>& wordDict) { int len = s.size(); // states[i] tell whether string till ith index is breakable or not. vector<bool> states(len, false); states[0] = true; for (int i = 1; i <= len; i++) { for (int j = 0; j < wordDict.size(); j++) { string str = wordDict[j]; int start = i - str.size(); if (start >= 0 && states[start] == true && s.substr(start, str.size()) == str) { states[i] = true; } } } return states[len]; } };
class Solution { public: struct TrieNode { TrieNode* children[26]; bool isLastCharacter; TrieNode() { isLastCharacter = false; for (int i = 0; i < 26; i++) children[i] = NULL; } }; // Memorizing the solution to avoid calculating the overlapping sub // problems. It will tell whether a substring starting from ith character is // breakable or not. unordered_map<int, bool> mp; bool wordBreak(string s, vector<string>& wordDict) { TrieNode* root = new TrieNode(); // Store all words of dictionary in the Trie for (int i = 0; i < wordDict.size(); i++) { string str = wordDict[i]; insert(root, str); } bool isBreakable = search(root, s, 0); return isBreakable; } void insert(TrieNode* root, string str) { TrieNode* copy = root; for (int i = 0; i < str.size(); i++) { char ch = str[i]; if (copy->children[ch - 'a'] == NULL) copy->children[ch - 'a'] = new TrieNode(); copy = copy->children[ch - 'a']; } copy->isLastCharacter = true; } bool search(TrieNode* root, string str, int idx) { if (idx == str.size()) return true; if (mp.contains(idx)) return mp[idx]; TrieNode* copy = root; for (int i = idx; i < str.size(); i++) { char ch = str[i]; if (copy->children[ch - 'a'] == NULL) { mp.insert({i, false}); return false; } copy = copy->children[ch - 'a']; if (copy->isLastCharacter) { // Now, once we memorize this sub problem, it will be used in // future iterations when search(root,str,i+1)--> false. if (search(root, str, i + 1)) { mp.insert({i, true}); return true; } } } mp.insert({idx,false}); return false; } };
DP approach has nested loops O(n2) with substring operations O(n). Trie approach reduces to O(n2) with memoization.
DP uses O(n)
space, Trie uses additional space for dictionary storage where 'n'
is string
length,
'm'
is
dictionary size, and 'k'
is average word
length.