1961. Check If String Is a Prefix of Array
Problem Description
You are given a string s
and an array of strings words
. You need to determine if s
can be formed by concatenating some consecutive strings from the beginning of the words
array.
A string s
is called a prefix string of words
if you can take the first k
strings from words
(where k
is at least 1 and at most the length of words
), concatenate them together in order, and get exactly s
.
For example:
- If
words = ["a", "b", "c"]
ands = "ab"
, thens
is a prefix string because"a" + "b" = "ab"
- If
words = ["a", "b", "c"]
ands = "ac"
, thens
is NOT a prefix string because you cannot form"ac"
by concatenating consecutive strings from the beginning
The solution works by iterating through the words
array and keeping track of the total length of concatenated strings using variable m
. For each word, it adds the word's length to m
. When m
equals the length of s
, it checks if concatenating all words up to the current index equals s
. If they match, it returns true
. If the loop completes without finding a match, it returns false
.
The key insight is that we only need to check for equality when the accumulated length exactly matches the target string's length, avoiding unnecessary string concatenations at every step.
Intuition
The key observation is that if s
is a prefix string, it must be formed by concatenating exactly the first k
strings from words
for some value of k
. This means we need to find a point where the concatenation of words exactly matches s
.
Instead of concatenating strings at each step (which would be inefficient), we can track the cumulative length of words as we iterate through the array. Why? Because if s
has length n
, and we've processed words with a total length of m
:
- If
m < n
, we haven't concatenated enough words yet - If
m > n
, we've gone too far ands
cannot be a prefix string - If
m == n
, this is the only point where we need to check if the concatenation equalss
This length-based approach saves us from performing expensive string concatenations at every iteration. We only build the concatenated string once - when the lengths match perfectly.
Think of it like filling a glass of water: you don't need to check if the glass is full after every drop. You only need to check when the water level reaches the rim. Similarly, we only need to verify the actual string content when the accumulated length matches our target length.
If we finish iterating through all words without ever hitting the exact length of s
, then s
cannot be formed by any prefix of words
, so we return false
.
Learn more about Two Pointers patterns.
Solution Approach
The implementation uses a traversal approach with length tracking to efficiently determine if s
is a prefix string.
Algorithm Steps:
-
Initialize variables: We start with
n = len(s)
to store the target length, andm = 0
to track the cumulative length of words processed so far. -
Iterate through words: We use
enumerate(words)
to traverse the array while keeping track of both the indexi
and the current wordw
. -
Accumulate lengths: For each word
w
, we add its length tom
usingm += len(w)
. This gives us the total length of all words concatenated up to the current position. -
Check for exact match: When
m == n
(cumulative length equals target length), we perform the actual string comparison:- We concatenate the first
i + 1
words using"".join(words[:i + 1])
- We compare this concatenation with
s
- If they match, we return
true
- We concatenate the first
-
Handle mismatches: If we complete the loop without finding a match (meaning either we never reached the exact length
n
, or we exceeded it), we returnfalse
.
Why this approach works:
- By tracking lengths first, we avoid unnecessary string operations until we find a potential match
- The condition
m == n
ensures we only check when we have exactly the right amount of characters - If
m
exceedsn
during iteration, the loop continues but will never satisfym == n
, eventually returningfalse
- The slicing operation
words[:i + 1]
gives us exactly the firsti + 1
words needed for the prefix
Time Complexity: O(n)
where n
is the length of string s
, as we potentially need to build and compare a string of length n
.
Space Complexity: O(1)
extra space if we don't count the space used for string concatenation in the comparison step.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with a concrete example to see how the algorithm works step by step.
Example:
words = ["cat", "dog", "fish"]
s = "catdog"
- Target length
n = 6
Step-by-step execution:
Iteration 1 (i=0, word="cat"):
- Current word: "cat" (length = 3)
- Update cumulative length:
m = 0 + 3 = 3
- Check if
m == n
? β3 == 6
? No - Continue to next word
Iteration 2 (i=1, word="dog"):
- Current word: "dog" (length = 3)
- Update cumulative length:
m = 3 + 3 = 6
- Check if
m == n
? β6 == 6
? Yes! - Now we perform the actual string check:
- Concatenate first
i+1 = 2
words:"cat" + "dog" = "catdog"
- Compare with
s
:"catdog" == "catdog"
? Yes!
- Concatenate first
- Return
true
The algorithm successfully identifies that "catdog" can be formed by concatenating the first 2 words from the array.
Counter-example to show when it returns false:
Let's try with s = "catfi"
(still length 6):
- Iteration 1: m = 3, not equal to 6, continue
- Iteration 2: m = 6, equals 6, check: "catdog" β "catfi", continue
- Iteration 3: m = 10, exceeds 6, continue
- Loop completes without finding a match
- Return
false
This demonstrates how the algorithm efficiently checks only when lengths match, avoiding unnecessary string operations.
Solution Implementation
1class Solution:
2 def isPrefixString(self, s: str, words: List[str]) -> bool:
3 """
4 Check if string s can be formed by concatenating a prefix of the words array.
5
6 Args:
7 s: Target string to check
8 words: List of words to concatenate
9
10 Returns:
11 True if s equals the concatenation of some prefix of words, False otherwise
12 """
13 target_length = len(s)
14 current_length = 0
15
16 # Iterate through words and accumulate their lengths
17 for index, word in enumerate(words):
18 current_length += len(word)
19
20 # If accumulated length matches target string length
21 if current_length == target_length:
22 # Check if concatenation of words[0:index+1] equals s
23 concatenated_string = "".join(words[:index + 1])
24 return concatenated_string == s
25
26 # If we've exceeded the target length, s cannot be formed
27 if current_length > target_length:
28 return False
29
30 # If we've processed all words without matching the length, return False
31 return False
32
1class Solution {
2 public boolean isPrefixString(String s, String[] words) {
3 // Build a string by concatenating words from the array
4 StringBuilder concatenatedString = new StringBuilder();
5
6 // Iterate through each word in the words array
7 for (String word : words) {
8 // Append current word to the concatenated string
9 concatenatedString.append(word);
10
11 // If concatenated string exceeds target string length, it cannot be a match
12 if (concatenatedString.length() > s.length()) {
13 return false;
14 }
15
16 // If concatenated string has same length as target string, check if they are equal
17 if (concatenatedString.length() == s.length()) {
18 return s.equals(concatenatedString.toString());
19 }
20 }
21
22 // If we've used all words but haven't reached the target string length, return false
23 return false;
24 }
25}
26
1class Solution {
2public:
3 bool isPrefixString(string s, vector<string>& words) {
4 string concatenated = ""; // String to store concatenated words
5
6 // Iterate through each word in the words array
7 for (const auto& word : words) {
8 // Append current word to the concatenated string
9 concatenated += word;
10
11 // If concatenated string exceeds target string length, it can't be a match
12 if (concatenated.size() > s.size()) {
13 return false;
14 }
15
16 // If concatenated string has same length as target, check if they're equal
17 if (concatenated.size() == s.size()) {
18 return concatenated == s;
19 }
20 }
21
22 // If we've used all words but haven't matched the target string, return false
23 return false;
24 }
25};
26
1/**
2 * Checks if string s is exactly formed by concatenating a prefix of the words array
3 * @param s - The target string to check
4 * @param words - Array of words to concatenate
5 * @returns true if s equals the concatenation of some prefix of words array, false otherwise
6 */
7function isPrefixString(s: string, words: string[]): boolean {
8 // Array to collect words that form the prefix
9 const prefixWords: string[] = [];
10
11 // Length of the target string
12 const targetLength: number = s.length;
13
14 // Running total of concatenated characters
15 let currentLength: number = 0;
16
17 // Iterate through each word in the words array
18 for (const word of words) {
19 // Add current word's length to running total
20 currentLength += word.length;
21
22 // If we've exceeded the target string length, it's not a valid prefix
23 if (currentLength > targetLength) {
24 return false;
25 }
26
27 // Add current word to our prefix collection
28 prefixWords.push(word);
29
30 // If lengths match exactly, check if concatenation equals target string
31 if (currentLength === targetLength) {
32 return s === prefixWords.join('');
33 }
34 }
35
36 // If we've used all words but haven't matched the target length, return false
37 return false;
38}
39
Time and Space Complexity
Time Complexity: O(n)
The algorithm iterates through the words
array and accumulates the total length m
. In the worst case, it needs to check all words until m
equals n
(the length of string s
). When m == n
, it performs a string concatenation and comparison using "".join(words[:i+1]) == s
. The join operation takes O(n)
time since it concatenates strings with a total length of n
characters, and the comparison also takes O(n)
time. Therefore, the overall time complexity is O(n)
.
Space Complexity: O(n)
The space complexity comes from the string concatenation operation "".join(words[:i+1])
. This creates a new string that, when m == n
, has exactly n
characters (same length as string s
). The slicing operation words[:i+1]
creates a new list reference but doesn't copy the string objects themselves. However, the join operation creates a new string of length n
, resulting in O(n)
space complexity.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Early Termination When Length Exceeds Target
The Problem:
In the provided code, there's an optimization attempt that returns False
when current_length > target_length
. However, this creates redundant code since the loop will naturally continue and never satisfy the current_length == target_length
condition anyway.
Why It's Problematic:
- Adds unnecessary complexity to the code
- The early termination check
if current_length > target_length: return False
is redundant because once we exceed the target length, we'll never match it again - Makes the code harder to read and maintain
Better Solution: Remove the redundant check and let the natural flow handle it:
def isPrefixString(self, s: str, words: List[str]) -> bool:
target_length = len(s)
current_length = 0
for index, word in enumerate(words):
current_length += len(word)
if current_length == target_length:
concatenated_string = "".join(words[:index + 1])
return concatenated_string == s
return False
Pitfall 2: Not Checking Character-by-Character Match
The Problem: The current solution only checks for string equality when lengths match, but it doesn't verify that the characters match as we go. This means we might do unnecessary iterations even when we already know the prefix doesn't match.
Example:
s = "abc"
,words = ["x", "yz"]
- The algorithm will check length 1, skip comparison, then check length 3, but the first character already doesn't match
Better Solution: Check character matching as we build the prefix:
def isPrefixString(self, s: str, words: List[str]) -> bool:
current_prefix = ""
for word in words:
current_prefix += word
# If prefix is longer than s, it can't match
if len(current_prefix) > len(s):
return False
# Check if current prefix matches the beginning of s
if not s.startswith(current_prefix):
return False
# If we've built exactly s, we found a match
if current_prefix == s:
return True
return False
Pitfall 3: Inefficient String Concatenation
The Problem:
Using "".join(words[:index + 1])
creates a new list slice and then joins it every time we need to check. This is inefficient if we need to check multiple times or with large word arrays.
Better Solution: Build the string incrementally:
def isPrefixString(self, s: str, words: List[str]) -> bool:
built_string = ""
for word in words:
built_string += word
if len(built_string) == len(s):
return built_string == s
elif len(built_string) > len(s):
return False
return False
This approach avoids repeated slicing and joining operations, making it more efficient for larger inputs.
Which of the following uses divide and conquer strategy?
Recommended Readings
Tech Interview Pattern Two Pointers Introduction If you prefer videos here's a super quick introduction to Two Pointers div class responsive iframe iframe src https www youtube com embed xZ4AfXHQ1VQ title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture allowfullscreen iframe div Two pointers is a common interview
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Donβt Miss This!