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 boolean wordBreak(String str, List<String> wordDict) { int len = str.length(); // states[i] tell whether string till ith index is breakable or not. boolean[] states = new boolean[len+1]; states[0] = true; for(int i=1;i<=len;i++){ for(int j=0;j<wordDict.size();j++){ String s1 = wordDict.get(j); int len1 = s1.length(); int start = i-len1; // Starting position of the substring. if(start>=0){ String s2 = str.substring(start,start+len1); // Condition: Previous part of string till length = start is breakable // & current substring i.e. s2 is contained in the dictionary. if(states[start]==true && s1.equals(s2)) states[start+len1]=true; } } } return states[len]; } }
// Basic Trie Node class TrieNode{ TrieNode[] children; boolean isEndOfWord; TrieNode(){ children = new TrieNode[26]; isEndOfWord = false; } } class Solution { // Memorize the solution // It will tell whether a substring starting from ith character is breakable or not. Map<Integer,Boolean> memo = new HashMap<>(); public void insert(TrieNode root, String str ){ // Simply insert the string into Trie TrieNode copy = root; for(int i=0;i<str.length();i++){ char ch = str.charAt(i); if(copy.children[ch-'a']==null) copy.children[ch-'a'] = new TrieNode(); copy = copy.children[ch-'a']; } copy.isEndOfWord = true; } public boolean search(TrieNode root, String str, int start){ if(start == str.length()) return true; if(memo.containsKey(start)) return memo.get(start); TrieNode copy = root; for(int i=start;i<str.length();i++){ char ch = str.charAt(i); if(copy.children[ch-'a']==null){ memo.put(start,false); return false; } copy = copy.children[ch-'a']; if(copy.isEndOfWord == true){ // If search comes true then return true else continue iterating // the string. if(search(root,str,i+1)){ memo.put(start,true); return true; } } } // Consider a case when Trie is sequence of 10 a's and our string is "aaa" memo.put(start,false); return false; } public boolean wordBreak(String s, List<String> wordDict) { TrieNode root = new TrieNode(); for(int i=0;i<wordDict.size();i++){ String str = wordDict.get(i); insert(root,str); } boolean isBreakable = search(root,s,0); return isBreakable; } }
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.