Can Construct

Updated: 01 February 2024

Problem

Given a target string and a list of strings return a boolean that indicates if the items in the string list can be combined to build the string in the target

An example of what we want to do is:

canConstruct(abcdef, [ab, cd, ef]) = true
canConstruct(abcdef, [ab, cd]) = false

In general it will probably be easier to create a shorter string than a longer one

We can consider a base case where:

canConstruct('', [a,b,c]) = true

Base Implementation

We can work on shrinking the string by stepping and splitting off subscrings in our input data ensuring that we do not break the sequencing of the initial string, so we do not remove any segments from the middle of the string

We will split off substrings if they are within the start of our initial string

dynamic-programming/memoization/can-construct.ts
export const canConstruct = (target: string, parts: string[]): boolean => {
  if (target == "") return true;

  for (let part of parts) {
    if (target.startsWith(part)) {
      const restOfWord = target.slice(part.length);

      const result = canConstruct(restOfWord, parts);

      if (result) return true;
    }
  }

  return false;
};

In this implementation the height of this tree can be the length of the target word $m$, for every element we would do a check which is $n$, in this case the time complexity is $O(n^m * m)$ (the last $m$ because we’re slicing the string on each iteration). The space complexity is the at most the length of our string of $m$ and the new string we have that is maybe also of length $m$, the space complexity is the $O(m^2)$

With memoization

We implement the memoization as in the previous examples

dynamic-programming/memoization/can-construct-memo.ts
type Memo = Record<string, boolean>;

export const canConstruct = (
  target: string,
  parts: string[],
  memo: Memo = {}
): boolean => {
  if (target in memo) return memo[target];
  if (target == "") return true;

  for (let part of parts) {
    if (target.startsWith(part)) {
      const restOfWord = target.slice(part.length);

      const result = canConstruct(restOfWord, parts, memo);

      if (result) {
        memo[target] = result;
        return true;
      }
    }
  }

  memo[target] = false;
  return false;
};

When we memoize this we are removing subtrees, this way we cut down the width of our tree. In the act of memoizing we define a map that stores the result of each substring which is of size $m$, the overall time complexity is the $O(n * m^2)$ and a space complexity of $O(m^2)$