title: Check If String Is a Prefix of Array
slug: check-if-string-is-a-prefix-of-array
difficulty: easy
leetcode_id: 1961
leetcode_url: https://leetcode.com/problems/check-if-string-is-a-prefix-of-array/
categories:
  - arrays
  - strings
patterns:
  - two-pointers

function_signature: "def is_prefix_string(s: str, words: list[str]) -> bool:"

test_cases:
  visible:
    - input: { s: "iloveleetcode", words: ["i", "love", "leetcode", "apples"] }
      expected: true
    - input: { s: "iloveleetcode", words: ["apples", "i", "love", "leetcode"] }
      expected: false
  hidden:
    - input: { s: "a", words: ["a", "b"] }
      expected: true
    - input: { s: "abc", words: ["abc"] }
      expected: true
    - input: { s: "ab", words: ["a", "bc"] }
      expected: false
    - input: { s: "abc", words: ["ab", "c", "d"] }
      expected: true

description: |
  Given a string `s` and an array of strings `words`, determine whether `s` is a **prefix string** of `words`.

  A string `s` is a **prefix string** of `words` if `s` can be made by concatenating the first `k` strings in `words` for some **positive** `k` no larger than `words.length`.

  Return `true` *if* `s` *is a prefix string of* `words`, *or* `false` *otherwise*.

constraints: |
  - `1 <= words.length <= 100`
  - `1 <= words[i].length <= 20`
  - `1 <= s.length <= 1000`
  - `words[i]` and `s` consist of only lowercase English letters.

examples:
  - input: 's = "iloveleetcode", words = ["i","love","leetcode","apples"]'
    output: "true"
    explanation: "s can be made by concatenating \"i\", \"love\", and \"leetcode\" together."
  - input: 's = "iloveleetcode", words = ["apples","i","love","leetcode"]'
    output: "false"
    explanation: "It is impossible to make s using a prefix of arr."

explanation:
  intuition: |
    Think of building a puzzle where you must place pieces strictly from left to right in order.

    You have a target string `s` that you need to recreate using the words in `words`, but there's a crucial rule: you can only use words from the **beginning** of the array, in sequence. You pick the 1<sup>st</sup> word, then optionally the 2<sup>nd</sup>, then optionally the 3<sup>rd</sup>, and so on — never skipping any word or changing the order.

    The core insight is that you're essentially checking if the concatenation of `words[0] + words[1] + ... + words[k-1]` **exactly equals** `s` for some value of `k`. Not a substring, not a partial match — an **exact match**.

    As you concatenate words one by one, you're building up a string. At each step, you check: "Does what I've built so far match `s` exactly?" If yes, you've found your answer. If you've built something longer than `s` or something that doesn't match the prefix of `s`, you can stop early.

  approach: |
    We solve this using a **Single Pass with String Building**:

    **Step 1: Initialise a result string**

    - `built`: Start with an empty string that we'll grow by appending words

    &nbsp;

    **Step 2: Iterate through the words array**

    - For each word in `words`, append it to `built`
    - After each append, check if `built == s`
    - If equal, return `true` immediately — we've found our prefix string
    - If `len(built) > len(s)`, return `false` — we've overshot, no point continuing

    &nbsp;

    **Step 3: Return false if loop completes**

    - If we exhaust all words without matching `s` exactly, return `false`

    &nbsp;

    This approach is efficient because we stop as soon as we find a match or overshoot the target length. We never do unnecessary work.

  common_pitfalls:
    - title: Checking for Substring Instead of Exact Match
      description: |
        A common mistake is to check if `s` is a *substring* of the concatenation, or if the concatenation *contains* `s`.

        For example, with `s = "i"` and `words = ["i", "love"]`, the concatenation `"ilove"` contains `"i"`, but that's not what we want. We need `s` to be **exactly equal** to the concatenation of some prefix of `words`.

        Always use equality (`==`), not substring checks (`in` or `startswith`).
      wrong_approach: "Check if s is substring of concatenation"
      correct_approach: "Check if s equals concatenation exactly"

    - title: Forgetting to Check After Each Word
      description: |
        Some implementations concatenate all words first, then check if the result equals `s`. This misses the requirement that `s` must equal the concatenation of the **first k words** for some specific `k`.

        With `s = "ilove"` and `words = ["i", "love", "leetcode"]`, concatenating all gives `"iloveleetcode"`, which doesn't equal `s`. But concatenating just the first 2 words gives `"ilove"`, which does equal `s`.

        You must check for equality **after each word** is added.
      wrong_approach: "Concatenate all, then check"
      correct_approach: "Check equality after each concatenation"

    - title: Not Handling the Overshoot Case
      description: |
        If the built string becomes longer than `s` but doesn't match, continuing to add more words is wasteful. For efficiency, check if `len(built) > len(s)` and return `false` early.

        This also handles the case where `words` contains very long strings that immediately overshoot `s`.

  key_takeaways:
    - "**Prefix concatenation**: This pattern of checking prefix concatenations appears in string matching problems — build incrementally and validate at each step"
    - "**Early termination**: Stop processing as soon as you find a match or determine a match is impossible (overshoot)"
    - "**Exact match vs. substring**: Be precise about what the problem asks — equality is stricter than containment"
    - "**Order matters**: The sequential constraint (must use words in order from the start) is what makes this problem tractable"

  time_complexity: "O(n) where n is the length of string `s`. In the worst case, we process characters up to the length of `s` before finding a match or overshooting."
  space_complexity: "O(n) where n is the length of string `s`. We build a string that grows up to the size of `s`."

solutions:
  - approach_name: String Building
    is_optimal: true
    code: |
      def is_prefix_string(s: str, words: list[str]) -> bool:
          # Build the concatenation incrementally
          built = ""

          for word in words:
              # Add the next word to our built string
              built += word

              # Check if we've matched s exactly
              if built == s:
                  return True

              # If we've overshot, no point continuing
              if len(built) > len(s):
                  return False

          # Exhausted all words without matching s
          return False
    explanation: |
      **Time Complexity:** O(n) — We process at most n characters where n = len(s).

      **Space Complexity:** O(n) — The built string grows up to the size of s.

      This solution builds the concatenation one word at a time, checking for an exact match after each addition. Early termination on overshoot keeps it efficient.

  - approach_name: Index Tracking
    is_optimal: true
    code: |
      def is_prefix_string(s: str, words: list[str]) -> bool:
          # Track our position in s
          index = 0

          for word in words:
              # Check if this word matches the next part of s
              if s[index:index + len(word)] != word:
                  return False

              # Move our position forward
              index += len(word)

              # Check if we've matched all of s
              if index == len(s):
                  return True

          # Didn't reach the end of s
          return False
    explanation: |
      **Time Complexity:** O(n) — We scan through s once using index tracking.

      **Space Complexity:** O(1) — Only using an integer index, no extra string built.

      This approach avoids building a new string by directly comparing slices of `s` with each word. It's more memory-efficient as it uses constant extra space.

  - approach_name: Join and Compare
    is_optimal: false
    code: |
      def is_prefix_string(s: str, words: list[str]) -> bool:
          # Try each possible prefix length k
          for k in range(1, len(words) + 1):
              # Join first k words and compare
              if "".join(words[:k]) == s:
                  return True
              # Early exit if we've already exceeded s
              if len("".join(words[:k])) > len(s):
                  return False

          return False
    explanation: |
      **Time Complexity:** O(k * n) — For each k, joining creates a new string.

      **Space Complexity:** O(n) — Each join creates a string up to length n.

      This approach is less efficient because `join` creates a new string for each value of k. The string building approach is better because it incrementally extends the same string.
