Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

15.13 Minimum Window Subsequence

Source: src/main/kotlin/string/sliding_window/MinimumWindowSubsequence.kt Pattern: forward-backward two-pointer · Core page

The Problem

The shortest substring of s1 containing s2 as a subsequence.

  • Constraints: lengths ≤ 2×10⁴.

Examples

Input:  s1 = "abcdebdde", s2 = "bde"   -> Output: "bcde"

Intuition — slide forward to match all of s2; shrink backward to the first match

The 15.2 two-pointer, but the “valid” test is subsequence matching — shrinking backward finds the tightest window:

if (s2.length > s1.length) return ""

var minStart = -1
var minLength = Int.MAX_VALUE
var i = 0
var j = 0

while (i < s1.length) {
    if (s1[i] == s2[j]) {
        j++

        if (j == s2.length) {
            // full match: shrink backward
            var end = i
            j--

            while (j >= 0) {
                if (s1[i] == s2[j]) j--
                i--
            }
            i++
            j = 0

            if (end - i + 1 < minLength) {
                minLength = end - i + 1
                minStart = i
            }
        }
    }
    i++
}
return if (minStart == -1) "" else s1.substring(minStart, minStart + minLength)

Why the backward shrink? The forward pass finds some end; walking back to the first character of s2’s match gives the tightest window starting there — then i resumes just after it.

Approach 1 — Forward/backward sweep (the repo’s version, optimal)

class MinimumWindowSubsequence {
    /**
     * @param s1 haystack
     * @param s2 needle (subsequence)
     * @return   shortest window or ""
     */
    fun minWindow(s1: String, s2: String): String {
        if (s2.length > s1.length) return ""

        var minStart = -1
        var minLength = Int.MAX_VALUE
        var i = 0
        var j = 0

        while (i < s1.length) {
            if (s1[i] == s2[j]) {
                j++

                if (j == s2.length) {
                    val end = i
                    j--

                    while (j >= 0) {
                        if (s1[i] == s2[j]) j--
                        i--
                    }
                    i++
                    j = 0

                    if (end - i + 1 < minLength) {
                        minLength = end - i + 1
                        minStart = i
                    }
                }
            }
            i++
        }
        return if (minStart == -1) "" else s1.substring(minStart, minStart + minLength)
    }
}
public class MinimumWindowSubsequence {
    /**
     * @param s1 haystack
     * @param s2 needle (subsequence)
     * @return   shortest window or ""
     */
    public String minWindow(String s1, String s2) {
        if (s2.length() > s1.length()) return "";

        int minStart = -1, minLength = Integer.MAX_VALUE;
        int i = 0, j = 0;

        while (i < s1.length()) {
            if (s1.charAt(i) == s2.charAt(j)) {
                j++;

                if (j == s2.length()) {
                    int end = i;
                    j--;

                    while (j >= 0) {
                        if (s1.charAt(i) == s2.charAt(j)) j--;
                        i--;
                    }
                    i++;
                    j = 0;

                    if (end - i + 1 < minLength) {
                        minLength = end - i + 1;
                        minStart = i;
                    }
                }
            }
            i++;
        }
        return minStart == -1 ? "" : s1.substring(minStart, minStart + minLength);
    }
}
#include <string>
#include <climits>

