09Sliding windowArrays and hashing

Minimum Window Substring

The shortest stretch that holds every letter of t: grow until nothing is missing, then shrink.

One Hint Away (@onehintaway). LeetCode 76, Minimum Window Substring (Hard). Pattern: sliding window (post 08) + a counting map (post 05).

Given strings s and t, return the shortest part of s that contains every letter of t, repeats included. If there is none, return "".

Worked example: s = "ABDBCA", t = "ABC" returns "BCA". LeetCode's own example s = "ADOBECODEBANC" returns "BANC".

The idea

  • need[letter]: how many of that letter the window still needs. It starts from t. It can go below 0, which means the window holds extra copies.
  • missing: how many letters of t the window still lacks. The window has everything exactly when missing == 0.
  • Grow on the right until missing == 0. Then shrink from the left while missing stays 0, recording every window along the way.

The walk

stepmovelettermissingneed A, B, Cwindowrecorded
0start31, 1, 1(empty)
1growA3 − 1 = 20, 1, 1A
2growB2 − 1 = 10, 0, 1AB
3growD1 − 0 = 10, 0, 1ABD
4growB1 − 0 = 10, −1, 1ABDB
5growC1 − 1 = 00, −1, 0ABDBCABDBC · 5
6shrinkA out0 + 1 = 11, −1, 0BDBC
7growA1 − 1 = 00, −1, 0BDBCA5, not shorter
8shrinkB out0 + 0 = 00, 0, 0DBCADBCA · 4
9shrinkD out0 + 0 = 00, 0, 0BCABCA · 3
10shrinkB out0 + 1 = 10, 1, 0CA

Answer: "BCA".

Pseudocode

Pseudocode · any language
function minWindow(s, t):
    need = empty map   // default 0
    for each c in t:
        need[c] = need[c] + 1
    missing = length(t)
    left = 0
    bestStart = 0
    bestLen = infinity
    for right from 0 to length(s) - 1:
        c = s[right]
        if need[c] > 0:
            missing = missing - 1
        need[c] = need[c] - 1
        while missing == 0:
            if right - left + 1 < bestLen:
                bestStart = left
                bestLen = right - left + 1
            d = s[left]
            need[d] = need[d] + 1
            if need[d] > 0:
                missing = missing + 1
            left = left + 1
    if bestLen == infinity:
        return ""
    return s from bestStart, bestLen letters

The trap: lowering missing for every letter that joins. It should only drop when that letter was still needed (need[c] > 0 before the decrement); otherwise extra copies, like the second B, look like progress. On the example, lowering it for every letter returns "B", which doesn't even hold an A, instead of "BCA".

Two more mistakes to watch for:

  • Raising missing every time a letter leaves. It should only rise when need[d] becomes positive, which means the window just lost a copy it needed.
  • Recording the window after the shrink instead of before it. You then miss the complete window you just had.

Why record start and length, not the string: copying a substring every time costs extra work. Cut the answer once at the end.

Code

import java.util.*;

class Solution {
    // O(m + n) time, O(k) space
    public String minWindow(String s, String t) {
        Map<Character, Integer> need = new HashMap<>();
        for (char c : t.toCharArray()) {
            need.put(c, need.getOrDefault(c, 0) + 1);
        }
        int missing = t.length();
        int left = 0;
        int bestStart = 0;
        int bestLen = Integer.MAX_VALUE;
        for (int right = 0; right < s.length(); right++) {
            char c = s.charAt(right);
            if (need.getOrDefault(c, 0) > 0) {
                missing = missing - 1;
            }
            need.put(c, need.getOrDefault(c, 0) - 1);
            while (missing == 0) {
                if (right - left + 1 < bestLen) {
                    bestStart = left;
                    bestLen = right - left + 1;
                }
                char d = s.charAt(left);
                need.put(d, need.get(d) + 1);
                if (need.get(d) > 0) {
                    missing = missing + 1;
                }
                left = left + 1;
            }
        }
        if (bestLen == Integer.MAX_VALUE) {
            return "";
        }
        return s.substring(bestStart, bestStart + bestLen);
    }
}

Complexity

VersionTimeExtra space
Try every stretchO(m²) stretches, each checked letter by letterO(k)
Grow and shrinkO(m + n)O(k)

m and n are the lengths of s and t, and k is the number of different letters. Each letter of s joins the window once and leaves at most once.

How it was tested

All four languages return "BCA" on the worked example and "BANC" on LeetCode's example. They were run on 3,005 inputs (3,000 random strings s of length 1 to 12 and t of length 1 to 4 over the letters A to D, plus the two examples and the edge cases "a"/"a", "a"/"aa" and "a"/"b") and matched a brute force that checks every stretch on every one.

Practice

Try these in order. Open the worked example first if you got stuck on the slides.