Problem Walkthrough

Min Window Subsequence LeetCode 727

A complete walkthrough of LeetCode 727 Minimum Window Subsequence covering the forward-backward two-pointer approach — where a forward scan finds the first position in S1 where S2 ends as a subsequence, then a backward scan shrinks the window to its minimum start — and the DP approach that uses a 2D table to track starting indices, both solving the problem of finding the smallest contiguous substring of S1 that contains S2 as a subsequence.

9 min read|

Min Window Subsequence

LeetCode 727

Problem Overview — Find the Minimum Window Containing T as a Subsequence

LeetCode 727 Minimum Window Subsequence gives you two strings S1 and S2, and asks you to find the minimum-length contiguous substring of S1 such that S2 is a subsequence of that substring. A subsequence means the characters of S2 appear in the substring in order, but not necessarily contiguously. If no such window exists, return an empty string.

This problem is distinct from Minimum Window Substring (LeetCode 76), which only requires each character of T to appear in the window with the right frequency. Here, the order matters strictly: the characters of S2 must appear in order within the chosen window. This ordering constraint makes a pure sliding window insufficient and requires either a forward-backward two-pointer strategy or a DP approach.

Understanding the problem precisely helps avoid common mistakes. The window is a contiguous substring of S1, but S2 is matched as a subsequence within that window. If S2 = "abc" and S1 = "axbycz", the window "axbycz" works because a, b, c appear in order. But "axbyc" also works and is shorter. The goal is to find the globally shortest such window across all starting positions in S1.

  • 1 <= s1.length <= 2 * 10^4
  • 1 <= s2.length <= 100
  • S1 and S2 consist of lowercase English letters only
  • S2 must appear as a subsequence (in order, not necessarily contiguous) within the returned window
  • Return empty string if no valid window exists in S1

Brute Force — Greedy Match from Each Starting Position

The brute force approach tries every possible starting position in S1. For each index i in S1, use a greedy forward scan: walk through S1 from position i, trying to match all characters of S2 in order. When all characters of S2 are matched, record the window S1[i..j] and update the global minimum if this window is shorter than the current best.

This approach runs in O(m * n) time in the worst case, where m = len(S1) and n = len(S2), because for each of the m starting positions in S1 we may scan up to m characters trying to match n characters of S2. The space complexity is O(1) beyond storing the result. While correct, it may be too slow for the given constraints of S1 up to 20,000 characters.

The brute force correctly handles all cases: multiple valid windows, windows starting at the first or last character of S1, and the empty-string return when no window exists. It establishes the baseline logic that more efficient approaches optimize. Recognizing that the brute force does redundant work — restarting from each i independently — motivates the forward-backward trick that avoids that redundancy.

💡

The Forward-Backward Trick Avoids Redundant Scanning

Instead of restarting from every index in S1, the forward-backward technique first scans forward through S1 to find the position where S2 is fully matched as a subsequence. Once that end position is found, it scans backward from that end, matching S2 in reverse, to find the earliest valid start. This gives the minimum window ending at that position in a single backward pass — no need to try every possible start independently.

Forward-Backward Two Pointer — Shrink to Minimum Start

The forward-backward two-pointer approach works in two passes per candidate window. In the forward pass, use a pointer j into S2 starting at 0. Walk through S1 with pointer i; each time S1[i] == S2[j], advance j. When j reaches len(S2), all characters of S2 have been matched in order — record i as the end of a candidate window.

In the backward pass, start at the recorded end position and walk backward through S1. Use a pointer k into S2 starting at len(S2) - 1. Each time S1[i] == S2[k], decrement k. When k reaches -1, all characters of S2 have been matched in reverse — the current i is the earliest valid start for a window ending at the recorded end. The window is S1[i..end].

