Substring with Concatenation of All Words
HardPrompt
You are given a string s and an array of strings words. All the strings of words are of the same length.
A concatenated string is a string that exactly contains all the strings of any permutation of words concatenated.
- For example, if
words = ["ab","cd","ef"], then"abcdef","abefcd","cdabef","cdefab","efabcd", and"efcdab"are all concatenated strings."acdbef"is not a concatenated string because it is not the concatenation of any permutation ofwords.
Return an array of the starting indices of all the concatenated substrings in s. You can return the answer in any order.
Example 1:
Input: s = "barfoothefoobarman", words = ["foo","bar"]
Output: [0,9]
Explanation:
The substring starting at 0 is "barfoo". It is the concatenation of ["bar","foo"] which is a permutation of words.
The substring starting at 9 is "foobar". It is the concatenation of ["foo","bar"] which is a permutation of words.
Example 2:
Input: s = "wordgoodgoodgoodbestword", words = ["word","good","best","word"]
Output: []
Explanation:
There is no concatenated substring.
Example 3:
Input: s = "barfoofoobarthefoobarman", words = ["bar","foo","the"]
Output: [6,9,12]
Explanation:
The substring starting at 6 is "foobarthe". It is the concatenation of ["foo","bar","the"].
The substring starting at 9 is "barthefoo". It is the concatenation of ["bar","the","foo"].
The substring starting at 12 is "thefoobar". It is the concatenation of ["the","foo","bar"].
Constraints:
1 <= s.length <= 1041 <= words.length <= 50001 <= words[i].length <= 30sandwords[i]consist of lowercase English letters.
Approaches
2 approaches with complexity analysis and trade-offs.
This approach iterates through every possible starting position of a concatenated substring in s. For each position, it checks if the following substring of length numWords * wordLen is a valid concatenation of words from the words array. This check is performed by using a hash map to count word frequencies within the substring and comparing it to the frequency map of the original words array.
Algorithm
- Get the number of words
numWords, the length of each wordwordLen, and the total length of the concatenated substringsubstringLen. - Create a frequency map
wordCountfor the words in thewordsarray. - Initialize an empty list
resultto store the starting indices. - Iterate through the string
sfrom indexi = 0tos.length() - substringLen. - For each
i, extract the substring of lengthsubstringLen. - Create a new frequency map
wordsSeenfor the words within this substring by breaking it into chunks ofwordLen. - Compare the
wordsSeenmap with thewordCountmap. - If the maps are identical, add the index
ito theresultlist. - Return the
resultlist after checking all possible indices.
Walkthrough
The method begins by pre-calculating essential values: numWords (the count of words), wordLen (the length of a single word), and substringLen (the total length of the target substring). A reference frequency map, wordCount, is created from the input words array to know what a valid permutation should contain.
The main part of the algorithm is a loop that iterates through s from index 0 up to the last possible starting point for a valid substring. In each iteration, a substring of length substringLen is extracted. To verify if this substring is a valid concatenation, we build a second frequency map, wordsSeen, by chopping the substring into wordLen-sized chunks and counting them. Finally, we compare wordsSeen with wordCount. If they are equal, the starting index i is a valid solution and is added to our results.
import java.util.ArrayList;import java.util.HashMap;import java.util.List;import java.util.Map; class Solution { public List<Integer> findSubstring(String s, String[] words) { if (s == null || s.length() == 0 || words == null || words.length() == 0) { return new ArrayList<>(); } int wordLen = words[0].length(); int numWords = words.length; int substringLen = wordLen * numWords; List<Integer> result = new ArrayList<>(); if (s.length() < substringLen) { return result; } Map<String, Integer> wordCount = new HashMap<>(); for (String word : words) { wordCount.put(word, wordCount.getOrDefault(word, 0) + 1); } for (int i = 0; i <= s.length() - substringLen; i++) { Map<String, Integer> wordsSeen = new HashMap<>(); for (int j = 0; j < numWords; j++) { int wordStart = i + j * wordLen; String word = s.substring(wordStart, wordStart + wordLen); wordsSeen.put(word, wordsSeen.getOrDefault(word, 0) + 1); } if (wordCount.equals(wordsSeen)) { result.add(i); } } return result; }}Complexity
Time
O(N * M * L)
Space
O(M * L)
Trade-offs
Pros
Conceptually simple and straightforward to implement.
It correctly solves the problem without the complexity of generating permutations.
Cons
Highly inefficient due to redundant computations. For each starting position
i, it re-evaluates the entire substring from scratch.The time complexity is very high, which will likely cause a 'Time Limit Exceeded' (TLE) error on platforms like LeetCode for larger inputs.
This approach significantly optimizes the search by using a sliding window technique. Since all words have the same length, wordLen, any valid concatenated substring must be composed of words that are aligned on a grid with a step size of wordLen. We can check all possible alignments by running wordLen separate passes over the string. Each pass uses a sliding window that moves wordLen characters at a time, efficiently updating word counts instead of re-computing them from scratch for each starting position.
Algorithm
- Pre-calculate
wordLen,numWords, and create a reference frequency mapwordCountfromwords. - Loop
ifrom0towordLen - 1. This outer loop handles all possible word alignments. - For each
i, initialize a sliding window withleft = i,wordsFound = 0, and an emptyseenWordsmap. - Use a second pointer
jto slide through the string fromiin steps ofwordLen. - At each step
j, extract theword. - If
wordis inwordCount:- Update its count in
seenWordsand incrementwordsFound. - If the count of
wordinseenWordsexceedswordCount, shrink the window from theleftuntil it's valid again, updatingseenWordsandwordsFound. - If
wordsFoundequalsnumWords, a valid substring is found. Addleftto the results, and slide the window one step to the right to continue the search.
- Update its count in
- If
wordis not inwordCount, reset the window and maps, and moveleftto the position after the invalid word. - Return the collected results.
Walkthrough
The key insight is that we don't need to check every single starting index. A valid substring starting at index k will be composed of words at k, k+L, k+2L, ... where L is wordLen. This means we can iterate through the string L times, once for each possible remainder modulo L (i.e., i = 0, 1, ..., L-1).
For each starting offset i, we use a sliding window. We maintain a left pointer for the start of the window and a j pointer for the end. We slide j through the string in steps of L. We use a seenWords map to track word frequencies within the current window.
- When a new word (at
j) enters the window, we updateseenWords. - If the new word is not in our target
wordCountmap, the window is invalid, so we reset it. - If a word's count in
seenWordsbecomes too high, we shrink the window from theleftuntil the counts are valid. - When the number of valid words in our window (
wordsFound) reachesnumWords, we've found a solution. We record theleftindex and then slide the window by one word to search for the next potential match.
This avoids re-computation and brings the complexity down significantly.
import java.util.ArrayList;import java.util.HashMap;import java.util.List;import java.util.Map; class Solution { public List<Integer> findSubstring(String s, String[] words) { if (s == null || s.length() == 0 || words == null || words.length == 0) { return new ArrayList<>(); } Map<String, Integer> wordCount = new HashMap<>(); for (String word : words) { wordCount.put(word, wordCount.getOrDefault(word, 0) + 1); } int wordLen = words[0].length(); int numWords = words.length; List<Integer> result = new ArrayList<>(); for (int i = 0; i < wordLen; i++) { int left = i; int wordsFound = 0; Map<String, Integer> seenWords = new HashMap<>(); for (int j = i; j <= s.length() - wordLen; j += wordLen) { String word = s.substring(j, j + wordLen); if (wordCount.containsKey(word)) { seenWords.put(word, seenWords.getOrDefault(word, 0) + 1); wordsFound++; while (seenWords.get(word) > wordCount.get(word)) { String leftWord = s.substring(left, left + wordLen); seenWords.put(leftWord, seenWords.get(leftWord) - 1); wordsFound--; left += wordLen; } if (wordsFound == numWords) { result.add(left); String leftWord = s.substring(left, left + wordLen); seenWords.put(leftWord, seenWords.get(leftWord) - 1); wordsFound--; left += wordLen; } } else { seenWords.clear(); wordsFound = 0; left = j + wordLen; } } } return result; }}Complexity
Time
O(N * L)
Space
O(M * L)
Trade-offs
Pros
Highly efficient, with a time complexity that is linear in the length of the input string
sand word lengthL.Avoids redundant work by intelligently sliding the window and updating counts incrementally.
Cons
The logic is more complex to understand and implement correctly compared to the brute-force approach.
It involves careful management of the sliding window, multiple pointers, and frequency maps.
Solutions
Solution
public class Solution { public IList < int > FindSubstring(string s, string[] words) { var cnt = new Dictionary < string, int > (); foreach(var w in words) { if (!cnt.ContainsKey(w)) { cnt[w] = 0; }++cnt[w]; } int m = s.Length, n = words.Length, k = words[0].Length; var ans = new List < int > (); for (int i = 0; i < k; ++i) { var cnt1 = new Dictionary < string, int > (); int l = i, r = i, t = 0; while (r + k <= m) { var w = s.Substring(r, k); r += k; if (!cnt.ContainsKey(w)) { cnt1.Clear(); t = 0; l = r; continue; } if (!cnt1.ContainsKey(w)) { cnt1[w] = 0; }++cnt1[w]; ++t; while (cnt1[w] > cnt[w]) { --cnt1[s.Substring(l, k)]; l += k; --t; } if (t == n) { ans.Add(l); } } } return ans; }}class Solution {public List<Integer> findSubstring(String s, String[] words) { Map<String, Integer> cnt = new HashMap<>(); for (String w : words) { cnt.merge(w, 1, Integer : : sum); } int m = s.length(), n = words.length; int k = words[0].length(); List<Integer> ans = new ArrayList<>(); for (int i = 0; i < k; ++i) { Map<String, Integer> cnt1 = new HashMap<>(); int l = i, r = i; int t = 0; while (r + k <= m) { String w = s.substring(r, r + k); r += k; if (!cnt.containsKey(w)) { cnt1.clear(); l = r; t = 0; continue; } cnt1.merge(w, 1, Integer : : sum); ++t; while (cnt1.get(w) > cnt.get(w)) { String remove = s.substring(l, l + k); l += k; cnt1.merge(remove, -1, Integer : : sum); --t; } if (t == n) { ans.add(l); } } } return ans; }}class Solution {public: vector<int> findSubstring(string s, vector<string> &words) { unordered_map<string, int> cnt; for (auto &w : words) { ++cnt[w]; } int m = s.size(), n = words.size(), k = words[0].size(); vector<int> ans; for (int i = 0; i < k; ++i) { unordered_map<string, int> cnt1; int l = i, r = i; int t = 0; while (r + k <= m) { string w = s.substr(r, k); r += k; if (!cnt.count(w)) { cnt1.clear(); l = r; t = 0; continue; } ++cnt1[w]; ++t; while (cnt1[w] > cnt[w]) { string remove = s.substr(l, k); l += k; --cnt1[remove]; --t; } if (t == n) { ans.push_back(l); } } } return ans; }};class Solution: def findSubstring(self, s: str, words: List[str]) -> List[int]: cnt = Counter(words) m, n = len(s), len(words) k = len(words[0]) ans = [] for i in range(k): cnt1 = Counter() l = r = i t = 0 while r + k <= m: w = s[r: r + k] r += k if w not in cnt: l = r cnt1 . clear() t = 0 continue cnt1[w] += 1 t += 1 while cnt1[w] > cnt[w]: remove = s[l: l + k] l += k cnt1[remove] -= 1 t -= 1 if t == n: ans . append(l) return ansVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.