Given a string s and a dictionary of strings wordDict, return true if s can be segmented into a space-separated sequence of one or more dictionary words. note that the same word in the dictionary may be reused multiple times in the segmentation.

Input: s = "codecs", wordDict = ["cs","code"]
Output: true
Explanation: "codecs" can be made using the words from dictionary.
Input: s = "applepenapple", wordDict = ["apple","pen"]
Output: true
Explanation: "applepenapple" can be made using "apple" and "pen" words from dictions and we have to use "pen" word two times and we are allowed to use as per problem statement.
Input: s = s = "catsandog", wordDict = ["cats","dog","sand","and","cat"]
Output: false
Explanation:"catsandog" cannot be made using the words from dictionary.

Contents

In this approach, we will start with every word from wordDict and check if the input string s is starting with that word or not, if it is continue this process with remainder substring.

Say for example if the input string given is "cscode" and word dictionary is {"code", "cs"}

If we look at the decision tree for all possible combinations, it will look like below diagram.

brute force approach public class WordBreak { /* brute force approach*/ static boolean bruteForceApproach(String s, String[] wordDict) { if(s.length()==0){ return true; } for(String word: wordDict) { if(s.indexOf(word) == 0) { // if the string starts with the word String remainder = s.substring(word.length()); if(bruteForceApproach(remainder, wordDict)) { return true; // possible to construct the string using words from wordDict } } } return false; } }
Complexity Analysis:

Time complexity: Above code runs in O(nm * m) time where n is the length of wordDict array and m is the length of the input target string s this is because whenever we found a match of starting word from dictionary, we are recursively calling the function for the remainder string and again for every match we are repeating the process.
The height of the decision will be m and for at each possible remainder we are making n calls to check every word from dictionary, that is O(nm) and the other m is because of the substring operation.
Space complexity: O(m2) since we calling the function recursively, this is memory used for remainder substring and number of recursive calls in the stack.

In the above implementation, we have created substrings using the indexes for easiness of understanding, but when we call the function recursively we could also pass starting index as parameter and avoid creating substrings, in that case space complexity will be O(nm).

From the above brute force approach, we can notice that there are some overlapping problems. For example, consider target s = "abcdxyz" and wordDict = {"abc", "d", "abcd", "lion"}

import java.util.HashMap; public class WordBreak { /* DP memoization approach */ static boolean dpMemoizationApproach(String s, String[] wordDict) { HashMap<String, Boolean> cache = new HashMap<>(); return dpMemoizationApproachRec(s, wordDict, cache); } static boolean dpMemoizationApproachRec(String s, String[] wordDict, HashMap<String, Boolean> cache) { if(s.length() == 0) { return true; } if(cache.containsKey(s)) { return cache.get(s); } for( String word: wordDict) { if(s.indexOf(word) == 0) { String remainder = s.substring(word.length()); if (dpMemoizationApproachRec(remainder, wordDict, cache)) { cache.put(remainder, true); return true; } } } cache.put(s, false); return false; } }
Complexity Analysis:

Time complexity: Above code runs in O(n * m2) time where n is the length of wordDict array and m is the length of the input target string s. This is because, for every word in dictionary, we are checking if this is a suffix or not (n * m), then creating a substring if it is a suffix (m).
Space complexity: O(m2) for the cache we are maintaining (m) , stack for recursive calls and the substring operation (m * m).

In this approach we will look at solving the problem using dynamic programming using tabulation caching mechanism.

In this solution, we are going to create a temporary array with size as m + 1, where m is target string length and +1 is to store value at the right end of cache array for base case. In this cache array, we are going to store whether a substring ending index i exists in the words dictionary or not.

public class WordBreak { /* DP tabulation memory optimized */ static boolean dpTabulationMemoryOptimized(String s, String[] wordDict) { boolean[] cache = new boolean[s.length()+1]; cache[s.length()] = true; // if we reach the end of input target string, that means string "s" // can be constructed using words from wordDict. for (int i=s.length()-1; i>=0; i--) { for(String word : wordDict) { if(i+ word.length() <= s.length() && word.equals(s.substring(i, i+word.length()))) { // i+ word.length() <= s.length() => // to check if "s" has enough characters to check cache[i] = cache[i + word.length()]; } if(cache[i] == true) { // we have found a matching word break; } } } return cache[0]; } }
Complexity Analysis:

Time complexity: Above code runs in O(n * m2) time where n is the length of wordDict array and m is the length of the input target string s. This is because, for every word in dictionary, we are checking if this is a suffix or not (n * m), then creating a substring if it is a suffix (m).
Space complexity: O(m) for the cache we are maintaining that is of input string length.

Above implementations source code can be found at GitHub link for Java code