After recording this candidate window, continue the outer loop from end + 1 to search for the next candidate window. Track the global minimum window across all candidates. This approach runs in O(m * n) worst-case time but is typically much faster in practice because the backward pass quickly identifies the true minimum start without redundant forward rescanning.

  1. 1Initialize result = "", j = 0 (pointer into S2), i = 0 (pointer into S1)
  2. 2Forward pass: walk i through S1; when S1[i] == S2[j], increment j
  3. 3When j == len(S2), all of S2 matched — record end = i
  4. 4Backward pass: walk i backward from end; when S1[i] == S2[k] (k from len(S2)-1), decrement k
  5. 5When k == -1, record start = i; window = S1[start..end+1]
  6. 6Update result if this window is shorter than current best
  7. 7Continue outer loop from end + 1 to find next candidate
  8. 8Return result (empty string if no window found)

DP Approach — Track Starting Index Through the Match

The DP approach uses a 2D table dp[i][j] where dp[i][j] represents the starting index in S1 at which the first j characters of S2 are matched as a subsequence ending at position i in S1. The transition is: if S1[i] == S2[j], then dp[i][j] = dp[i-1][j-1] (propagate the start from where the previous character was matched); otherwise dp[i][j] = dp[i-1][j] (carry forward the best start seen so far without advancing j).

Initialize dp[i][0] = i for all i, meaning zero characters of S2 matched starting at position i. When dp[i][n] (where n = len(S2)) is not infinity, a complete match of S2 ends at position i in S1 starting from dp[i][n]. The window is S1[dp[i][n]..i+1]. Scan all i to find the minimum-length such window.

Space can be optimized to O(n) by only keeping one row of the DP table at a time, since each row only depends on the previous row. The DP formulation is more systematic and handles edge cases cleanly, but the two-pointer approach is typically preferred in interviews for its lower constant factors and O(1) space.

ℹ️

DP vs Two-Pointer: Same Time Complexity, Different Space

Both the DP approach and the two-pointer approach are O(m*n) time in the worst case, where m = len(S1) and n = len(S2). The DP approach uses O(m*n) space for the full table (or O(n) with row compression), while the two-pointer approach uses O(1) extra space. In practice, the two-pointer approach is faster because it does not fill an entire 2D table — it only scans the portions of S1 that contribute to valid windows. For interview settings, the two-pointer approach is usually the preferred answer.

Code Walkthrough — Python and Java Two-Pointer Implementation

Python two-pointer: def minWindow(s1, s2): result = ""; n1, n2 = len(s1), len(s2); i = 0. While i < n1: j = 0; while i < n1 and j < n2: if s1[i] == s2[j]: j += 1; i += 1. If j == n2: end = i - 1; k = n2 - 1; while k >= 0: if s1[i-1] == s2[k]: k -= 1; i -= 1. start = i; window = s1[start:end+1]; if not result or len(window) < len(result): result = window; i = end + 1. Return result. O(m*n) worst case, O(1) space.

Java two-pointer: public String minWindow(String s1, String s2) { String result = ""; int n1 = s1.length(), n2 = s2.length(), i = 0; while (i < n1) { int j = 0; while (i < n1 && j < n2) { if (s1.charAt(i) == s2.charAt(j)) j++; i++; } if (j == n2) { int end = i - 1; int k = n2 - 1; while (k >= 0) { if (s1.charAt(i-1) == s2.charAt(k)) k--; i--; } int start = i; String window = s1.substring(start, end + 1); if (result.isEmpty() || window.length() < result.length()) result = window; i = end + 1; } } return result; }

Both implementations follow the same structure: an outer loop driving forward scans, an inner forward scan matching S2 left-to-right, a backward scan matching S2 right-to-left to find the minimum start, and advancement to end + 1 for the next iteration. The key correctness point is that the backward pass decrements both the S1 pointer and the S2 reverse pointer simultaneously, stopping when all of S2 has been matched in reverse.

⚠️

Backward Pass Must Match S2 in Reverse — Not Forward Again

A common mistake is to run a second forward pass from the start of the window to find the earliest valid start. This produces an incorrect or non-minimum window. The backward pass must match S2 in reverse order — starting from the last character of S2 and working backward. Matching forward again from the beginning of the window will find the first occurrence of S2[0] but that is not necessarily the earliest start that produces the minimum-length window ending at the recorded end position.

Ready to master algorithm patterns?

YeetCode flashcards help you build pattern recognition through active recall and spaced repetition.

Start practicing now