top of page
Search
Vasu Pasupuleti

Backtracking and Memoization

Updated: Dec 29, 2023



Backtracking is a technique that we talked about when solving permutations. Let’s look at another problem where backtracking is the key to solving it.


Problem: Given a non-empty string `s` and a dictionary, `dict` determine if `s` can be segmented into a sequence of one or more dictionary words.


Algorithm 1:

Using a backtracking approach find the first substring that is a valid dictionary word and then recurse on the remaining string to see if it can be further split.


splitWord(s[0..n]) = s[0..i] + splitWord(s[i+1..n]) where s[0..i] is a dictionary word.


If splitWord(s[i+1..n]) fails, then we backtrack to index i where the last match was found and scan for the next valid dictionary word.


splitWord(s[0..n]) = s[0..j] + splitWord([j..n]) where j > i and s[0..j] is a dictionary word

bool splitWord(string s, set<int> dict)
{
   if(s.length() == 0)
     return true;
 
   for(int i=1;i<=s.length();i++) {     
      string temp = s.substr(0,i);     

      if((dict.count(temp)!=0) && 
         splitWord(s.substr(i))) {
         return true;
     }
   }
   return false;
}

Example
s: catsanddogs
dict: {cat, cats, and, dogs}
1. {cat, sanddogs} //cat is a match
2. {sanddogs} — no match
   1. return to step 1 and extend from the last match
4. {cats, anddogs} //cats is a match
5. {anddogs}
6. {and, dogs} //and is a match
7. {dogs} — matched.
Answer: True -> {cats, and, dogs}
Step 2 is where we backtracked to last match and extended the string to find next possible match.

Run time for this solution is O(2^n) because in the worst case each character of the string might be present in the dictionary.


Example: 
s: aaab
dict: {a, aa, aaa}
In the above case each letter is a valid dictionary word. So, we have to do recursive call on every possible prefix.
1. {aaab} -> {a, aab} 
2. {aab} -> {a, ab} 
3. {ab} -> {a, b} 
4. {b} //no match, 
    2. {aab} -> {aa, b} //return to step 2
5. {b} //no match 
    1. {aaab} -> {aa, ab} 
6. {ab} -> {a, b} 
7. {b} //no match 
    1. {aaab} -> {aaa, b} 
8. {b} //no match 

// 2^n-1 calls. Also, in each call we have to iterate through the length of the string O(n).
T(n) = n + T(n-1) + T(n-2) + .. T(1)
T(n) = n + n-1 + 2*T(n-2) + 2*T(n-3) + .. T(1)
T(n) = 2n — 1 + 2*T(n-2) + 2*T(n-3)+2*T(n-4)
T(n) = 4n — 5 + 4*T(n-3) + 4*T(n-4)
T(n) = 8n — 17 + 8*T(n-4)
T(n) = (n*2^n-1) = O(2^n)

The run time can be reduced if we memorize the solution. E.g., As part of T(n-2) we solve T(n-3) and remember the solution for T(n-3). When we start to solve T(n-1) we can reuse the solution of T(n-3) without going into further recursive calls.


bool splitWord(string s, set<string> &visited, set<string> dict)
{
   if(s.length() == 0)
   return true;
 
   for(int i=1;i<=s.length();i++) {
   string temp = s.substr(0,i);
   if(dict.count(temp)!=0 && 
      visited.count(s)==0)
   {
     if(splitWord(s.substr(i), visited, dict)) {
        return true;
      } 
      visited.insert(s.substr(i));
     }
   }
   return false;
 }

Example: 
s: aaab
dict: {a, aa, aaa}
1. {aaab} -> {a, aab} 
2. {aab} -> {a, ab} 
3. {ab} -> {a, b} 
4. {b} //no match
   3. insert b in visited
   2. insert ab in visited
   2. {aab} -> {aa, b} //skip as b is visited
   1. insert aab in visited
   1. {aaab} -> {aa, ab} //skip as ab is visited
   1. {aaab} -> {aaa, b} // skip as b is visted. 
In total we have made only n-1 recursive calls for a string of length n. However, we travese a string of length n in each recursive call.
T(n) = n+T(n-1) = 2n + T(n-2) = 3n + T(n-3) = n²


27 views0 comments

Recent Posts

See All

Comments


bottom of page