AlgoMaster Logo

Substring with Concatenation of All Words

hardFrequency9 min readUpdated June 23, 2026

Understanding the Problem

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.

Key Constraints:

  • All words have the same length. This is the constraint that enables a word-chunk-based sliding window: any candidate substring of length 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.

Approach 1: Brute Force

Intuition

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).

Algorithm

  1. Compute w (word length), m (number of words), and totalLen = m * w
  2. Build a frequency map target from the words array
  3. For each starting index i from 0 to s.length - totalLen:
    1. Extract m substrings of length w starting at positions ii + wi + 2w, ...
    2. Build a frequency map seen from these substrings, stopping early if any chunk's count exceeds its target count
    3. If all m chunks were valid, add i to the result
  4. Return the result list

Example Walkthrough

Take 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.

Code

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.

Approach 2: Sliding Window (Optimal)

Intuition

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.

Algorithm

  1. Build a frequency map target from words. Let distinctWords be the number of distinct keys in target
  2. For each starting offset k from 0 to w - 1:
    • Initialize an empty frequency map seen and matchCount = 0
    • Initialize left = k (left boundary of the window)
    • For each position right starting at k, stepping by w, while right + w <= s.length:
    • Extract the word rightWord = s[right..right+w-1]
    • If rightWord is in target:
      • Increment seen[rightWord]
      • If seen[rightWord] now equals target[rightWord], increment matchCount
      • If seen[rightWord] is one more than target[rightWord], decrement matchCount (we went from matched to over-counted)
    • If the window has more than m words (i.e., right - left >= totalLen):
      • Extract the word leftWord = s[left..left+w-1]
      • If leftWord is in target:
        • If seen[leftWord] equals target[leftWord], decrement matchCount (about to lose a match)
        • Decrement seen[leftWord]
        • If seen[leftWord] now equals target[leftWord], increment matchCount (removing the excess restored the match)
      • Move left forward by w
    • If matchCount == distinctWords, add left to the result
  3. Return the result

Example Walkthrough

Take 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].

Code