Skip to main content

Minimum Window Substring

How to turn "does it contain everything?" into a satisfaction condition and shrink the window only while it stays valid.

Given two strings s and t, return the minimum substring of s that contains all the characters of t, including duplicates.

If no valid substring exists, return "".

In the SeniorPath playground, when multiple valid answers have the same length, return the one that appears first.

Example:

Input:  s = "ADOBECODEBANC", t = "ABC"
Output: "BANC"

What to notice before coding

Common mistakes

Step 1 - Understand what "contains everything" really means

If t = "ABC", the window must contain:

  • at least one A
  • at least one B
  • at least one C

If t = "AABC", the rule changes:

  • two A
  • one B
  • one C

So the right question is not only "does this character exist?".

The right question is:

  • did the current window already reach every required frequency?

Step 2 - Write the most direct baseline

The natural baseline is:

  • fix a start
  • expand the end
  • once the window becomes valid, update the best answer

It works.

But it restarts too much work for every start.

Step 3 - Find the satisfaction rule

Build a need map from the frequencies in t.

Then, while expanding the window, keep a window map.

The window only becomes valid when, for every required character, window[char] >= need[char].

A good way to track that is counting how many character kinds already reached the required frequency.

Step 4 - Expand until satisfied and shrink until violated

This is the heart of the problem:

  • right expands the window
  • once it satisfies everything, left contracts it
  • while it stays valid, keep shrinking

The best window is born during that contraction phase.

The interactive editor needs JavaScript. You can still read the prompt and copy the starter code below.

Starter code

export function minWindow(s: string, t: string): string {
  // your solution here
  return ""
}
solution.ts
Stuck? Hints available.

Haven't solved it yet?

Viewing the solution now may reduce learning. It's worth trying a bit more.

Open the reference solution

Without JavaScript, the reference solution is shown inline instead of in a dialog.

Solution

Final complexity

Time: O(n)

Space: O(charset)

Solution 1 - Test every start and expand until valid

Good baseline because it makes the validity rule visible.

function coversAll(window: Map<string, number>, need: Map<string, number>): boolean {
  for (const [char, amount] of need.entries()) {
    if ((window.get(char) ?? 0) < amount) {
      return false
    }
  }

  return true
}

export function minWindow(s: string, t: string): string {
  const need = new Map<string, number>()

  for (const char of t) {
    need.set(char, (need.get(char) ?? 0) + 1)
  }

  let best = ""

  for (let start = 0; start < s.length; start += 1) {
    const window = new Map<string, number>()

    for (let end = start; end < s.length; end += 1) {
      const char = s[end]
      window.set(char, (window.get(char) ?? 0) + 1)

      if (coversAll(window, need)) {
        const candidate = s.slice(start, end + 1)

        if (best === "" || candidate.length < best.length) {
          best = candidate
        }

        break
      }
    }
  }

  return best
}

It works, but it repeats too much counting for every new start.

Solution 2 - Sliding window with Hash Maps and satisfaction in O(n)

Now the window moves incrementally.

export function minWindow(s: string, t: string): string {
  if (t.length === 0 || s.length < t.length) {
    return ""
  }

  const need = new Map<string, number>()
  for (const char of t) {
    need.set(char, (need.get(char) ?? 0) + 1)
  }

  const window = new Map<string, number>()
  const requiredKinds = need.size
  let satisfiedKinds = 0
  let left = 0
  let bestStart = -1
  let bestLength = Infinity

  for (let right = 0; right < s.length; right += 1) {
    const rightChar = s[right]
    window.set(rightChar, (window.get(rightChar) ?? 0) + 1)

    if (need.has(rightChar) && window.get(rightChar) === need.get(rightChar)) {
      satisfiedKinds += 1
    }

    while (satisfiedKinds === requiredKinds) {
      const currentLength = right - left + 1

      if (currentLength < bestLength || (currentLength === bestLength && left < bestStart)) {
        bestStart = left
        bestLength = currentLength
      }

      const leftChar = s[left]
      window.set(leftChar, window.get(leftChar)! - 1)

      if (need.has(leftChar) && window.get(leftChar)! < need.get(leftChar)!) {
        satisfiedKinds -= 1
      }

      left += 1
    }
  }

  if (bestStart === -1) {
    return ""
  }

  return s.slice(bestStart, bestStart + bestLength)
}

The strong insight here is splitting one window into two phases:

  • expand until satisfied
  • shrink until almost broken

What to say in the interview

A strong short explanation would be:

The problem is not only about containing the characters from t, but also the required frequencies. The baseline tests every start and expands until it finds a valid window. The stronger version uses sliding window with two Hash Maps and a counter for how many character kinds already met the required frequency. Once the window satisfies everything, I shrink from the left to minimize it.

That shows that you:

  • understood the real validity condition
  • explained why contraction exists
  • treated sliding window as an invariant, not a template

When this pattern shows up again

This pattern comes back when you need to:

  • maintain a window with frequency counts
  • distinguish “character kind present” from “enough frequency present”
  • expand until satisfied and shrink until violated

The point is not memorizing Minimum Window Substring.

The point is learning how to model window satisfaction.

Next challenge Rotate Image Previous challenge Trapping Rain Water

Related challenges

Related articles