Substring with Concatenation of All Words
Substring with Concatenation of All Words
Given a string s and a list of words words, return all starting indices of substrings in s that are a concatenation of every word exactly once (in any order) with no intervening characters. All words have the same length.
Brute force checks each start with O(n * k) scanning. A sliding window by word length can do this in near-linear time.
Why this is worth learning
Equal word length turns character positions into several independent token streams. Each offset modulo wordLen needs its own sliding window.
Before coding: one-minute experiment
For s="barfoofoo" and words=["foo","bar"], why does start 0 match but start 3 fail?
Function signature
func FindSubstring(s string, words []string) []int
Inputs / outputs
s: the search string.words: list of words (all same length).- Return: starting indices of valid concatenations.
Example
s = "barfoothefoobarman"
words = ["foo","bar"]
Output: [0,9]
Constraints
0 <= len(s) <= 100_0000 <= len(words) <= 5000- All words have equal length.
Core idea (near-solution)
Treat s as a sequence of fixed-length words (size wordLen). Use a sliding window that moves in steps of wordLen.
For each offset in [0, wordLen-1]:
- Maintain a count map for the current window.
- If a word appears too many times, move the left side forward by one word at a time.
- When the window contains exactly
len(words)words with valid counts, record the start.
Invariant: the window always contains whole words (aligned to wordLen), and counts never exceed the required frequency for any word.
Algorithm (step-by-step)
- If
wordsis empty, return empty. - Compute
wordLenandwordCount. IfwordLen == 0, return empty. IfwordCount > len(s)/wordLen, the full concatenation cannot fit, so return empty. - Build a frequency map
needforwords. - For each
offsetfrom 0 towordLen-1:- Set
left = offset,count = 0, and clearwindowmap. - For
rightfromoffsettolen(s)-wordLenin steps ofwordLen:- Read
word = s[right:right+wordLen]. - If
wordis not inneed: reset the window and token count, setleft = right + wordLen, and continue to the next complete token. - Else increment its count; if count exceeds need, shrink from left until valid.
- If
count == wordCount, recordleftand then move left by one word.
- Read
- Set
- Return all recorded indices.
Correctness sketch
Within one offset, each step consumes one complete token. A token absent from the required dictionary cannot belong to a valid concatenation, so clearing the active window after it discards only impossible starts. For a known token whose count is excessive, remove leftmost tokens until its count is allowed again. All retained token counts are then at most their required multiplicities. If the retained token count equals the number of required words, those coordinate-wise bounds force exact equality of every multiplicity, so the start is valid. Every character start belongs to exactly one offset, and its token-aligned window is considered there. Overlapping answers remain discoverable after removing the leftmost token of a found match.
Trace (hand walkthrough)
Example input:
s = "barfoothefoobarman"
words = ["foo","bar"]
Output: [0,9]
Walk:
- wordLen=3, totalLen=6
- index 0: "barfoo" -> {bar,foo} matches -> add 0
- index 3: "foothe" no; index 6: "thefoo" no
- index 9: "foobar" matches -> add 9
Pitfalls to avoid
- Moving by one byte inside a token stream while still treating its entries as whole words. Every possible byte start is covered by the separate offset scans.
- Forgetting to reset when encountering a word not in
need. - Not handling duplicate words in
words.
Debugging lens
Test repeated dictionary words, a foreign token between matches, multiple offsets, and overlaps. Resetting a map with capacity len(words) at every foreign token wastes work; a fresh unhinted map avoids that repeated preallocation.
Complexity
Let N=len(s), K=len(words), L=word length, and U=number of distinct words. Expected time is O(N·L+K·L) when string hashing/equality costs O(L); for fixed L this is linear in N+K. Auxiliary live counting space is O(U), plus O(z) result indices. Empty inputs and L=0 return immediately.
Implementation notes (Go)
- Slide in steps of
wordLenand maintain counts in a map. - Move the left pointer in word-sized steps when a count exceeds its limit.
Contract precision
All words have the same length. Empty words list, empty text, or zero word length returns an empty result in this pack. Starts are emitted grouped by offset modulo word length, not necessarily globally sorted; callers/tests must not require sorted output.
Reconstruction checkpoint
For s="barfoofoo" and words=["foo","bar"], why does start 0 match but start 3 fail?
Model answer
Start 0 contains bar and foo exactly once. Start 3 contains foo twice and omits bar; total character length alone is not enough.