AlgoMaster Logo

Substring with Concatenation of All Words

hardFrequencyUpdated August 26, 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

Visualization and Code

Loading animation...

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

Visualization and Code

Loading animation...