201 lines
8.8 KiB
YAML
201 lines
8.8 KiB
YAML
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:
|
|
- slug: two-pointers
|
|
is_optimal: true
|
|
|
|
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
|
|
|
|
|
|
|
|
**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
|
|
|
|
|
|
|
|
**Step 3: Return false if loop completes**
|
|
|
|
- If we exhaust all words without matching `s` exactly, return `false`
|
|
|
|
|
|
|
|
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.
|