|
| 1 | +// DIFFICULTY: HARD |
| 2 | +// |
| 3 | +// Given two strings s and t of lengths m and n respectively, return the minimum window substring of s such that every |
| 4 | +// character in t (including duplicates) is included in the window. If there is no such substring, return the empty |
| 5 | +// string "". |
| 6 | +// |
| 7 | +// The testcases will be generated such that the answer is unique. |
| 8 | +// |
| 9 | +// @see {@link https://leetcode.com/problems/minimum-window-substring/} |
| 10 | +export { minWindow }; |
| 11 | + |
| 12 | +// SOLUTION: |
| 13 | +// |
| 14 | +// This can be solved using the sliding window technique with a bunch of extra bookkeeping. The right pointer will be |
| 15 | +// expanded until we have a valid window, then the left pointer will be shrunk to the minimum size window that still |
| 16 | +// satisfies the requirements. |
| 17 | +// |
| 18 | +// COMPLEXITY: |
| 19 | +// |
| 20 | +// Time complexity is O(m + n) because we iterate through both the source and the target strings. |
| 21 | +// |
| 22 | +// The want map is O(n) space complexity because we store character frequency of the target string. The got map is |
| 23 | +// O(m) space complexity because we store character frequency of the source string. Together it's O(m + n). |
| 24 | +function minWindow(s: string, t: string): string { |
| 25 | + // It is not possible to return a minimum window here; the target substring MUST be shorter than the source string. |
| 26 | + if (t.length > s.length) { |
| 27 | + return ''; |
| 28 | + } |
| 29 | + |
| 30 | + // First we need to create a frequency map, so we can easily check if a substring has all the characters in t. This |
| 31 | + // is required because the characters in t can be duplicated, and we require the same number of duplicates in the |
| 32 | + // source substring. |
| 33 | + const want = new Map<string, number>(); |
| 34 | + for (const c of t) { |
| 35 | + const freq = want.get(c) || 0; |
| 36 | + want.set(c, freq + 1); |
| 37 | + } |
| 38 | + |
| 39 | + // Define both pointers to start at the source string, and start by expanding the right pointer. Once a valid window |
| 40 | + // is discovered, we contract the left pointer. |
| 41 | + let left = 0; |
| 42 | + let right = 0; |
| 43 | + |
| 44 | + // Define a map to keep track of the characters we have seen so far in the window. This will let us know when we have |
| 45 | + // a valid window. |
| 46 | + // |
| 47 | + // We also keep track of which characters we've 'gotten' so far. That is, if we have a requirement to contain X |
| 48 | + // number of 'a' characters, and we have seen X characters, then we have 'gotten' that character. When we have |
| 49 | + // 'gotten' all characters in the string, we have a valid window. |
| 50 | + const got = new Map<string, number>(); |
| 51 | + let gotten = 0; |
| 52 | + |
| 53 | + // Keep track of our minimum window substring. The left pointer should point to the start, and the size will tell us |
| 54 | + // the length of the window, allowing us to call s.substring() later. |
| 55 | + // |
| 56 | + // If we run through the algorithm and don't get a valid window, because minSize never got updated, then we return ''. |
| 57 | + let minLeft = 0; |
| 58 | + let minSize = Infinity; |
| 59 | + |
| 60 | + // Begin by expanding the right pointer until we have a valid window. |
| 61 | + while (right < s.length) { |
| 62 | + // Update the frequency of the character that we have got. |
| 63 | + const c = s[right]; |
| 64 | + const freq = got.get(c) || 0; |
| 65 | + got.set(c, freq + 1); |
| 66 | + |
| 67 | + // If it turns out that we got exactly the frequency of character c that we wanted, then the requirement to contain |
| 68 | + // X number of character c has been satisfied. |
| 69 | + if (want.has(c) && want.get(c) === got.get(c)) { |
| 70 | + gotten++; |
| 71 | + } |
| 72 | + |
| 73 | + // If we've gotten all the characters at exactly the right frequency, then we have a valid window. In that case, |
| 74 | + // we can begin shrinking the window to see if it stays valid. |
| 75 | + while (gotten === want.size) { |
| 76 | + const k = s[left]; |
| 77 | + |
| 78 | + // Calculate the size of the window. Note that left and right are INCLUSIVE indexes, so if we want the window |
| 79 | + // size of say [0, 1, 2, 3] with left = 0 and right = 3 (a window size of 4), we should do 3 - 0 + 1 = 4. |
| 80 | + // |
| 81 | + // So make sure to add 1 here to get the correct size. |
| 82 | + const size = right - left + 1; |
| 83 | + if (size < minSize) { |
| 84 | + minSize = size; |
| 85 | + minLeft = left; |
| 86 | + } |
| 87 | + |
| 88 | + // Now try to contract the window by moving the left pointer. When we do this, we have to update the frequency |
| 89 | + // of characters we've gotten so far, and update the gotten count. |
| 90 | + // |
| 91 | + // Update the frequency of the character that we are about to lose. Then if we have got fewer characters than |
| 92 | + // wanted, we decrement the 'gotten' count because we no longer have a valid window. |
| 93 | + got.set(k, got.get(k)! - 1); |
| 94 | + if (want.has(k) && got.get(k)! < want.get(k)!) { |
| 95 | + gotten--; |
| 96 | + } |
| 97 | + |
| 98 | + // After performing the bookkeeping, we can now shrink the window by moving the left pointer. |
| 99 | + left++; |
| 100 | + } |
| 101 | + |
| 102 | + // Keep expanding the right window until we have a valid window. |
| 103 | + right++; |
| 104 | + } |
| 105 | + |
| 106 | + // This means we didn't find any valid window. |
| 107 | + if (minSize === Infinity) { |
| 108 | + return ''; |
| 109 | + } |
| 110 | + |
| 111 | + return s.substring(minLeft, minLeft + minSize); |
| 112 | +} |
0 commit comments