We need to find every starting index in s where a substring of length words.length * wordLength is exactly a concatenation of all words in some order. The order doesn't matter, so we are looking for positions where the substring can be split into word-sized chunks that form the same multiset as words.
Checking permutations is unnecessary. Since order does not matter, we only need to verify that the frequency of each word-chunk matches the frequency of words in the array. Two multisets are equal if and only if every element appears the same number of times in both.
words can contain duplicates (like ["word","good","best","word"] in Example 2), so a simple set is not enough. We need a frequency map.
m * w always splits into m non-overlapping chunks of length w.words.length <= 5000 and words[i].length <= 30. The total window can be up to 150,000 characters, but s is at most 10^4, so the window is bounded by s.length. A position-by-position scan that rebuilds a frequency map costs O(n m w), roughly 1.5 * 10^9 operations in the worst case, too slow without reuse across windows.For every possible starting position in s, extract a substring of the right total length, split it into word-sized chunks, and check whether those chunks form the same multiset as words.
Build a frequency map of words once upfront. Then for each starting index i, extract m consecutive chunks of length w from position i, build a frequency map of those chunks, and compare the two maps. If they match, i is a valid starting index. To avoid building the full map when it cannot match, increment the count of each chunk as it is read and stop early the moment any chunk's count exceeds its target count (which also covers chunks that are not in the target at all, since their target count is zero).
w (word length), m (number of words), and totalLen = m * wtarget from the words arrayi from 0 to s.length - totalLen:m substrings of length w starting at positions i, i + w, i + 2w, ...seen from these substrings, stopping early if any chunk's count exceeds its target countm chunks were valid, add i to the resultTake s = "barfoothefoobarman", words = ["foo","bar"]. Here w = 3, m = 2, totalLen = 6, and target = {foo: 1, bar: 1}. Valid starting positions run from 0 to 18 - 6 = 12.
i = 0: chunks "bar", "foo". seen = {bar: 1, foo: 1}, no count exceeds target. Valid, add 0.i = 1: first chunk "arf" has target count 0, so seen["arf"] = 1 > 0. Break, reject.i = 2: first chunk "rfo", count exceeds 0. Reject.i = 3: chunks "foo", then "the". "the" has target count 0. Reject.i = 4 through i = 8: each starts with a chunk not in target ("oot", "oth", "the", "hef", "efo"). All rejected after the first chunk.i = 9: chunks "foo", "bar". seen = {foo: 1, bar: 1}. Valid, add 9.i = 10: first chunk "oob", not in target. Reject.i = 11: first chunk "oba", not in target. Reject.i = 12: first chunk "arm", not in target. Reject.The result is [0, 9], matching the expected output.
n - totalLen starting positions. At each position, we extract m substrings of length w. Substring extraction is O(w), so each position costs O(m * w). The early exit helps in practice but doesn't change the worst case.target map stores up to m words of length w, and the seen map is rebuilt at each position with the same capacity.The bottleneck is that we rebuild the seen frequency map from scratch at every starting position. Moving from position i to position i + w, the two windows share m - 1 of the same word chunks, but this approach discards that overlap and starts over. The next approach reuses it by sliding a fixed-size window one chunk at a time.
Since every word has the same length w, the string s decomposes into word-sized chunks, but the decomposition depends on where the alignment starts. Starting at index 0 gives chunks s[0..w-1], s[w..2w-1], s[2w..3w-1], and so on. Starting at index 1 shifts every chunk by one character. There are exactly w distinct alignments, one for each starting offset 0, 1, 2, ..., w-1, and every possible window start falls into exactly one alignment.
Within one alignment, the chunks form a fixed sequence of non-overlapping words. That lets us slide a window of exactly m consecutive chunks one chunk at a time: add the new chunk on the right, and once the window holds more than m chunks, remove the leftmost one. A window whose chunk frequencies match target is a valid concatenation.
Comparing the full seen and target maps at every step would cost O(distinct words) per step. Instead, maintain a single integer matchCount that tracks how many distinct words currently satisfy seen[word] == target[word]. The window is valid exactly when matchCount equals the number of distinct words in target, and matchCount updates in O(1) whenever a word enters or leaves the window.
target from words. Let distinctWords be the number of distinct keys in targetk from 0 to w - 1:seen and matchCount = 0left = k (left boundary of the window)right starting at k, stepping by w, while right + w <= s.length:rightWord = s[right..right+w-1]rightWord is in target:seen[rightWord]seen[rightWord] now equals target[rightWord], increment matchCountseen[rightWord] is one more than target[rightWord], decrement matchCount (we went from matched to over-counted)m words (i.e., right - left >= totalLen):leftWord = s[left..left+w-1]leftWord is in target:seen[leftWord] equals target[leftWord], decrement matchCount (about to lose a match)seen[leftWord]seen[leftWord] now equals target[leftWord], increment matchCount (removing the excess restored the match)left forward by wmatchCount == distinctWords, add left to the resultTake s = "barfoofoobarthefoobarman", words = ["bar","foo","the"]. Here w = 3, m = 3, totalLen = 9, target = {bar: 1, foo: 1, the: 1}, and distinctWords = 3. The expected answer is [6, 9, 12]. All three indices are multiples of 3, so offset k = 0 finds all of them and offsets 1 and 2 contribute nothing. Trace offset 0, whose chunks are bar foo foo bar the foo bar man. Each step adds the chunk at right, then shrinks from left once the window holds more than m words.
right = 0 ("bar"): seen = {bar: 1}, which matches its target, matchCount = 1. Window still under m words, no shrink. left = 0. Not valid.right = 3 ("foo"): seen = {bar: 1, foo: 1}, matchCount = 2. left = 0. Not valid.right = 6 ("foo"): seen[foo] = 2, one over target, so matchCount drops to 1. left = 0. Not valid.right = 9 ("bar"): seen[bar] = 2, over target. The window now spans 4 words (right - left = 9 >= totalLen), so it shrinks: leftWord "bar" at index 0 has seen[bar] return to 1, which restores its match and brings matchCount to 1. left = 3. Not valid.right = 12 ("the"): adds a match, matchCount = 2. Shrink: leftWord "foo" at index 3 has seen[foo] drop from 2 to 1, restoring its match, matchCount = 3. left = 6. Valid, add 6.right = 15 ("foo"): seen[foo] = 2, over target, matchCount = 2. Shrink: leftWord "foo" at index 6 returns to 1, restored, matchCount = 3. left = 9. Valid, add 9.right = 18 ("bar"): seen[bar] = 2, over target, matchCount = 2. Shrink: leftWord "bar" at index 9 returns to 1, restored, matchCount = 3. left = 12. Valid, add 12.right = 21 ("man"): not in target, so the right side is unchanged. Shrink: leftWord "the" at index 12 was matched, so matchCount drops to 2 and seen[the] = 0. left = 15. Not valid.The window then reaches the end of the string, offsets 1 and 2 add nothing, and the result is [6, 9, 12].
w offsets. For each offset, we scan through at most n/w positions, and at each position we extract a substring of length w (which is O(w)). So each offset costs O(n/w w) = O(n). Across all w offsets, the total is O(n * w). Since w <= 30 for this problem, this is effectively O(30n) = O(n).target and seen maps store up to m distinct words, each of length w. The result list can hold up to O(n) indices in the worst case.