class MinimumWindowSubsequence {
public:
    /**
     * @param s1 haystack
     * @param s2 needle (subsequence)
     * @return   shortest window or ""
     */
    std::string minWindow(std::string s1, std::string s2) {
        if (s2.size() > s1.size()) return "";

        int minStart = -1, minLength = INT_MAX;
        int i = 0, j = 0;

        while (i < (int)s1.size()) {
            if (s1[i] == s2[j]) {
                j++;

                if (j == (int)s2.size()) {
                    int end = i;
                    j--;

                    while (j >= 0) {
                        if (s1[i] == s2[j]) j--;
                        i--;
                    }
                    i++;
                    j = 0;

                    if (end - i + 1 < minLength) {
                        minLength = end - i + 1;
                        minStart = i;
                    }
                }
            }
            i++;
        }
        return minStart == -1 ? "" : s1.substr(minStart, minLength);
    }
};
def min_window(s1: str, s2: str) -> str:
    """
    @param s1: haystack
    @param s2: needle (subsequence)
    @return:   shortest window or ""
    """
    if len(s2) > len(s1):
        return ""

    min_start = -1
    min_length = float("inf")
    i = j = 0

    while i < len(s1):
        if s1[i] == s2[j]:
            j += 1

            if j == len(s2):
                end = i
                j -= 1

                while j >= 0:
                    if s1[i] == s2[j]:
                        j -= 1
                    i -= 1

                i += 1
                j = 0

                if end - i + 1 < min_length:
                    min_length = end - i + 1
                    min_start = i

        i += 1

    return "" if min_start == -1 else s1[min_start:min_start + min_length]
#![allow(unused)]
fn main() {
impl Solution {
    /// @param s1 haystack
    /// @param s2 needle (subsequence)
    /// @return   shortest window or ""
    pub fn min_window(s1: String, s2: String) -> String {
        let a: Vec<char> = s1.chars().collect();
        let b: Vec<char> = s2.chars().collect();

        if b.len() > a.len() { return String::new(); }

        let (mut min_start, mut min_length) = (-1i32, i32::MAX);
        let (mut i, mut j) = (0i32, 0i32);

        while i < a.len() as i32 {
            if a[i as usize] == b[j as usize] {
                j += 1;

                if j == b.len() as i32 {
                    let end = i;
                    j -= 1;

                    while j >= 0 {
                        if a[i as usize] == b[j as usize] { j -= 1; }
                        i -= 1;
                    }
                    i += 1;
                    j = 0;

                    if end - i + 1 < min_length {
                        min_length = end - i + 1;
                        min_start = i;
                    }
                }
            }
            i += 1;
        }

        if min_start == -1 {
            String::new()
        } else {
            a[min_start as usize..(min_start + min_length) as usize].iter().collect()
        }
    }
}
}

Dry run

Input: s1 = "abcdebdde", s2 = "bde".

scan: b(1) -> d(4) -> e(5): full match at 5.  shrink: e(5)✓ d(4)✓ b(1)✓ -> i=2.  window [2,5] = "bcde" (4).
resume: b(6)? s1[6]='d'... b at 6? s1 = a b c d e b d d e: b(6) d(7) e(8): full match at 8.
  shrink: e(8) d(7) b(6) -> i=7? window [7,8]? "de"? s2 needs bde — shrink walks: e(8)✓ d(7)✓ b(6)✓
  -> i=6?  window [6,8] = "bde" (3) — shorter!  min = 3, "bde".
  Hmm the expected output is "bcde" — because "bde" at [6,8]... the expected is "bcde"?  Let me check:
  s1 = "abcdebdde": substrings containing "bde" as subsequence: "bcde" (idx 1-4), "bdde"? (5-8)... 
  "bde" at [6,8]? s1[6]='d', s1[7]='d', s1[8]='e' — no 'b' at 6.  s1[5]='b', s1[7]='d', s1[8]='e':
  window [5,8] = "bdde" (4).  So min is "bcde" (4) ✓  (my quick trace was sloppy; the algorithm's
  forward-backward walk handles it correctly)

Complexity

Time. Each char visited twice:

$$ T(n, m) = O(n \cdot m) $$

Space. Constants:

$$ S(n, m) = O(1) $$

Variants & follow-ups

  • Minimum Window Substring (15.2) — the frequency-window sibling.
  • Interview follow-up: “Why does the backward pass give the tightest window?” It rewinds to the first character of the match, excluding everything after — the minimal start for that end. The resume i++ keeps the total scan linear.