Minimum covering window: track unmet demand
Constructed practice problem; no company attribution. Prerequisites: longest unique window and anagram counts.
Candidate brief
A log inspector receives a text and a multiset of required marker characters. Highlight the shortest contiguous slice containing every requested copy, allowing extra characters. Explain how duplicate requirements change your state before coding.
Write this:
def minimum_covering_window(text, required):
...
| Contract | Decision |
|---|---|
| Input | Python strings text and required; exact code points |
| Output | Shortest half-open (start, end), earliest start on ties; None if impossible |
| Boundaries | Empty requirement returns (0, 0); surplus copies are allowed |
| Invalid input | Either non-string argument raises ValueError |
| Excluded | Reordering characters or matching tokens across noncontiguous positions |
Optional refresher · the underlying tool
A covering window may contain characters the request does not need. Represent the outstanding demand as counts, not a set:
from collections import Counter
need = Counter("AAB") # A is required twice
print(need["A"]) # 2
With text "AAAB" and requirement "AAB", the shortest cover is "AAB" at the end. Expanding earns characters; shrinking is safe only while every required count remains satisfied.
A design choice worth saying aloud
Keep the requested counts separate from the mutable remaining_by_character. The latter answers how many more of each character the current window owes; a negative count means surplus, not failure. A single missing total can tell you when to shrink, while the count map tells you which removal would break coverage.
What the interviewer expects
The interviewer gives you the scenario and the contract above. Explain what a successful call returns, walk through one example below, and name what your state means before choosing a data structure.
Done means: Shortest half-open (start, end), earliest start on ties; None if impossible.
Now predict each output before looking at the reference; invalid input should leave any existing state unchanged unless the contract says otherwise.
Test-case scenarios to settle before coding
01 · Representative
- Input / starting state
"ABAAC", required"AAC"- Expected result
(2, 5)for"AAC"
What it is testing: Required multiplicities drive validity.
02 · Empty requirement
- Input / starting state
- any text, required
"" - Expected result
(0, 0)
What it is testing: The empty need is already satisfied.
03 · Impossible multiplicity
- Input / starting state
"ab", required"aa"- Expected result
None
What it is testing: Presence without enough copies is insufficient.
04 · Surplus
- Input / starting state
"AAABC", required"AC"- Expected result
(2,5)for"ABC"
What it is testing: Extra required characters must not inflate unmet demand.
05 · Tie
- Input / starting state
"ABXAB", required"AB"- Expected result
(0,2)for the first"AB"
What it is testing: State the deterministic result before coding.
06 · Invalid
- Input / starting state
- either argument is not a string
- Expected result
ValueError
What it is testing: Validation precedes scanning.
For each case, show which branch or state change produces that result.
minimum_covering_window("ABAAC", "AAC") == (2, 5) highlights "AAC".
minimum_covering_window("ab", "aa") is None; minimum_covering_window("", "") == (0, 0).
minimum_covering_window("abc", None) raises ValueError.
Before opening the explanation, restate the contract, trace the smallest useful example, implement a baseline, and identify the repeated work. Then implement your improvement independently and derive tests from the contract. Say what your state means before saying which data structure stores it.
Worked lesson, changed requirements, and reference
Make validity a cheap question
A baseline enumerates each start and extends a count map until all requirements
are covered. Repeating that scan costs O(n²) updates plus validity checks. A set
is insufficient: required AAC needs two As. The reusable method is to name
the expensive question, then maintain its answer incrementally. Let need[c]
be required count minus current-window count, and missing be the number of
required occurrences still absent. Negative need means harmless surplus.
| Read / window | need A | need C | missing | Action |
|---|---|---|---|---|
| initially | 2 | 1 | 3 | expand |
| A | 1 | 1 | 2 | expand |
| AB | 1 | 1 | 2 | ignore irrelevant B |
| ABAA | -1 | 1 | 1 | extra A is surplus |
| ABAAC | -1 | 0 | 0 | valid; start shrinking |
| BAAC, then AAC | 0 | 0 | 0 | improve to [2, 5) |
| AC after removing A | 1 | 0 | 1 | invalid; expand again |
When adding a required character, decrement missing only if its prior need was
positive, then decrement need. When removing one, increment need first; if it
becomes positive, increment missing. This update order distinguishes a required
copy from surplus. Non-required characters need no map entries.
While missing is zero, record the candidate and advance left. This visits the
shortest valid window ending at each right edge. A start discarded during
shrinking cannot improve at a later right edge: its window would only get
longer. Both boundaries move forward at most n times, giving expected O(n + m)
time and O(k) auxiliary space for k required code points. The returned indices
use O(1) space. Strictly shorter updates preserve the earliest tie. This proof
depends on coverage being preserved by expansion; arbitrary window predicates
do not automatically support two pointers.
Follow-up 1: matches must appear in required order
Predict what breaks when required="AC" means a subsequence in that order.
Inventory sees CA as valid although the sequence is impossible.
| Text prefix | Coverage interpretation | Ordered interpretation |
|---|---|---|
| C | A still missing | no A has started a match |
| CA | complete inventory | A starts a future match |
| CAC | complete inventory | AC at [1, 3) completes |
Use state for progress through the required sequence, such as the latest viable start for each matched prefix. Update matching states backward per text character so one occurrence cannot advance several repeated required positions. An O(nm) dynamic program is a defensible baseline; the count invariant is gone.
Follow-up 2: several requirement sets share one text
Independent demand states remain correct but cost O(qn + total demand size). Do not claim one universal left boundary: different queries become valid at different positions. A senior candidate derives the exact update order and tests multiplicity and ties. A lead candidate sets query-count and retained-text budgets and measures whether shared preprocessing is justified by the workload.
Run and check
From the repository root:
cd curriculum/01-code/02-data-structures-algorithms/problems/05-minimum-covering-window
python -m unittest -v test_solution.py
Reference implementation (download file, source below) · Contract and oracle tests (download file, source below). Read the tests after your attempt. A green reference suite verifies the supplied implementation; it does not demonstrate independent transfer. Reimplement one follow-up with the reference closed and explain which old invariant no longer holds.
def minimum_covering_window(text, required):
if not isinstance(text, str) or not isinstance(required, str):
raise ValueError("arguments must be strings")
if not required:
return 0, 0
need = {}
for char in required:
need[char] = need.get(char, 0) + 1
missing = len(required)
left = 0
best = None
for right, char in enumerate(text):
if char in need:
if need[char] > 0:
missing -= 1
need[char] -= 1
while missing == 0:
if best is None or right + 1 - left < best[1] - best[0]:
best = left, right + 1
departing = text[left]
if departing in need:
need[departing] += 1
if need[departing] > 0:
missing += 1
left += 1
return best
import itertools
import unittest
from collections import Counter
from solution import minimum_covering_window
class CoverTests(unittest.TestCase):
def test_examples_and_ties(self):
self.assertEqual(minimum_covering_window("ABAAC", "AAC"), (2, 5))
self.assertEqual(minimum_covering_window("abxba", "ab"), (0, 2))
self.assertIsNone(minimum_covering_window("ab", "aa"))
self.assertEqual(minimum_covering_window("", ""), (0, 0))
self.assertEqual(minimum_covering_window("🙂x🙂é", "🙂é"), (2, 4))
with self.assertRaises(ValueError):
minimum_covering_window("abc", None)
def test_all_substrings_oracle(self):
for n in range(6):
for chars in itertools.product("ab", repeat=n):
text = "".join(chars)
for required in ["", "a", "b", "aa", "ab", "aab"]:
candidates = [(i, j) for i in range(n + 1) for j in range(i, n + 1)
if not (Counter(required) - Counter(text[i:j]))]
expected = min(candidates, key=lambda pair: (pair[1] - pair[0], pair[0])) if candidates else None
self.assertEqual(minimum_covering_window(text, required), expected)
if __name__ == "__main__":
unittest.main()