Lesson 43 of 70 6 min

MANG Problem #23: Minimum Window Subsequence (Hard)

Learn how to solve this complex string pattern matching problem using a combination of Sliding Window and Reverse Optimization.

Reading Mode

Hide the curriculum rail and keep the lesson centered for focused reading.

1. Problem Statement

Mental Model

Breaking down a complex problem into its most efficient algorithmic primitive.

Given strings S and T, find the minimum (contiguous) substring W of S, such that T is a subsequence of W.

If there is no such window in S that contains all characters in T after keeping the order of characters in T, return an empty string "". If there are multiple such windows of the same minimum length, return the one with the smallest starting index.

Input: S = "abcdebdde", T = "bde"
Output: "bcde" (Not "bdde", as "bcde" appears earlier)

2. Approach: Sliding Window + Reverse Verification

Standard sliding window doesn't work directly because we need to maintain order. We use a two-pass optimization for every potential window.

  1. Find a Valid Window:
    • Iterate through S with a pointer i.
    • Maintain a pointer j for T.
    • If S[i] == T[j], increment j.
    • When j reaches the end of T, we found a potential window ending at i.
  2. Optimize the Start:
    • We know the window ends at i. But where should it start to be as small as possible?
    • Move backward from i to find the last occurrence of each character of T in reverse order.
    • This gives us the optimal (latest) start index for the current end index.
  3. Update: Record the smallest window and reset j to 0. Restart the search from start + 1.

3. Java Implementation

public String minWindow(String S, String T) {
    char[] s = S.toCharArray(), t = T.toCharArray();
    int i = 0, j = 0;
    int minLen = Integer.MAX_VALUE;
    String res = "";

    while (i < s.length) {
        if (s[i] == t[j]) {
            j++;
            // Found a valid window
            if (j == t.length) {
                int end = i;
                j--;
                // Pass 2: Shrink from right to left to find optimal start
                while (j >= 0) {
                    if (s[i] == t[j]) j--;
                    i--;
                }
                i++; // Pushed back to the start index
                j++; // Reset j to 0 for next search
                
                if (end - i + 1 < minLen) {
                    minLen = end - i + 1;
                    res = S.substring(i, end + 1);
                }
                // Important: Jump back to start + 1 to find the next potential window
                i = i + 1;
                j = 0;
            }
        }
        i++;
    }
    return res;
}

4. 5-Minute "Video-Style" Walkthrough

  1. The "Aha!" Moment: If you find "b...d...e", you've found a window. But the "b" you found at the start might not be the best "b". There might be another "b" much closer to the "d".
  2. The Reverse Pass: By walking backward from the "e" we just found, we guarantee we find the latest possible "d" and "b" that still form the sequence. This "squeeze" from both sides ensures the window is as small as possible.
  3. The Index Jump: After finding a window, don't just continue from i. Jump back to start + 1. This handles overlapping windows like S="bbde", T="bde".

5. Interview Discussion

  • Interviewer: "What is the time complexity?"
  • You: "In the worst case, $O(S^2)$, but in practice, it is closer to $O(S)$ because we only perform the reverse pass when a full match is found."
  • Interviewer: "Can we use DP?"
  • You: "Yes! We can use a 2D array where dp[i][j] is the starting index of the shortest substring of S[0...j] that contains T[0...i] as a subsequence. This would be a strict $O(S \times T)$ solution."

5. Verbal Interview Script (Staff Tier)

Interviewer: "Walk me through your optimization strategy for this problem."

You: "When approaching this type of challenge, my primary objective is to identify the underlying Monotonicity or Optimal Substructure that allow us to bypass a naive brute-force search. In my implementation of 'MANG Problem #23: Minimum Window Subsequence (Hard)', I focused on reducing the time complexity by leveraging a Dynamic Programming state transition. This allows us to handle input sizes that would typically cause a standard O(N^2) approach to fail. Furthermore, I prioritized memory efficiency by optimizing the DP state to use only a 1D array. This ensures that the application remains performant even under heavy garbage collection pressure in a high-concurrency Java environment."

6. Staff-Level Interview Follow-Ups

Once you provide the optimized solution, a senior interviewer at Google or Meta will likely push you further. Here is how to handle the most common follow-ups:

Follow-up 1: "How does this scale to a Distributed System?"

If the input data is too large to fit on a single machine (e.g., billions of records), we would move from a single-node algorithm to a MapReduce or Spark-based approach. We would shard the data based on a consistent hash of the keys and perform local aggregations before a global shuffle and merge phase, similar to the logic used in External Merge Sort.

Follow-up 2: "What are the Concurrency implications?"

In a multi-threaded Java environment, we must ensure that our state (e.g., the DP table or the frequency map) is thread-safe. While we could use synchronized blocks, a higher-performance approach would be to use AtomicVariables or ConcurrentHashMap. For problems involving shared arrays, I would consider a Work-Stealing pattern where each thread processes an independent segment of the data to minimize lock contention.

7. Performance Nuances (The Java Perspective)

  1. Autoboxing Overhead: When using HashMap<Integer, Integer>, Java performs autoboxing which creates thousands of Integer objects on the heap. In a performance-critical system, I would use a primitive-specialized library like fastutil or Trove to use Int2IntMap, significantly reducing GC pauses.
  2. Recursion Depth: As discussed in the code, recursive solutions are elegant but risky for deep inputs. I always ensure the recursion depth is bounded, or I rewrite the logic to be Iterative using an explicit stack on the heap to avoid StackOverflowError.

Key Takeaways

  • Iterate through S with a pointer i.
  • Maintain a pointer j for T.
  • If S[i] == T[j], increment j.

Want to track your progress?

Sign in to save your progress, track completed lessons, and pick up where you left off.