Jc-alt logo
jc

LeetCode: Two Pointers II Sliding Window

LeetCode: Two Pointers II Sliding Window
86 min read
data structures and algorithms

Sliding Window Intro

Leetcode problems with elegant solutions using the sliding window technique.

What is Sliding Window

Sliding Window is a technique used for iterating over a subset of data within a larger structure (like arrays or strings) by maintaining a moving window that can expand, contract, or shift across the input.

There are two main types of sliding windows:

  1. Fixed size (constant length) window
  2. Variable size (dynamic length) window

Sliding window often replaces nested loops by reusing computation from the previous window to achieve better efficiency.

Why Use Sliding Window

Sliding Window is ideal for problems that involve:

  • Finding optimal subarray/substring
  • Summation or counting within a range
  • Tracking a condition inside a window

It typically reduces time complexity from O(n2) (naive nested loops) to O(n) since each element is processed at most twice (entering/exiting the window).

Sliding Window Application: Fixed Size Window

Fixed size windows help maintain a constant window of k elements while scanning through a sequence.

Ex: Maximum sum of any subarray of size k

    def maxSumSubarray(nums, k):
        window_sum = sum(nums[:k])
        max_sum = window_sum
        
        for i in range(k, len(nums)):
            window_sum += nums[i] - nums[i - k]
            max_sum = max(max_sum, window_sum)
        
        return max_sum

    # maxSumSubarray([1, 4, 2, 10, 2, 3, 1, 0, 20], 4) = 24

Sliding Window Application: Variable Size Window

Variable size windows expand and shrink dynamically depending on whether some condition or constraint is met (e.g., substring uniqueness, sum ≤ target).

Ex: Longest substring without repeating characters

    def lengthOfLongestSubstring(s: str) -> int:
        char_index = {}
        left = max_len = 0
        
        for right in range(len(s)):
            if s[right] in char_index and char_index[s[right]] >= left:
                left = char_index[s[right]] + 1
            
            char_index[s[right]] = right
            max_len = max(max_len, right - left + 1)
        
        return max_len

    # "abcabcbb" → lengthOfLongestSubstring = 3

121. Best Time to Buy and Sell Stock ::3:: - Easy

Topics: Array, Dynamic Programming, Sliding Window

Intro

You are given an array prices where prices[i] is the price of a given stock on the ith day. You want to maximize your profit by choosing a single day to buy one stock and choosing a different day in the future to sell that stock. Return the maximum profit you can achieve from this transaction. If you cannot achieve any profit, return 0.

Example InputOutput
prices = [7,1,5,3,6,4]5
prices = [7,6,4,3,1]0

Constraints:

1 ≤ prices.length ≤ 105

0 ≤ prices[i] ≤ 104

Abstraction

Given an array of stock values, find the highest profit possible.

Pseudocode

Sol 1: N Size Window (Left Right Curr Buy Today Sell Prices)
1. (left, right = 0, 0)
1. (maxProfit = 0)
2. while right < n:
   a. currBuyPrice = prices[left] 
   b. todaySellPrice = prices[right]
   c. if currBuyPrice < todaySellPrice:
         maxProfit = max(maxProfit, (todaySellPrice - currBuyPrice))
   d. else:
         left = right
   e. right += 1
3. return maxProfit

Sol 3: Best Buy Sell Price Up To Today State Compression Variables
1. (currBuyPrice = -prices[0])
2. (maxProfit = 0)
2. For i in range(1, n):
   a. currBuyPrice = max(currBuyPrice, -prices[i])
   b. maxProfit = max(maxProfit, currBuyPrice + prices[i])
3. return maxProfit

Solution 1: [Sliding Window] N Size Window Tracking Best Buy And Today Sell With Left Right - Sliding Window/Variable Size Window

    def maxProfit(self, prices: List[int]) -> int:

        # Sliding Window (Variable Size)

        # Stock Representation:
        #   - Window will grow from 0 to n as we iterate
        #   - Left and right represent: 
        #        left: best buy day so far (lowest price seen)
        #        right: current sell day candidate (highest prices seen)
        #   - Treat each day as a potential sell day

        # Idea:
        #   - Always buy before you sell
        #   - If current price is higher than buy price, compute profit
        #   - If current price is lower than buy price, we have found better buy price, reset window

        n = len(prices)

        # Sliding Window Representation:
        # There is no window representation,
        # the window simply grows to include the entire array
        # with our left and right tracking the best buy and sell prices within the window

        # Max profit seen so far given our best buy and best sell
        maxProfit = 0

        # Left: lowest price so far
        # Right: todays price
        left = 0
        right = 0

        # tc: O(n)
        while right < n:

            # Grab current best prices
            currBuyPrice = prices[left]
            todaySellPrice = prices[right]

            # Check: 
            # can we make a profit
            if currBuyPrice < todaySellPrice:
                
                # Calculate profit and compare
                profit = todaySellPrice - currBuyPrice
                maxProfit = max(maxProfit, profit)

            # Check: 
            # is this a better buy
            else:
                # update best buy day
                left = right

            # Iterate to next price
            right += 1

        # overall: tc O(n)
        # overall: sc O(1)
        return maxProfit

Solution 2: [Dynamic Programming] Track Best Buy And Sell Up To ith Day Complete Table - Sliding Window/Variable Size Window

    def maxProfit(self, prices: List[int]) -> int:

        # Dynamic Programming (Full DP Table)

        # Window Representation:
        #   - Maintain window within [i]
        #   - currBuyPrice[i][0]: best buy day so far:
        #       we represent this by min(-currBuyPrice) for easier later calculations
        #
        #   - currBuyPrice[i][1]: best profit price up to day i    

        # Idea:
        #   - Always buy before you sell (<= index 1)
        #   - Check if new buy day found
        #   - Check if new best profit found


        # Empty check: no profit to be made
        if not prices:
            return 0

        n = len(prices)

        # Initialize DP:
        # sc: O(n)
        dp = []
        
        # Fill dp table:
        #   - dp[i][0]: best buy price up to day i
        #   - dp[i][1]: best profit price up to day i
        # tc: O(n)
        for i in range(n):
            dp.append([0, 0])

        
        # Initialize DP:
        # i = day
        # dp[i][0]: set best buy price to day 0
        # dp[i][1]: set best profit made up to day 0 
        #           (cannot buy and sell on same day, so profit on day 0 is 0)
        # tc: O(1)
        dp[0][0] = -prices[0]
        dp[0][1] = 0

        # start from second day
        # tc: iterate across n days O(n)
        for i in range(1, n):

            # Check: grab todays buy price
            # Use profit to explain our buy price:
            #     profit = hold + todaySellPrice
            #            = (-3) + 10
            #            = 7       
            todayPrice = -prices[i]

            # Check: grab best buy price up to yesterday
            bestBuyUpToYesterday = dp[i-1][0]

            # Check: grab new buy price
            dp[i][0] = max(bestBuyUpToYesterday, todayPrice)     

            # Check: todays profit
            todayProfit = bestBuyUpToYesterday + prices[i]
            
            # Check: grab new best profit, cannot sell today, so use yesterdays price    
            dp[i][1] = max(dp[i-1][1], todayProfit)


        # overall: tc O(n)
        # overall: sc O(n)
        return dp[-1][1]

Solution 3: [Dynamic Programming] Track Best Buy And Sell Up To Today State Compression Variables - Sliding Window/Variable Size Window

    def maxProfit(self, prices: List[int]) -> int:

        # Dynamic Programming (State Compression)

        # Stock Representation:
        #   - Treat each day as potential sell day
        #   - Maintain a state tracking where:
        #        currBuyPrice: best buy price so far (lowest price seen)
        #        profit: most profit made so far (highest profit made using each day as a sell day)

        # Idea:
        #   - Always buy before you sell
        #   - If current price is higher than buy price, compute profit
        #   - If current price is lower than buy price, we have found better buy price, reset window

        n = len(prices)

        # set up compressed dynamic programming table first iteration:        
        # [i][0] -> min buying price up to day i
        # [i][1] -> max profit up to day i

        # Initialize Compressed DP:
        #   - min price up to day 0
        #   - max profit up to day 0
        currBuyPrice = -prices[0]
        maxProfit = 0

        # start from second day
        # tc: iterate over n days O(n)
        for i in range(1, n):
            
            # Put buy price as negative,
            # to ensure we always subtract it from out buy price
            #     profit = hold + todaySellPrice
            #            = (-3) + 10
            #            = 7       
            todayPrice = -prices[i]

            # Check: compare best buy price up to yesterday to today buy price
            currBuyPrice = max(currBuyPrice, todayPrice)

            todayProfit = currBuyPrice + prices[i]

            # profit = hold + todaySellPrice
            #        = (-3) + 10
            #        = 7
            maxProfit = max(maxProfit, todayProfit)  

        # overall: tc O(n)
        # overall: sc O(1)
        return maxProfit

219. Contains Duplicate II ::2:: - Easy

Topics: Array, Hash Table, Sliding Window

Intro

Given an integer array nums and an integer k, return true if there are two distinct indices i and j in the array such that nums[i] == nums[j] and abs(i - j) lte k.

Example InputOutput
nums = [1,2,3,1], k = 3true
nums = [1,0,1,1], k = 1true
nums = [1,2,3,1,2,3], k = 2false

Constraints:

1 ≤ nums.length ≤ 10^5

-10^9 ≤ nums[i] ≤ 10^9

0 ≤ k ≤ 10^9

Abstraction

Given a list of numbers, and a window range of k, determine if there is a duplicate anywhere in the array within of k steps of each other.

Pseudocode

Sol 1: MRI HashMap Checking Duplicates In K Window Based On Left Right Pointers
1. if n <= 1 or k <= 0: 
    a. return false
2. (mri = {})
3. For right in range(n):
    a. if nums[right] in mri:
         left = right - k
         prevIndex = mri[nums[right]]
         if left <= prevIndex <= right: 
             return true
   b. mri[nums[right]] = right
4. return false

Sol 2: Window HashSet K Tracking Nums Within Window
1. if n <= 1 or k <= 0: 
    return false
2. (window = {}) 
3. (left = 0)
3. while right < n:
    a. if nums[right] in window: 
         return true
    b. window.add(nums[right])
    c. if k < len(window):
         window.remove(nums[left])
         left += 1
    d. right += 1
4. return false

Solution 1: [Sliding Window] MRI HashMap Checking Duplicates In K Window Based On Left Right Pointers - Sliding Window/Variable Size Window

    def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool:
        
        # Sliding Window + HashMap (Fixed Size Window):

        # Window Representation:
        #   - HashMap for each number representing (val -> most recent index)

        # Idea:
        #   - Slide window over num list and add index at right boundary
        #   - Check mri if we have a duplicate of the current element,
        #     and if a duplicate exists within the k window [left, right]
    
        # Track more recent index appearance for num
        # sc: O(n)
        mri = {}

        # Sliding Window Variables
        n = len(nums)
        right = 0

        # Empty Case:
        # if not enough days to have a duplicate
        if n <= 1 or k <= 0:
            return False

        # tc: O(n)
        for right in range(n):

            currNum = nums[right]

            # Check: if num has already been seen
            # tc: O(1)
            if currNum in mri:

                #     k distance
                #  | ------------ |
                # left          right 

                # Index Distance No More Than k Apart (not element count):
                # the furthest left index that is still within k distance
                left = right - k

                # Check: 
                # if previous num copy is within current window, duplicate exists
                if left <= mri[currNum] <= right:
                    return True

            # Implies: no duplicate exists
            # Then: update mri for num
            mri[currNum] = right

        # No duplicates found within distance k

        # overall: tc O(n)
        # overall: sc O(min(n, k))
        return False

Solution 2: [Sliding Window] Window HashSet K With Left Right Boundaries And Set Length Early Exit - Sliding Window/Variable Size Window

    def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool:
        
        # Sliding Window HashSet Right Left Representation (Fixed Size Window):

        # Window Representation:
        #   - Set() of width k spanning [left, right]
        #   - Numbers enter and exit window via left and right

        # Idea:
        #   - Slide fixed size window over list
        #   - Add element at right, remove element from left
        #   - Check if num we added is already within window boundary

        n = len(nums)

        # Empty Case:
        # Single list or window of length k cannot have duplicates
        if n <= 1 or k <= 0:
            return False

        # Sliding Window Data:
        # sc: O(k)
        window = set()

        # Sliding Window Boundaries:
        left = 0
        right = 0

        # Early exit: 
        # all elements are unique, no duplicates
        if len(set(nums)) == n:
            return False

        # tc: O(n)
        while right < n:

            # Check: 
            # if duplicate num exist in window
            if nums[right] in window:
                return True

            # Window Extend:
            # right index enters window:
            window.add(nums[right])
            right += 1

            # Window Shrink:
            # if we've fixed width of window,
            # left index exits window:
            if k < len(window):
                window.remove(nums[left])
                left += 1



        # No duplicates found within k window

        # overall: tc O(n)
        # overall: sc O(min(n, k))
        return False

239. Sliding Window Maximum ::3:: - Hard

Topics: Array, Queue, Sliding Window, Heap (Priority Queue), Monotonic Queue

Intro

You are given an array of integers nums, there is a sliding window of size k which is moving from the very left of the array to the very right. You can only see the k numbers in the window. Each time the sliding window moves right by one position. Return the max sliding window.

Example InputOutput
nums = [1,3,-1,-3,5,3,6,7], k = 3Output: [3,3,5,5,6,7]
nums = [1], k = 1[1]

Constraints:

1 ≤ nums.length ≤ 105

-104 ≤ nums[i] ≤ 104

1 ≤ k ≤ nums.length

Abstraction

Given list of numbers, and sliding window size k, return array of max element for each window.

Pseudocode

Sol 3: Fixed Size K Window With Indices Monotonic Decreasing Dequeue
1. (n = len(nums))
2. (right = 0)
2. (res = [])
3. (dq = deque())
4. while right < n:
    a. while dq and nums[dq[-1]] < nums[right]:
         dq.pop()
    dq.append(right)
    left = right - k + 1
    if not left <= dq[0] <= right:
         dq.popleft()
    if k - 1 <= right:
         res.append(nums[dq[0]])
    right += 1
5. return res

Solution 1: [Sliding Window] [MinHeap] Fixed Size K Window With MaxHeap - Sliding Window/Fixed Size Window

    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:

        # Sliding Window (Fixed Size Window)

        # Window Representation:
        #   - Maintain window of size k [left, right]
        #   - MinHeap will store (-value, index), which turns it into a MaxHeap
        #   - MaxHeap top element will always hold the window maximum

        # Idea:
        #   - Push first k elements into heap
        #   - Iterate fixed window over array:
        #       1. Add right as new element
        #       2. Remove left as old element (index <= (right - k))
        #       3. Heap top is max of current window

        # Empty check:
        # tc: O(1)
        if not nums or k == 0:
            return []

        n = len(nums)

        # List of max values for each window
        # sc: O(n)
        result = []

        # MaxHeap():
        #   - a Python minHeap with negative values, resulting in a maxHeap
        #   - O(log n) insertion and removal 

        # MaxHeap data representation: (-value, index)
        #   - Need the -value to turn MinHeap into MaxHeap 
        #   - Need the index to check if top of heap is outside window
        maxHeap = []  

        # MaxHeap top representation (maxHeap[0]):
        #   - Top: largest element index in current window
        #   - All other: no guarantee for any other element, 
        #              we are only guaranteed that the top is the current max in the MaxHeap
        # 

        # Sliding Window Variables
        # sc: O(1)
        right = 0

        # Initialize first window:
        # push first k elements to maxHeap
        # tc: O(k)
        while right < k:
            
            # tc: O(log k)
            heapq.heappush(maxHeap, (-nums[right], right))

            # Expand substring
            # tc: O(1)
            right += 1

        # MaxHeap:
        # root of maxHeap now holds the max of the current window,
        # so its holding the max for the first window,
        # we append to the result list
        peekMax = -maxHeap[0][0]
        result.append(peekMax)

    
        # tc: O(n)
        while right < n:

            # Expand window
            newElem = nums[right]

            # Push new element: (-value, index)
            heapq.heappush(maxHeap, (-newElem, right))

            # Index Distance No More Than k Apart (not element count): 
            # The distance between left and right indices in the current window is right - left
            left = right - k

            # Keep removing top of maxHeap if it is stale and outside window
            # tc: O(1)
            while not left <= maxHeap[0][1] <= right:
                heapq.heappop(maxHeap)

            # MaxHeap:
            # root of maxHeap now holds the max of the current window,
            # we append to the result list
            peekMax = -maxHeap[0][0]
            result.append(peekMax)

            # Expand substring
            # tc: O(1)
            right += 1

        # overall: tc O(n log k)
        # overall: sc O(n)
        return result

Solution 2: [Dynamic Programming] K Block Partitions Shifted L To R and R to L Tracking Grabbing Leftmost and Rightmost For Each Window Iteration [TC Opt] - Sliding Window/Fixed Size Window

    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:

        # Sliding Window (Fixed Size Window)

        # Substring Representation:
        #   - Partition array into blocks of size k
        #   - leftMax[i]  = max from block start to i
        #   - rightMax[i] = max from block end to i (reverse scan)

        # Empty check:
        if not nums or k == 0:
            return []

        # We will split the original nums array into k sized blocks:
        # index:   0  1  2    3  4  5    6  7
        # nums:    1  3 -1   -3  5  3    6  7
        # blocks: [------]  [------]   [----]
        #           B0         B1        B2

        # Blocks are based only on index position and k size
        # They are not based on the direction of the scan 

        n = len(nums)

        # L to R Max Values, to L Max Values
        leftMax = [0] * n
        rightMax = [0] * n

        # Fill leftMax: find the running max on a per block basis L to R
        for i in range(n):

            # Start of new block: max is first element of block
            if i % k == 0:
                leftMax[i] = nums[i]

            # In Middle of block: compare currNum to prevMax
            else:
                leftMax[i] = max(leftMax[i-1], nums[i])

        # Fill rightMax: find the running max on a per block basis R to L
        for i in range(n-1, -1, -1):

            # Start of new block: max is first element of block
            if (i+1) % k == 0 or i == n-1:
                rightMax[i] = nums[i]

            # In Middle of block: compare currNum to prevMax
            else:
                rightMax[i] = max(rightMax[i+1], nums[i])

    
        # Compute sliding fixed window max 
        res = []
        left = 0
        right = k - 1

    
        # -----------------------------
        # Combining Precomputed with Fixed Window:
        # we are iterating a fixed window of size k:

        # Case 1: fixed window fully inside block
        #
        #   [  Window   ]
        #   [   BLOCK   ]
        #  

        # Case 2: fixed window spans 2 blocks
        #
        #     [  Window   ]
        # [ BLOCK A ][ BLOCK B ]


        # -----------------------------
        # Grabbing Farthest Data Available To Use From Left and Right:

        # Case 1: fixed window fully inside block
        #
        #   [  Window   ]
        #   [   BLOCK   ]
        #   ^           ^
        #   R           L

        # Case 2: fixed window spans 2 blocks
        #
        #     [  Window   ]
        # [ BLOCK A ][ BLOCK B ]
        #     ^           ^
        #     R           L

        # -----------------------------
        # Window Max Coverage:
        #       R = index max from iterating Right to Left (<---)
        #       L = index max from iterating Left to Right (--->)

        # Case 1: Entire Window Is Accounted For
        #
        #   [  Window   ]
        #   [   BLOCK   ]
        #   ^           ^
        #   R --------- L        
        #  (<---)     (--->)

        # Case 2: Entire Window Is Accounted For
        #
        #     [  Window   ]
        # [ BLOCK A ][ BLOCK B ]
        #     ^           ^
        #     R --------- L        
        #    (<---)     (--->)
        
        # tc: iterate over nums array O(n)
        while right < n:

            # Re: see above diagrams
            res.append(max(rightMax[left], leftMax[right]))
            left += 1
            right +=1

        # overall: tc O(n)
        # overall: sc O(n)
        return res

Solution 3: [Sliding Window] [Monotonic] [Deque] Fixed Size K Window With Indices Monotonic Decreasing Deque Max On Left MRI Min On Right Safe For Duplicates [TC Opt] - Sliding Window/Fixed Size Window

    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:

        # Sliding Window (Fixed Size Window)

        # Substring Representation
        #   - Maintain window of size k [left, right]
        #   - Monotonic Decreasing Deque storing indices within window
        #   - Front of deque always holds max element index
        #   - Back of deque always holds the min element index
        #   - Back of deque always holds the most recently added index

        # Idea:
        #   - Remove elements smaller than incoming element from the back of the deque
        #   - Remove max front element if it exits window
        #   - Front of deque will always window max

        # Safe For Duplicates:
        #   - Stores indices instead of values
        #   - When removing outgoing element, we compare by index not value
        #   - Each index is unique, so we always know exactly which element is leaving
        #   - Duplicate values are handled correctly since we never confuse two
        #     elements at different positions

        # Deque():
        #   - a double ended queue
        #   - O(1) insertion and removal from both sides

        # Deque left and right ends representation:
        #   - Front Left dq[0]: largest element index in current window
        #   - Back Right dq[-1]: most recently added index, min element in window

        # Monotonic decreasing order means:
        #   High -> Low

        # left (high)             right (low)
        # [ idx=2,  idx=4,  idx=5,  idx=7 ]  Our Queue: Monotonic Increasing Indexes
        # [ val=8,  val=6,  val=3,  val=1 ]  We Can Access: Values always Monotonic Decreasing

        # Empty check:
        if not nums or k == 0:
            return []

        n = len(nums)

        right = 0

        # Max element list for each window
        # sc: O(n)
        res = []

        # stores indices in monotonic decreasing order of their values
        # sc: O(k)
        dq = deque()

        # tc: O(n)
        while right < n:

            # Monotonic Property:
            # dq[0]: current max element in deque (front of deque)
            # dq[-1]: current min element in deque (back of deque)  <-- pop this one
            # remove any elements smaller than the current new element,
            # so that the new element ends up at the back of the deque,
            # and we keep the Monotonic property dq[0] High -> Low dq[-1]
            # tc: O(1) amortized
            while dq and nums[dq[-1]] < nums[right]:
                dq.pop()

            # Add INDEX to be the new back of deque: dq[-1] => index
            dq.append(right)

            # Sliding Window Single Iteration:
            # since at most, we are only adding 1 new element
            # and since we have a fixed window size,
            # only 1 element can go stale (end up out of bounds of the window),
            # however, we will only check the top of the deque for stale indexes
            
            # Monotonic Trick:
            # Since we keeping both:
            #   - Monotonic High -> Low
            #   - Iterating by 1 Index at a time

            # left (high)             right (low)
            # [ idx=2,  idx=4,  idx=5,  idx=7 ]  Our Queue: Monotonic Increasing Indexes
            # [ val=8,  val=6,  val=3,  val=1 ]  We Can Access: Values always Monotonic Decreasing

            # So we only have to check if the highest value,
            # which by definition will have the lowest index (oldest value in the deque)
            # is now out of bounds of the fixed window

            # Element Count of Max k (not index distance):
            # left = right - (k-1): the leftmost index needed so the window
            # spans exactly k elements (fencepost: k elements = k-1 steps back)
            # left is INSIDE the window (first valid index),
            # calculate NEW left distance after adding currNum
            left = right - k + 1

            # Keep removing top of deque if it is stale and outside window
            # dq[0]: current max element in deque (front of deque)  <-- pop this one
            # dq[-1]: current min element in deque (back of deque)
            if not left <= dq[0] <= right:
                dq.popleft()

            # Left K Fixed Window:
            # We generated left from the fixed window size k,
            # so we cannot use it in a right - left + 1 
            # as this would just always give us k,
            # instead check if right has passed the kth index
            if 0 <= left:

                # Deque front now holds max valid index within the current window,
                # now we only want to record once we have reached our fixed window full length,
                # to ensure we have grabbed all the elements we need
                res.append(nums[dq[0]])

            # Iterate window:
            right += 1

        # overall: tc O(n)
        # overall: sc O(n)
        return res

3. Longest Substring Without Repeating Characters ::2:: - Medium

Topics: Hash Table, String, Sliding Window

Intro

Given a string s, find the length of the longest without duplicate characters.

Example InputOutput
s = "abcabcbb"3
s = "bbbbb"1
s = "pwwkew"3

Constraints:

0 ≤ s.length ≤ 5 * 104

s consists of English letters, digits, symbols and spaces.

Abstraction

Given a string, find longest substring without duplicates

Pseudocode

Sol 1: MRI HashMap With Jumping Left
1. (mri = {})
2. (left, right = 0, 0)
3. (maxLen = 0)
4. (n = len(s)-1)
5. while right <= n:
   a. newChar = s[right]
   b. if newChar in mri:
         prevIndex = mri[newChar]
         if left <= prevIndex <= right: 
             left = prevIndex + 1
   c. mri[newChar] = right
   d. currLen = right - left + 1
   d. maxLen = max(maxLen, currLen)
   e. right += 1
6. return maxLen

Solution 2: Window HashSet Tracking Chars With Removal
1. (window = {}) 
2. (left, right = 0, 0)
3. (maxLen = 0)
4. (n = len(s)-1)
5. while right < n:
   a. newChar = s[right]
   b. while newChar in window:
         window.remove(s[left])
         left += 1
   c. window.add(newChar)
   d. maxLen = max(maxLen, len(window))
   e. right += 1
6. return maxLen

Solution 1: [Sliding Window] MRI HashMap Jump Left When Invalid - Sliding Window/Variable Size Window

    def lengthOfLongestSubstring(self, s: str) -> int:

        # Sliding Window (Variable Size)

        # Window Representation:
        #   - HashSet contains most recent index of all chars encountered
        #   - Window boundaries kept via [left, right]
        #   - Valid window contains no duplicate characters

        # Idea:
        #   - Expand variable size window over list by extending right
        #   - If new character results in a duplicate,
        #     move the left window boundary to 1 in front of the duplicate
        #   - For each valid window compare size to max window

        n = len(s)-1

        # Track more recent index appearance for char
        mri = {} 

        # Sliding Window Variables
        # left and right boundary of substring
        left = 0
        right = 0

        # max length so far
        maxLen = 0

        # tc: O(n)
        while right <= n:
                
                # Character we just added to substring
                newChar = s[right]

                # If we have encountered character already
                if newChar in mri:
                    
                    # Grab index of previous encounter
                    prevIndex = mri[newChar]
                    
                    # Jump from left if window is invalid:
                    # - jump left to 1 index after duplicate
                    if left <= prevIndex <= right:
                        left = prevIndex + 1

                # Update most recent index for character
                mri[newChar] = right

                # Valid window, compare to max
                currLen = right - left + 1
                maxLen = max(maxLen, currLen)

                # Iterate window
                right += 1
            
        # overall: tc O(n)
        # overall: sc O(min(n, k))
        return maxLen

Solution 2: [Sliding Window] HashSet Tracking Window Boundaries And Shrink While Invalid - Sliding Window/Variable Size Window

    def lengthOfLongestSubstring(self, s: str) -> int:

        # Sliding Window HashSet Right Left Representation (Variable Size Window)

        # Window Representation:
        #   - Set() contains chars within window [left, right]    
        #   - Numbers enter and exit window via left and right
        #   - Valid window contains no duplicate characters

        # Idea:
        #   - Expand variable size window over list by extending right
        #   - If duplicate found, shrink window from left until duplicate is removed
        #   - For each valid window compare size to max window

        n = len(s)

        # Sliding Window Data:
        # sc: O(n)
        window = set()

        # Sliding Window Boundaries:
        left = 0
        right = 0

        # Track longest valid window
        maxLen = 0

        # tc: O(n)
        while right < n:

            # Grab char we want to add
            newChar = s[right]

            # Shrink from left while window is invalid:
            # - if duplicate exists in window set
            # tc: O(n)
            while newChar in window:
                window.remove(s[left])
                left += 1

            # Add char to window
            window.add(newChar)

            # Compare valid window length to max
            maxLen = max(maxLen, len(window))

            # Expand valid window
            right += 1

        # overall: tc O(n)
        # overall: sc O(min(n, k))
        return maxLen

1208. Get Equal Substrings Within Budget ::1:: - Medium

Topics: String, Sliding Window

Intro

You are given two strings s and t of the same length and an integer maxCost. You want to change s to t. Changing the ith character of s to ith character of t costs |s[i] - t[i]| (i.e., the absolute difference between the ASCII values of the characters). Return the maximum length of a substring of s that can be changed to be the same as the corresponding substring of t with a cost less than or equal to maxCost. If there is no substring from s that can be changed to its corresponding substring from t, return 0.

Example InputOutput
s = "abcd", t = "bcdf", maxCost = 33
s = "abcd", t = "cdef", maxCost = 31
s = "abcd", t = "acde", maxCost = 01

Constraints:

1 ≤ s.length ≤ 10^5

t.length == s.length

0 ≤ maxCost ≤ 10^6

s and t consist of only lowercase English letters.

Abstraction

Find the largest window for s that stays under the budget for changing all necessary characters from s to match t

Pseudocode

  oh! pseudocode hasn't been written yet, try another card! :)

Solution 1: [Sliding Window] Cost Tracking Window Cost And Shrink While Invalid - Sliding Window/Variable Size Window

    def equalSubstring(self, s: str, t: str, maxCost: int) -> int:
        
        # Sliding Window + Two Pointers
        
        # Substring Representation:
        #   - Cost tracks cost to change every character in s to match t
        #   - Valid window has a windowChangeCost <= maxCost

        # Idea:
        #   - Expand variable size window over list by extending right
        #   - Add new char cost to change s[right] -> t[right] to current total window cost
        #   - If cost exceeds budget, shrink window from left until cost is valid again
        #   - For each valid window compare size to max window

        n = len(s)
        
        # Sliding Window Variables
        left = 0
        right = 0
        maxLen = 0

        # Cost to change entire current window from s -> t
        windowChangeCost = 0

        # tc: O(n)
        while right < n: 

            # Add new char cost to window cost:
            # - cost of single char from s -> t
            rightCharS = s[right]
            rightCharT = t[right]
            cost = abs(ord(rightCharS) - ord(rightCharT))
            windowChangeCost += cost

            # Shrink from left while window is invalid:
            # - curr cost to change entire window is higher than budget
            while maxCost < windowChangeCost:

                # Remove old character and save cost from window cost:
                # - cost of single char from s -> t
                leftCharS = s[right]
                leftCharT = t[right]
                saving = abs(ord(s[left]) - ord(t[left]))
                windowChangeCost -= saving

                # Shrink window
                left += 1
            
            # Valid window, compare to max
            windowLen = (right - left) + 1
            maxLen = max(maxLen, windowLen)

            # Iterate window
            right += 1

        # overall: tc O(n)
        # overall: sc O(1)
        return maxLen

1004. Max Consecutive Ones III ::2:: - Medium

Topics: Array, Binary Search, Sliding Window, Prefix Sum

Intro

Given a binary array nums and an integer k, return the maximum number of consecutive 1's in the array if you can flip at most k 0's.

Example InputOutput
nums = [1,1,1,0,0,0,1,1,1,1,0], k = 26
nums = [0,0,1,1,0,0,1,1,1,0,1,1,0,0,0,1,1,1,1], k = 310

Constraints:

1 ≤ nums.length ≤ 10^5

nums[i] is either 0 or 1

0 ≤ k ≤ nums.length

Abstraction

Find the largest window for nums that stays under the budget for changing at most k 0's from 0 to 1.

Pseudocode

  oh! pseudocode hasn't been written yet, try another card! :)

Solution 1: [Sliding Window] Two Pointer Sliding Window With Shrinking Max Tracking - Sliding Window/Variable Size Window

    def longestOnes(self, nums: List[int], k: int) -> int:

        # Sliding Window (Variable Size Window)

        # Window Representation:
        #   - Zero tracks total flips needed for substring to be consecutive 1's
        #   - Valid window only has consecutive 1's
        #   - Valid window has zeroCount <= k

        # Idea:
        #   - Expand variable size window over list by extending right
        #   - Add new int to zero count
        #   - If zeroCount exceeds budget, shrink window from left until num of zeros is valid again
        #   - For each valid window compare size to max window

        n = len(nums)-1

        # Sliding Window Boundaries
        left = 0
        right = 0

        # Number of zeros in current window
        zeroCount = 0

        # Longest valid window seen
        maxLen = 0

        # tc: O(n)
        while right <= n:

            # Update zero count:
            if nums[right] == 0:
                zeroCount += 1

            # Shrink from left while window is invalid:
            #   - num of 0's must be <= k
            # tc: O(1) amortized
            while k < zeroCount:

                # Update count
                if nums[left] == 0:
                    zeroCount -= 1
                left += 1

            # Validate window, compare size to max
            currWindow = right - left + 1
            maxLen = max(maxLen, currWindow)

        # overall: tc O(n)
        # overall: sc O(1)
        return maxLen

904. Fruit Into Baskets ::2:: - Medium

Topics: Array, Hash Table, Sliding Window

Intro

You are visiting a farm that has a single row of fruit trees arranged from left to right. The trees are represented by an integer array fruits where fruits[i] is the type of fruit the ith tree produces. You want to collect as much fruit as possible. However, the owner has some strict rules that you must follow: You only have two baskets, and each basket can only hold a single type of fruit. There is no limit on the amount of fruit each basket can hold. Starting from any tree of your choice, you must pick exactly one fruit from every tree (including the start tree) while moving to the right. The picked fruits must fit in one of your baskets. Once you reach a tree with fruit that cannot fit in your baskets, you must stop. Given the integer array fruits, return the maximum number of fruits you can pick.

Example InputOutput
fruits = [1,2,1]3
fruits = [0,1,2,2]3
fruits = [1,2,3,2,2]4

Constraints:

1 ≤ fruits.length ≤ 10^5

0 ≤ fruits[i] < fruits.length

Abstraction

wow! again

Pseudocode

  oh! pseudocode hasn't been written yet, try another card! :)

Solution 1: [Sliding Window] Fruit Count HashMap With Shrink From Left While More Than 2 Fruit In Window - Sliding Window/Variable Size Window

    def totalFruit(self, fruits: List[int]) -> int:

        # Sliding Window + Hashmap
        
        # Window Representation:
        #   - HashMap tracks count of fruit within window
        #   - Valid window contains a max of 2 unique fruit

        # Rules:
        #   - Expand variable size window over list by extending right
        #   - Add new fruit to count
        #   - If HashMap has more than 2 unique fruit, shrink window from left until unique fruit is valid again
        #   - For each valid window compare size to max window
        
        # Fruit Count:
        # Holds at most 2 unique fruits at any moment
        # sc: O(1)
        count = defaultdict(int)

        # Window Representation:
        left = 0
        right = 0

        n = len(fruits)

        # Longest string up to now
        maxLen = 0

        # tc: O(n)
        while right < n:

            # Add right num
            rightNum = fruits[right]
            count[rightNum] += 1

            # Shrink from left while window is invalid:
            #   - more than 2 unique fruits in window
            while len(count) > 2:
                
                # Decrease left fruit count
                leftNum = fruits[left]
                count[leftNum] -= 1

                # Remove fruit type if no more fruit remain:
                if count[fruits[left]] == 0:
                    del count[fruits[left]]

                # Shrink window
                left += 1

            # Valid window, compare to max length
            currWindow = right - left + 1
            maxLen = max(maxLen, currWindow)

            # Iterate window
            right += 1

        # overall: tc O(n)
        # overall: sc O(1)
        return maxLen

Solution 2: [Sliding Window] MRI HashMap Jump Left When Invalid - Sliding Window/Variable Size Window

    def totalFruit(self, fruits: List[int]) -> int:

        # Sliding Window + Most Recent Index Map

        # Idea:
        #   - Maintain a window [l, r]
        #   - Track last seen index of each fruit type
        #   - If we ever have > 2 fruit types:
        #       remove the fruit with the smallest last seen index
        #       and move left pointer just past it

        mri = {}

        n = len(fruits)-1

        left = 0
        right = 0

        maxLen = 0

        # tc: O(n)
        while right <= n:

            # Update most recent index of current fruit
            mri[fruits[right]] = right

            # Jump from left if window is invalid:
            # - more than 2 unique fruit types, currently 3 types
            if len(mri) > 2:

                # Remove the fruit farthest to the left
                # - jump left to 1 index after farthest fruit
                farthestLeftFruit = min(mri, key=mri.get)
                left = mri[farthestLeftFruit] + 1

                # Remove fruit from map
                del mri[farthestLeftFruit]

            # Valid window, compare to max
            currLen = right - left + 1
            maxLen = max(maxLen, currLen)

            # Iterate window
            right += 1

        # overall: tc O(n)
        # overall: sc O(n)
        return maxLen

1438. Longest Continuous Subarray With Absolute Diff Less Than or Equal to Limit ::1:: - Medium

Topics: Array, Queue, Sliding Window, Heap (Priority Queue), Ordered Set, Monotonic Queue

Intro

Given an array of integers nums and an integer limit, return the size of the longest non-empty subarray such that the absolute difference between any two elements of this subarray is less than or equal to limit.

Example InputOutput
nums = [8,2,4,7], limit = 42
nums = [10,1,2,4,7,2], limit = 54
nums = [4,2,2,2,4,4,2,2], limit = 03

Constraints:

1 ≤ nums.length ≤ 10^5

1 ≤ nums[i] ≤ 10^9

0 ≤ limit ≤ 10^9

Abstraction

wow! again

Pseudocode

  oh! pseudocode hasn't been written yet, try another card! :)

Solution 1: [Sliding Window] [Monotonic] [Deque] Two Pointer Sliding Window - Sliding Window/Variable Size Window

    def longestSubarray(self, nums: List[int], limit: int) -> int:
        
        # Sliding Window + Monotonic Deques
    
        # Window Representation:
        #   - Max and min deque track max and min for current window
        #   - Window boundaries kept via [left, right]
        #   - Valid window has absolute diff less <= limit

        # Idea:
        #   - Expand variable size window over list by extending right
        #   - 
        #   - Maintain two deques for the cu:
        #       max_d: decreasing deque for maximum
        #       min_d: increasing deque for minimum
        #   - Expand right pointer; shrink left if max - min > limit.
        #   - Track maximum window length.
        
        # Rules:
        #   1. Append nums[right] to both deques, maintaining monotonic quality
        #   2. while current window violates limit (max - min > limit), shrink from left
        #   3. Update maximum window length

        # Sliding Window Data
        # sc: O(n)
        maxDeque = deque()  # stores elements in decreasing order
        minDeque = deque()  # stores elements in increasing order

        # Window Representation:
        # sc: O(1)
        left = 0
        right = 0

        # Current max length
        maxLen = 0
        n = len(nums)

        # Iterate over window 
        while right < n:
            
            # Add new num to window
            rightNum = nums[right]

            # Maintain Monotonic Decreasing max deque
            while maxDeque and rightNum > maxDeque[-1]:
                maxDeque.pop()
            maxDeque.append(rightNum)

            # Maintain Monotonic increasing min deque
            while minDeque and rightNum < minDeque[-1]:
                minDeque.pop()
            minDeque.append(rightNum)

            # No need to use abs()
            # since minDeque[0] <= maxDeque[0]
            # so result will always be positive
            difference = maxDeque[0] - minDeque[0]

            # Shrink from left while window is invalid:
            # - difference exceeds limit
            while limit < difference:

                # if current max is the left most num, remove from maxDeque from window
                if nums[left] == maxDeque[0]:
                    maxDeque.popleft()
                
                # if current min is the left most num, remove from minDeque from window
                if nums[left] == minDeque[0]:
                    minDeque.popleft()

                # Shrink window
                left += 1

                # grab new difference
                difference = maxDeque[0] - minDeque[0]

            # Update maximum window length
            maxLen = max(maxLen, right - left + 1)

            # Iterate window 
            right += 1

        # overall: tc O(n)
        # overall: sc O(n)
        return maxLen

424. Longest Repeating Character Replacement ::1:: - Medium

Topics: Hash Table, String, Sliding Window

Intro

You are given a string s and an integer k. You can choose any character of the string and change it to any other uppercase English character. You can perform this operation at most k times. Return the length of the longest substring containing the same letter you can get after performing the above operations.

Example InputOutput
s = "ABAB", k = 24
s = "AABABBA", k = 14

Constraints:

1 ≤ s.length ≤ 105

s consists of only uppercase English letters.

0 ≤ k ≤ s.length

Abstraction

Given a string, and count k, return the longest substring of matching characters you can get after replacing k or less characters.

Pseudocode

Sol 1: HashMap Tracking CharCount To Find MostOccur
1. (charCount = {}) 
2. (left, right = 0, 0)  
3. (mostOccur = 0) 
4. (maxLen = 0)
2. while right < n:
   a. charCount[s[right]] += 1
   b. mostOccur = max(mostOccur, charCount[s[right]])
   c. currWindowLen = right - left + 1
   d. allOtherChars = currWindowLen - mostOccur
   e. while k < allOtherChars:
        charCount[s[left]] -= 1
        left += 1
        currWindowLen = right - left + 1
        allOtherChars = currWindowLen - mostOccur
   f. maxLen = max(maxLen, currWindowLen)
   g. right += 1
3. return maxLen

Solution 1: [Sliding Window] Count HashMap Tracking Char In Window To Find MostOccur To Verify AllOtherChars In Window Are Replaceable Or Shrink From Left Until AllOtherChars Are Replaceable Less Than Or Equal To K - Sliding Window/Variable Size Window

    def characterReplacement(self, s: str, k: int) -> int:
        
        # Sliding Window (Variable Size Window)

        # Window Representation:
        #   - HashMap tracks char count within window
        #   - Valid window contains a single consecutive character
        #   - Valid window changes at most k characters from c -> seqChar

        # Idea:
        #   - Expand variable size window over list by extending right
        #   - Add new char to count
        #   - mostOccur is total count of the most occurring character in the window
        #   - allOtherChars is the total count of all other characters in the window
        #   - If allOtherChars is more than the char change budget k, shrink window from left until allOtherChars <= k

        n = len(s)

        # char count within window boundaries
        count = defaultdict(int)

        # Sliding Window Variables
        # sc: O(1)
        # left and right window boundaries
        left = 0
        right = 0

        # Sliding Window Data
        # sc: O(1)
        # mostOccur: most occurring character in window
        mostOccur = 0  
        # max length seen so far
        maxLen = 0

        # tc: O(n)
        while right < n:

            # Character we just encountered
            # tc: O(1)
            currChar = s[right]
            charCount[currChar] += 1

            # Update mostOccur in window
            # tc: O(1)
            mostOccur = max(mostOccur, charCount[currChar])

            # length of curr window
            currWindowLen = right - left + 1

            # how many other chars exists, (anything that is not mostOccur)
            allOtherChars = currWindowLen - mostOccur

            # Shrink from left while window is invalid:
            # We need allOtherChars to be less than k (num of chars we can replace),
            # so that we can replace allOtherChars with the mostOccur.
            # thus, shrink window from left by 1 until allOtherChars is valid
            # tc: O(1) amortized
            while k < allOtherChars:

                # Do NOT update mostOccur here:
                #   - while shrinking, the mostOccur may become stale and decreased
                #   - mostOccur may have shifted to a different character

                # This is fine because:
                #   - for window [left:right], we have agreed 
                #     that mostOccur is highest char up to right
                #   - A stale mostOccur will under estimate allOtherChars 
                
                # This is safe because: 
                #   - No char can exceed the current mostOccur while shrinking,
                #     we are only shrinking the window so counts can only go down
                
                # Thus during shrink, mostOccur:
                #   - not acting as a true most occurring character
                #   - acting as a flag for the original mostOccur for original window
                #     that existed before shrinking
                
                # Remove left character from window
                charCount[s[left]] -= 1

                left += 1

                # Recompute currWindowLen and allOtherChars after shrink
                currWindowLen = right - left + 1
                allOtherChars = currWindowLen - mostOccur

            # Once allOtherChars is valid, check maxLen
            maxLen = max(maxLen, currWindowLen)

            # Iterate window
            right += 1

        # overall: tc O(n)
        # overall: sc O(1)
        return maxLen

713. Subarray Product Less Than K ::1:: - Medium

Topics: Array, Binary Search, Sliding Window, Prefix Sum

Intro

Given an array of integers nums and an integer k, return the number of contiguous subarrays where the product of all the elements in the subarray is strictly less than k.

Example InputOutput
nums = [10,5,2,6], k = 1008
nums = [1,2,3], k = 00

Constraints:

1 ≤ nums.length ≤ 3 * 10^4

1 ≤ nums[i] ≤ 1000

0 ≤ k ≤ 10^6

Abstraction

wow! again

Pseudocode

  oh! pseudocode hasn't been written yet, try another card! :)

Solution 1: [Sliding Window] Running Product Sliding Window - Sliding Window/Variable Size Window

    def numSubarrayProductLessThanK(self, nums: List[int], k: int) -> int:

        # Sliding Window + Running Product

        # Window Representation:
        #   - Product tracks product of current window
        #   - Valid window has a product <= k

        # Rules:
        #   - Expand variable size window over list by extending right
        #   - Multiple new num into running product
        #   - If product is greater than k, shrink window from left until product is valid again 
        #   - For each valid window compare size to max window

        # Early Exit:
        # No array can have a product less than 1,
        # since int values are guaranteed to be 1 <= int
        if k <= 1:
            return 0

        # Sliding window boundaries
        left = 0

        # Running product of current window
        runningProduct = 1

        # Total valid subarrays
        validArrayCount = 0

        # Expand right boundary
        # tc: O(n)
        for right in range(len(nums)):

            # Include current element into window
            runningProduct *= nums[right]

            # Shrink from left while window is invalid:
            # - runningProduct < k
            # tc: O(1) amortized
            while runningProduct >= k:

                # Remove left element from runningProduct
                runningProduct //= nums[left]

                # Shrink window
                left += 1

            # Add all valid subarray ending at right,
            # all other subarrays are counted at different iterations
            arraysEndingAtRight = right - left + 1
            validArrayCount += arraysEndingAtRight

        # overall: tc O(n)
        # overall: sc O(1)
        return validArrayCount

567. Permutation in String ::2:: - Medium

Topics: Hash Table, String, Sliding Window

Intro

Given two strings s1 and s2, return true if s2 contains a permutation of s1, or false otherwise. In other words, return true if one of s1's permutations is the substring of s2.

Example InputOutput
s1 = "ab", s2 = "eidbaooo"true
s1 = "ab", s2 = "eidboaoo"false

Constraints:

1 ≤ s1.length, s2.length ≤ 104

s1 and s2 consist of lowercase English letters.

Abstraction

Given a string 1 and string 2, r return true if string 1 is a permutation within string 2.

Pseudocode

  oh! pseudocode hasn't been written yet, try another card! :)

Solution 1: [Sliding Window] Needle In Haystack Window Count HashMap For Char And Removing From Left While Window Size Flag Is False - Sliding Window/Fixed Size Window

    def checkInclusion(s1: str, s2: str) -> bool:

        # Sliding Window (Fixed Size Window)

        # Substring Representation:
        #   - Maintain window [left, right]
        #   - Window size always equals len(s1)
        #   - Hashmap: char -> frequency

        # Idea:
        #   - Build a frequency map for s1
        #   - Expand right pointer to build window in s2
        #   - If window size exceeds len(s1), shrink from left
        #   - If window frequency == s1 frequency, permutation exists

        # Since window size is fixed, left moves whenever
        # window grows larger than lens1)

        n1 = len(s1)
        n2 = len(s2)

        # s2 < s1: permutation not possible
        # tc: O(1)
        if n2 < n1:
            return False

        # s1 Frequency
        # tc: O(n)
        # sc: O(n)
        needleCharCount = defaultdict(int)
        for c in s1:
            needleCharCount[c] += 1

        # Substring window frequency
        # sc: O(1)
        windowCharCount = defaultdict(int)
        
        # left and right boundary of current window
        left = 0
        right = 0

        # tc: O(n)
        for right in range(n2):

            # Add char count to window
            rightChar = s2[right]
            windowCharCount[rightChar] += 1

            # Window length
            currWindowLen = right - left + 1

            # Shrink from left while window is invalid:
            #   - window length is greater than needle length
            #   - since we extend window by 1 char, we only need to decrease by 1 char
            if n1 < currWindowLen:

                # Decrease char count from window
                leftChar = s2[left]
                windowCharCount[leftChar] -= 1

                # Remove char if frequency reached zero,
                # to match needle hashmap
                if windowCharCount[leftChar] == 0:
                    del windowCharCount[leftChar]
                
                # Shrink window
                left += 1
            
            # If window hashmap matches the needle hashmap,
            # valid permutation found
            if windowCharCount == needleCharCount:
                return True

        # overall: tc O(n) 
        # overall: sc O(n)
        return False

Solution 2: [Sliding Window] Needle In Haystack Window Count Array With 97 Lowercase Ascii Shifting [SC Opt] - Sliding Window/Fixed Size Window

    def checkInclusion(self, s1: str, s2: str) -> bool:

        # Sliding Window (Fixed Size Window)

        # Substring Representation:
        #   - Maintain window [left, right]
        #   - Window size always equals len(s1)
        #   - Frequency array: char -> frequency (size 26 for lowercase letters)

        # Idea:
        #   - Build a frequency array for s1
        #   - Expand right pointer to build window in s2
        #   - If window size exceeds len(s1), shrink from left
        #   - If window frequency == s1 frequency, permutation exists

        # Since window size is fixed, left moves whenever
        # window grows larger than len(s1)

        n1 = len(s1)
        n2 = len(s2)

        # If needle length is greater than haystack length:
        if n2 < n1:
            return False

        # s1 Frequency
        # tc: O(n)
        # sc: O(1)
        needleCharCount = [0] * 26
        for c in s1:
            needleCharCount[ord(c) - 97] += 1

        # Substring window frequency
        # sc: O(1)
        windowCharCount = [0] * 26

        # left and right boundary of current window
        left = 0
        right = 0

        # tc: O(n)
        for right in range(n2):

            # Add char count to window
            rightChar = s2[right]
            windowCharCount[ord(rightChar) - 97] += 1

            # Window length
            currWindowLen = right - left + 1

            # Shrink from left while window is invalid:
            #   - window length is greater than needle length
            #   - since we extend window by 1 char, we only need to decrease by 1 char
            if n1 < currWindowLen:

                # Decrease char count from window
                leftChar = s2[left]
                windowCharCount[ord(leftChar) - 97] -= 1

                # No need to "remove" the 0 count chars
                # since all empty chars are already set to 0

                # Shrink window
                left += 1

            # If window frequency array matches the needle frequency array,
            # valid permutation found
            if windowCharCount == needleCharCount:
                return True

        # overall: tc O(n)
        # overall: sc O(1)
        return False

1358. Number of Substrings Containing All Three Characters ::2:: - Medium

Topics: Hash Table, String, Sliding Window

Intro

Given a string s consisting only of characters a, b and c. Return the number of substrings containing at least one occurrence of all these characters a, b and c.

Example InputOutput
s = "abcabc"10
s = "aaacb"3
s = "abc"1

Constraints:

3 ≤ s.length ≤ 5 * 10^4

s only consists of a, b or c characters

Abstraction

Give the total number of valid substrings. A valid substring contains all 3 chars.

Pseudocode

  oh! pseudocode hasn't been written yet, try another card! :)

Solution 1: [Sliding Window] Two Pointer Sliding Window - Sliding Window/Variable Size Window

    def numberOfSubstrings(self, s: str) -> int:

        # Sliding Window (Variable Size Window)

        # Window Representation:
        #   - Maintain window [left, right]
        #   - HashMap stores frequency of each char in current window
        #   - Window is valid when it contains at least one of each: a, b, c

        # Idea:
        #   - Expand right to add new characters
        #   - Once window is valid (has all 3 chars), shrink from left
        #     to find the smallest valid window ending at right
        #   - Every starting index from [0, left-1] also forms a valid substring
        #     ending at right, since adding more chars to the left keeps it valid
        #   - So add left to result at each right

        n = len(s)-1

        # HashMap: char -> frequency in current window
        # sc: O(1) at most 3 chars
        count = defaultdict(int)

        # Sliding Window Boundaries
        left = 0
        right = 0

        # Total valid substrings found
        res = 0

        # tc: O(n)
        while right <= n:

            # Add char count to window
            count[s[right]] += 1

            # Shrink from left while window is valid:
            # - window contains all 3 chars
            while True:

                # Check if window contains all 3 characters
                windowValid = True
                for c in "abc":
                    if count[c] <= 0:
                        windowValid = False
                        break

                # Stop Shrinking
                if not windowValid:
                    break

                # Decrease char count
                count[s[left]] -= 1

                # Shrink window
                left += 1

            # Include all valid subarrays from 0 to left:
            # [z, z, z, a b c] -> left is set to index 4, when window becomes invalid
            # so there are 4 valid subarrays
            # tc: O(1)
            res += left

            # Iterate window
            right += 1

        # overall: tc O(n)
        # overall: sc O(1)
        return res

Solution 2: [Last Seen Index] 3 MRI Variables - Sliding Window/Variable Size Window

    def numberOfSubstrings(self, s: str) -> int:

        # Last Seen Index Tracking (Single Pass)

        # Representation:
        #   - Track the last seen index of each char: a, b, c
        #   - Valid window contains all 3 chars

        # Idea:
        #   - Expand variable size window over list by extending right
        #   - Ignore all other chars
        #   - Update abc index to mri
        #   - All valid windows start at the left most mri after full iteration

        n = len(s) - 1

        # Init mri to -1,
        # if char is never encountered then count results in 0
        mriA = mriB = mriC = -1

        # Total valid substrings found
        count = 0

        right = 0

        # tc: O(n)
        while right <= n:

            # Update last seen index for current char
            if s[i] == 'a':
                mriA = i
            elif s[i] == 'b':
                mriB = i
            elif s[i] == 'c':
                mriC = i

            # Leftmost mri:
            # [z, z, z, a b c] -> mri for a is 3, 
            # so there are 3 + 1 = 4 subarrays with all 4 chars
            count += min(mriA, mriB, mriC) + 1

            # Extend window
            right += 1

        # overall: tc O(n)
        # overall: sc O(1)
        return count

209. Minimum Size Subarray Sum ::2:: - Medium

Topics: Binary Search, Sliding Window, Prefix Sum

Intro

Given an array of positive integers nums and a positive integer target, return the minimal length of a whose sum is greater than or equal to target. If there is no such subarray, return 0 instead. Follow up: If you have figured out the O(n) solution, try coding another solution of which the time complexity is O(n log(n)).

Example InputOutput
target = 7, nums = [2,3,1,2,4,3]2
target = 4, nums = [1,4,4]1
target = 11, nums = [1,1,1,1,1,1,1,1]0

Constraints:

1 ≤ target ≤ 10^9

1 ≤ nums.length ≤ 10^5

1 ≤ nums[i] 10^4

Abstraction

Given array of nums, return the min length subarray with a sum greater than or equal to target. If none exists just return 0

Pseudocode

  oh! pseudocode hasn't been written yet, try another card! :)

Solution 1: [Sliding Window] Running Sum Window Shrink While Valid - Sliding Window/Variable Size Window

    def minSubArrayLen(self, target: int, nums: List[int]) -> int:

        # Sliding Window (Variable Size Window)

        # Window Representation:
        #   - Running count tracks sum of window
        #   - Valid window has sum greater than or equal to target

        # Idea:
        #   - Expand variable size window over list by extending right
        #   - Add new value to running sum
        #   - All nums are positive, sum always increases when extending and decreases when shrinking
        #   - If sum exceeds target, shrink from left until sum is valid again
        #   - For each valid window compare size to max window

        n = len(nums)

        left = 0

        # Sum of current window
        runningSum = 0
        
        minLen = float('inf')

        # tc: O(n)
        for right in range(n):

            # Add num to running count
            windowSum += nums[right]

            # Shrink from left while window is valid:
            # - windowSum meets or exceeds target
            # tc: O(1) amortized
            while target <= windowSum:

                # Valid window, compare to min length 
                currWindowLen = right - left + 1
                minLen = min(minLen, currWindowLen)

                # Remove left num from window sum
                windowSum -= nums[left]

                # Shrink window
                left += 1

        # No valid window was found
        if minLen == float('inf'):
            return 0
        
        # overall: tc O(n)
        # overall: sc O(1)
        return minLen

76. Minimum Window Substring ::2:: - Hard

Topics: Hash Table, String, Sliding Window

Intro

Given two strings s and t of lengths m and n respectively, return the minimum window of s such that every character in t (including duplicates) is included in the window. If there is no such substring, return the empty string "". The testcases will be generated such that the answer is unique.

Example InputOutput
s = "ADOBECODEBANC", t = "ABC""BANC"
s = "a", t = "a""a"
s = "a", t = "aa"""

Constraints:

m == s.length

n == t.length

1 ≤ m, n ≤ 105

s and t consist of uppercase and lowercase English letters.

Abstraction

Given two strings s and t, of lengths m and n, return the minimum substring of s, such that every character in t including duplicates is within the window.

Pseudocode

  oh! pseudocode hasn't been written yet, try another card! :)

Solution 1: [Sliding Window] Needle and Window Count HashMap with Have and Need Flag Indicate Valid Window Shrink While Valid - Sliding Window/Fixed Size Window

    def checkInclusion(self, s: str, t: str) -> bool:

        # Sliding Window (Fixed Size Window)

        # Window Representation:
        #   - Two Hashmaps:
        #       needleFreq -> required counts
        #       windowFreq -> counts in current window
        #   - Valid window has equal char counts needle chars between needle and window freq

        # Idea:
        #   - Expand variable size window over list by extending right
        #   - Track how many required character are satisfied via needleFreq
        #   - Add new char count to windowFreq
        #   - While chars between needle and window freq are satisfied, 
        #     shrink from left to minimize window
        #   - For each valid window compare size to min window

        # haystack < needle: 
        # If haystack is smaller than needle, 
        # substring is not possible
        if len(s) < len(t):
            return ""

        # Build needle character map
        # sc: O(m)
        needleCharCount = defaultdict(int)
        for c in t:
            needleCharCount[c] += 1

        # Sliding Window Variables
        left = 0
        right = 0

        # Char count for current window
        windowCharCount = defaultdict(int)

        # Length and starting index for min window
        minLen = float('inf')
        minStart = 0

        # Flags for unique chars slots from needle hashmap length
        needCharSlots = len(needleCharCount) 
        haveCharSlots = 0                 

        # tc: O(n)
        for right in range(len(s)):

            # Expand window
            rightChar = s[right]
            windowCharCount[rightChar] += 1

            # If char is a specific slot char
            if rightChar in needleCharCount:

                # If char has met requirements
                if windowCharCount[rightChar] == needleCharCount[rightChar]:
                    haveCharSlots += 1


            # Shrink from left while window is valid:
            #   - All specific slot chars have met requirements
            while haveCharSlots == needCharSlots:
                
                # Valid window, compare to min
                windowLen = right - left + 1                
                if windowLen < minLen:
                    minLen = windowLen
                    minStart = left

                # Remove left character
                leftChar = s[left]
                windowCharCount[leftChar] -= 1
                
                # If char is a slot char with requirements
                if leftChar in needleCharCount:

                    # If char no longer meets requirements
                    if  windowCharCount[leftChar] < needleCharCount[leftChar]:
                        haveCharSlots -= 1

                # Shrink window
                left += 1

        # If no valid substring was found
        if minLen == float("inf"):
            return ""

        # Grab min substring
        res = s[minStart:minStart + minLen]

        # overall: tc O(n + m)
        # overall: sc O(n)
        return res

Solution 2: [Sliding Window] Needle Hashmap And stillNeedThisMany And GlobalRemainingCountAcrossChars Indicate Valid Window Shrink While Valid - Sliding Window/Variable Size Window

    def minWindow(self, s: str, t: str) -> str:

        # Sliding Window (Variable Size Window)

        # Substring Representation:
        #   - One HashMap: 
        #       char -> remaining required count
        #   - targetCharsRemaining: tracks total chars with requirements still needed

        # Idea:
        #   - Expand variable size window over list by extending right
        #   - Update char remaining needed count
        #   - 
        #   - Decrease count of remaining required count
        #   - When targetCharsRemaining == 0, window is valid
        #   - Shrink from left to minimize window
        #   - Track smallest valid window

        # Needle is longer than haystack:
        # substring not possible
        if len(s) < len(t):
            return ""

        # Build required frequency map
        windowCharCount = defaultdict(int)
        for ch in t:
            windowCharCount[ch] += 1

        # Total chars still needed
        globalRemainingCountAcrossChars = len(t)         

        # Sliding Window Variables
        left = 0
        right = 0

        # Min window substring
        minWindowRight = float('inf')
        minWindowLeft = 0          

        # tc: O(n)
        for right in range(len(s)):

            # Expand window
            rightChar = s[right]
            
            # windowCharCount:
            # To start, hashmap will contains:
            #   - positive values of required needle chars
            #   - all other possible keys are set to 0
            # Moving forward, all other possible keys will 
            # always be less than or equal to 0 (regardless of adding or removing).
            # Thus, to track chars that are in the needle,
            # we only count chars that at some point have a positive count,
            # to contribute towards decreasing or increasing the global remaining 
            stillNeedThisMany = windowCharCount[rightChar]

            if stillNeedThisMany > 0:
                globalRemainingCountAcrossChars -= 1

            # Increase char count for windowCharCount 
            # regardless of it being a required or non-required char,
            # both cases covered per above
            windowCharCount[rightChar] -= 1

            # Shrink from left while window is valid:
            #   - Total needle char count is covered
            # tc: amortized O(n)
            while globalRemainingCountAcrossChars == 0:

                # Valid window, compare to min
                currWindowLen = right - left + 1
                minWindowLen = minWindowRight - minWindowLeft + 1
                if currWindowLen < minWindowLen:
                    minWindowRight = right
                    minWindowLeft = left

                # Remove left character, and to char required count,
                # again per above:
                #   - required needle chars have a positive required count
                #   - non-required chars have a required count of 0
                leftChar = s[left]
                windowCharCount[leftChar] += 1

                # If this char has a positive required count:
                #   - it is a required needle char
                #   - we need one more copy of the char, since we just lost a occurrence of it
                #   - add to global need count
                stillNeedThisMany = windowCharCount[leftChar]

                if stillNeedThisMany > 0:
                    globalRemainingCountAcrossChars += 1

                # Shrink window
                left += 1

        # No valid window was found
        if minWindowRight == float("inf"):
            return ""

        # Grab min window
        res = s[minWindowLeft:minWindowRight + 1]

        # overall: tc O(n + m)
        # overall: sc O(n)
        return res

1234. Replace the Substring for Balanced String :::: - Medium

Topics: String, Sliding Window

Intro

You are given a string s of length n containing only four kinds of characters: 'Q', 'W', 'E', and 'R'. A string is said to be balanced if each of its characters appears n / 4 times where n is the length of the string. Return the minimum length of the substring that can be replaced with any other string of the same length to make s balanced. If s is already balanced, return 0.

Example InputOutput
s = "QWER"0
s = "QQWE"1
s = "QQQW"2

Constraints:

n == s.length

4 ≤ n ≤ 10^5

n is a multiple of 4

s contains only 'Q', 'W', 'E', and 'R'.

Abstraction

Given a string containing only 'QWER', return the minimum substring length that can be replaced to make the string balanced. A balanced string has each of the 4 characters appear exactly n/4 times, or in other words, each character appears no more than n/4 times.

Pseudocode

  oh! pseudocode hasn't been written yet, try another card! :)

Solution 1: [Sliding Window] Full Frequency HashMap Tracking All Chars Outside Window - Sliding Window/Variable Size Window

    def balancedString(self, s: str) -> int:

        # Sliding Window (Variable Size Window)

        # String Representation:
        #   - HashMap tracks char count OUTSIDE window
        #   - Valid window makes up for uneven outside count by changing chars INSIDE the window

        # Idea:
        #   - Expand variable size window over list by extending right
        #   - Remove chars from OUTSIDE count when they enter the window
        #   - Once all outside frequencies <= target, window is valid
        #   - For each valid window compare size to min window
        
        #   s = "QQWE", target = 1
        #   outsideCount = {Q:2, W:1, E:1, R:0}
        
        # When window is at the first Q,
        # that Q is remove from the OUTSIDE char count,
        # leaving the max(outsideCount.values()) -> 1
        # which is equal to the target == 1,
        # meaning that the current window is the smallest possible section
        # we can replace to make the entire string balanced

        n = len(s)

        # Target char count:
        #   - s length is multiple of 4, and has 4 possible characters (Q W E R)
        #   - balanced s has each char appear n // 4 times
        target = n // 4

        # Sliding Window Boundaries
        left = 0
        right = 0

        # Outside HashMap:
        #   - Stores frequencies of chars OUTSIDE current window
        outsideCount = defaultdict(int)
        for c in s:
            outsideCount[c] += 1

        # Minimum valid window size
        minLen = float('inf')

        # Balanced Check:
        #   - Current window length is 0
        #   - All chars are outside the window (we are checking the original string)
        #   - If balanced value == target, then the string is already balanced
        # tc: O(1)
        if max(outsideCount.values()) == target:
            return 0

        # Expand sliding window
        # tc: O(n)
        for right in range(n):

            # Add char to window,
            # remove new char from OUTSIDE count
            rightChar = s[right]
            outsideCount[rightChar] -= 1

            # Shrink from left while window is valid: 
            #   - OUTSIDE count balanced value is equal to the target  
            #   - string is balanced
            #   - left has not passed right (left and catch up to right, leaving a window length of 1)
            # tc: O(1) amortized
            while left <= right and max(outsideCount.values()) == target:

                # Valid window, compare to min
                currWindowLen = right - left + 1
                minLen = min(minLen, currWindowLen)

                # Remove char from window,
                # add char back to OUTSIDE count
                leftChar = s[left]
                outsideCount[leftChar] += 1

                # Shrink window
                left += 1

        # overall: tc O(n)
        # overall: sc O(1)
        return minLen

862. Shortest Subarray with Sum at Least K ::1:: - Hard

Topics: Array, Binary Search, Queue, Sliding Window, Heap (Priority Queue), Prefix Sum, Monotonic Queue

Intro

Given an integer array nums and an integer k, return the length of the shortest non-empty subarray of nums with a sum of at least k. If there is no such subarray, return -1. A subarray is a contiguous part of an array.

Example InputOutput
nums = [1], k = 11
nums = [1,2], k = 4-1
nums = [2,-1,2], k = 33

Constraints:

1 ≤ nums.length ≤ 10^5

-10^5 ≤ nums[i] ≤ 10^5

1 ≤ k ≤ 10^9

Abstraction

wow! again

Pseudocode

  oh! pseudocode hasn't been written yet, try another card! :)

Solution 1: [Sliding Window] [Monotonic] [Deque] Prefix Sum With Monotonic Deque - Sliding Window/Variable Size Window

    def shortestSubarray(self, nums: List[int], k: int) -> int:

        # Prefix Sum + Monotonic Increasing Deque

        # Representation:
        #   - Prefix sum for each index in the array
        #   - Subarray sum [i, j] = P[j+1] - P[i]
        #   - Deque stores indices of monotonic increasing prefix sums

        # Idea:
        #   - We want shortest subarray with sum >= k
        #   - Equivalent to finding shortest j - i where P[j] - P[i] >= k
        #   - Maintain monotonic increasing deque of prefix sum indices
        #   - For each j, pop from front when P[j] - P[dq[0]] >= k
        #     (valid subarray found, pop to find even shorter one)
        #   - Pop from back when P[j] <= P[dq[-1]]
        #     (current prefix is smaller, back element can never be a useful left boundary)

        # Why not standard sliding window?
        #   - nums can contain negative numbers
        #   - Expanding window doesn't guarantee increasing sum
        #   - Standard two pointer breaks down with negatives
        #   - Prefix sum + monotonic deque handles negatives correctly

        # Deque Representation:
        #   - Monotonic Increasing order means:
        #     Low -> High
        #
        #   left (low)               right (high)
        #   [ P[2], P[4], P[5], P[7] ]  prefix sums always increasing
        #   [ idx=2, idx=4, idx=5, idx=7 ]  indices always increasing

        n = len(nums)

        # Prefix sum array
        # sc: O(n)
        p = [0] * (n + 1)

        # Build Prefix Sum Array:
        # p[i] = sum of nums[0] to nums[i-1]
        # p[i] = prefixSum of previous index + previous index's value
        #      = p[i-1] + nums[i-1]
        # tc: O(n)
        for i in range(1, n+1):
            p[i] = p[i-1] + nums[i-1]

        # Monotonic Increasing Deque:
        #        
        # dq = [dq[0], dq[1], dq[2], ..., dq[-1]]
        #        ^                           ^
        #  older (left most) index    newer (right most) index
        #      small p[i]                  large p[]

        # sc: O(n)
        dq = deque()

        # Min valid subarray length:
        # set to larger than array to say we have not found a valid array yet
        minLen = float('inf')

        right = 0

        # Need to check up to nth prefix sum (indices 0 through n, n+1 total prefix sums)
        # tc: O(n)
        while right <= n:

            # Shrink from left while window is valid:
            #   - dq[0]: index of the oldest, smallest prefix sum still in the deque
            #   - p[i]: prefix sum up to (but not including) the current index i (sum of nums[0..i-1])
            #   - k: min sum required
            #   - Take the difference between the prefix sums to get the sum of the '+' array in between the indexes
            #
            #   [ --------- +++++++ xxxxxxxxx ]
            #           dq[0]       i
            #                     p[i]
            #
            #   p[i] - dq[0] => results in sum of the '+' area

            # tc: O(1) amortized
            while dq and k <= p[right] - p[dq[0]]:

                # Valid window, compare to min:

                # Grab left index of window
                left = dq[0]

                # Compute length of window:
                #   - no need for (right - left + 1)
                #   - prefixSum array is already 1-indexed relative to nums and accounts for the +1
                subarrayLen = right - left

                # Compare to min
                if subarrayLen < minLen:
                    minLen = subarrayLen

                # Shrink window
                dq.popleft()

            # Maintain Monotonic Increase:
            #   - remove the most recently added prefix sum that are larger than the current prefix
            #   - append newest large prefix sum to deque
            # tc: O(1) amortized
            while dq and p[right] <= p[dq[-1]]:
                dq.pop()

            # Extend window
            dq.append(right)
            right += 1

        # No valid window found
        if minLen == float('inf'):
            return -1

        # overall: tc O(n)
        # overall: sc O(n)
        return minLen

2962. Count Subarrays Where Max Element Appears at Least K Times ::1:: - Medium

Topics: Array, Sliding Window

Intro

You are given an integer array nums and a positive integer k. Return the number of subarrays where the maximum element of nums appears at least k times in that subarray. A subarray is a contiguous sequence of elements within an array.

Example InputOutput
nums = [1,3,2,3,3], k = 26
nums = [1,4,2,1], k = 30

Constraints:

1 ≤ nums.length ≤ 10^5

1 ≤ nums[i] ≤ 10^6

1 ≤ k ≤ 10^5

Abstraction

wow! again

Pseudocode

  oh! pseudocode hasn't been written yet, try another card! :)

Solution 1: [Sliding Window] Sliding Window - Sliding Window/Variable Size Window

    def countSubarrays(self, nums: List[int], k: int) -> int:
        
        # Sliding Window / Variable Size Window

        # Window Representation:
        #   - 

        # Idea:
        #   - Find global maximum element
        #   - Maintain sliding window
        #   - Track frequency of maximum element inside window
        #   - Once max frequency >= k:
        #       every subarray starting from left
        #       and ending at/right of current right is valid

        # Key Observation:
        #
        #   If current window already contains k occurrences of max_num,
        #   then extending further right keeps condition valid
        #
        #       all subarrays:
        #           [left ... right]
        #           [left ... right+1]
        #           ...
        #           [left ... n-1]
        #
        #   Therefore all subarrays from right -> n-1 are valid
        #   Count added: n - right

        n = len(nums)

        # Global maximum element
        maxNum = max(nums)

        # Sliding Window Boundaries
        left = 0
        right = 0

        # Max within current window
        maxCount = 0

        # Total valid subarrays
        count = 0

        # tc: O(n)
        while right < n:

            # Add num count
            if nums[right] == maxNum:
                maxCount += 1

            # Shrink from left while window is valid:
            # - most occurring element appears at least least k times
            # tc: O(1) amortized
            while k <= maxCount:

                # n - right:
                #   - Current window contains k occurrences of max num
                #   - All right extensions keep the condition valid
                #   - Include all subarrays from right to n-1:
                #
                # nums  = [3, 3, 1], k = 2, n = 3
                # index =  0  1  2  3  4
                #          ^  ^
                #          l  r 
                #
                #        = n - right = 3 - 1 = 2 subarrays
                #
                #   left=1: nums[0..1] = [3, 3]             two 3's => value
                #   left=2: nums[0..2] = [3, 3, 1]          two 3's => value

                # tc: O(1)
                count += n - right

                # Remove num count
                if nums[left] == maxNum:
                    maxCount -= 1

                # Shrink window
                left += 1
            
            # Extend window
            right += 1

        # overall: tc O(n)
        # overall: sc O(1)
        return count

992. Subarrays with K Different Integers ::1:: - Hard

Topics: Array, Hash Table, Sliding Window, Counting

Intro

Given an integer array nums and an integer k, return the number of good subarrays of nums. A good array is an array where the number of different integers in that array is exactly k. For example, [1,2,3,1,2] has 3 different integers: 1, 2, and 3. A subarray is a contiguous part of an array.

Example InputOutput
nums = [1,2,1,2,3], k = 27
nums = [1,2,1,3,4], k = 33

Constraints:

1 ≤ nums.length ≤ 2 * 10^4

1 ≤ nums[i], k ≤ nums.length

Abstraction

Get total count of subarrays that have exactly k unique integers.

Pseudocode

  oh! pseudocode hasn't been written yet, try another card! :)

Solution 1: [Sliding Window] Two Pointer Sliding Window atMost() - Sliding Window/Variable Size Window

    def subarraysWithKDistinct(self, nums: List[int], k: int) -> int:
                
        # Sliding Window (Variable Size Window)

        # Substring Representation:
        #   - Maintain window [left, right] representing current subarray
        #   - Window is valid if it contains <= k distinct numbers
        #   - For exactly k distinct: count = atMostK(k) - atMostK(k-1)

        # Idea:
        #   - Use a sliding window to count number of subarrays
        #      with at most k unique integers
        #   - For each right pointer expansion:
        #       1. Add nums[right] to window
        #       2. Shrink left while unique count exceeds k
        #       3. Add window length (right - left + 1) to total count
        #   - Exact k unique subarrays = atMostK(k) - atMostK(k-1)

        # Count of subarrays with at most k unique integers
        def atMostK(nums, currK):

            # Sliding Window Data
            count = defaultdict(int)

            # Unique Values In Window
            uniqueCharCount = 0

            # Sliding Window Variables
            left = 0
            right = 0

            n = len(nums)

            res = 0

            # tc: O(n)
            while right < n:

                # Expand window
                rightNum = nums[right]

                # Check if new num has been found
                if count[rightNum] == 0:
                    uniqueCharCount += 1

                # Increase num count 
                count[rightNum] += 1

                # Shrink from left while window is invalid:
                # - too many unique values
                while currK < uniqueCharCount:

                    # Remove old value
                    leftNum = nums[left]
                    count[leftNum] -= 1

                    # Check if we lost a unique value
                    if count[leftNum] == 0:
                        uniqueCharCount -= 1

                    # Shrink window
                    left += 1

                # Valid window found:
                #   - all subarrays within this valid subarray are valid
                #   - all subarrays are counted depending on where their right ends
                #   - shrink from left allows the length to get count of subarrays
                res += right - left + 1

                # Expand right pointer
                right += 1

            # tc: O(n)
            # sc: O(n)
            return res

        # atMost(k):   counts all subarrays with <= k distinct elements
        # atMost(k-1): counts all subarrays with <= k-1

        # atMost(k) - atMost(k-1) == subarrays with k unique integers
        res = atMostK(nums, k) - atMostK(nums, k-1)

        # overall: tc O(n)
        # overall: sc O(n)
        return res

Solution 2: [Sliding Window] Single Pass, Single Map, Two-Phase Shrink (Optimized) - Sliding Window/Variable Size Window

    def subarraysWithKDistinct(self, nums: List[int], k: int) -> int:

        # Sliding Window (Variable Size Window) — Single Pass, Single Map

        # Window Representation:
        #   - Maintain one window [left, right] with one frequency map
        #   - distinct: number of distinct values currently in window
        #   - prefix: number of "free" extra left positions that also
        #     give exactly k distinct, beyond the minimal left boundary

        # Idea (Two-Phase Shrink):
        #   Phase 1 — forced shrink while distinct > k (standard invalid shrink):
        #     moves left until window has at most k distinct; resets prefix
        #     once, after shrinking, since the window boundary genuinely changed
        #   Phase 2 — opportunistic shrink while distinct == k:
        #     keep removing nums[left] as long as its frequency > 1
        #     (removing it won't drop distinct count below k)
        #     each such removal is one more valid left boundary -> prefix += 1

        # Why Phase 2 must run EVERY iteration where distinct == k:
        #   Adding a DUPLICATE value at right increases that value's frequency
        #   inside the window — which can retroactively make the current left
        #   element removable, even though `distinct` itself didn't change.
        #   So Phase 2 must re-attempt shrinking every time distinct == k,
        #   not just the first time k is reached.

        # Window Example:
        #   nums = [1, 2, 1, 2, 3], k = 2
        #
        #   right=0 (1): distinct=1, not k, ans += 0
        #   right=1 (2): distinct=2 == k, freq[nums[left]]=freq[1]=1, not >1, no shrink
        #                ans += prefix(0) + 1 = 1         -> ans=1
        #   right=2 (1, dup): distinct stays 2, freq[nums[left=0]]=freq[1]=2 > 1, shrink:
        #                left=1, prefix=1
        #                ans += prefix(1) + 1 = 2          -> ans=3
        #   right=3 (2, dup): distinct stays 2, freq[nums[left=1]]=freq[2]=2 > 1, shrink:
        #                left=2, prefix=2
        #                ans += prefix(2) + 1 = 3           -> ans=6
        #   right=4 (3, new): distinct=3 > k, Phase 1 shrink:
        #                remove nums[2]=1, freq[1]->0, distinct=2, left=3, prefix=0
        #                Phase 2: freq[nums[left=3]]=freq[2]=1, not >1, no shrink
        #                ans += prefix(0) + 1 = 1            -> ans=7
        #
        #   ans = 7  (matches expected output for this input)

        n = len(nums)

        # Frequency of each value in current window
        # sc: O(k)
        freq = defaultdict(int)

        # Sliding Window Boundary
        left = 0

        # Number of extra "free" valid left starts for the current right
        prefix = 0

        # Number of distinct values in window
        distinct = 0

        # Total valid subarrays found
        ans = 0

        # tc: O(n)
        for right in range(n):

            # Check if new distinct value found
            if freq[nums[right]] == 0:
                distinct += 1
            freq[nums[right]] += 1

            # Phase 1 — Shrink from left while window is invalid:
            # - too many distinct values (distinct > k)
            # - boundary genuinely moved, so reset prefix once after shrinking
            # tc: O(1) amortized
            if distinct > k:
                while distinct > k:
                    freq[nums[left]] -= 1

                    if freq[nums[left]] == 0:
                        distinct -= 1

                    left += 1
                prefix = 0

            # Phase 2 — window has exactly k distinct:
            # opportunistically shrink further while it doesn't cost distinctness
            # tc: O(1) amortized
            while distinct == k and freq[nums[left]] > 1:
                freq[nums[left]] -= 1
                left += 1
                prefix += 1

            # All left positions from (left - prefix) to left are valid starts
            if distinct == k:
                ans += prefix + 1

        # overall: tc O(n)
        # overall: sc O(k)
        return ans

1248. Count Number of Nice Subarrays ::3:: - Medium

Topics: Array, Hash Table, Math, Sliding Window, Prefix Sum

Intro

Given an array of integers nums and an integer k. A continuous subarray is called nice if there are k odd numbers on it. Return the number of nice sub-arrays.

Example InputOutput
nums = [1,1,2,1,1], k = 32
nums = [2,4,6], k = 10

Constraints:

1 ≤ nums.length ≤ 50000

1 ≤ nums[i] ≤ 10^5

1 ≤ k ≤ nums.length

Abstraction

wow! again

Pseudocode

  oh! pseudocode hasn't been written yet, try another card! :)

Solution 1: [Sliding Window] Two Pointer Sliding Window atMost() [SC Opt] - Sliding Window/Variable Size Window

    def numberOfSubarrays(self, nums: List[int], k: int) -> int:

        # Sliding Window (Variable Size Window)

        # Window Representation:
        #   - Maintain window [left, right]
        #   - oddCount tracks number of odd numbers in current window

        # Idea:
        #   - Directly counting subarrays with exactly k odds is hard
        #     with two pointers because we can't know when to stop shrinking
        #   - Instead, count subarrays with at most k odds via atMost()
        #   - exactly(k) = atMost(k) - atMost(k-1)

        # Why atMost works:
        #   - atMost(k)     counts subarrays with 0, 1, 2, ... k odds
        #   - atMost(k-1) counts subarrays with 0, 1, 2, ... k-1 odds
        #   - subtracting leaves only subarrays with exactly k odds

        # Window Example:+
        #   nums = [1, 1, 2, 1, 1], k = 3
        #
        #   atMost(3): all windows with oddCount <= 3
        #   [1, 1, 2, 1, 1]
        #    LR              oddCount=1, res += 1
        #    L  R            oddCount=2, res += 2
        #    L     R         oddCount=2, res += 3
        #    L        R      oddCount=3, res += 4
        #    L           R   oddCount=4 > 3, shrink -> left=1, oddCount=3, res += 4
        #   atMost(3) = 14
        #
        #   atMost(2): all windows with oddCount <= 2
        #   [1, 1, 2, 1, 1]
        #    LR              oddCount=1, res += 1
        #    L  R            oddCount=2, res += 2
        #    L     R         oddCount=2, res += 3
        #    L        R      oddCount=3 > 2, shrink -> left=1, oddCount=2, res += 3
        #       L        R   oddCount=3 > 2, shrink -> left=2, oddCount=2, res += 3
        #   atMost(2) = 12
        #
        #   exactly(3) = atMost(3) - atMost(2) = 14 - 12 = 2
        #   valid subarrays: [1,1,2,1] and [1,1,2,1,1]

        def atMost(k: int) -> int:

            # Sliding Window Boundaries
            # sc: O(1)
            left = 0
            right = 0

            n = len(nums)

            # Sliding Window Data:
            # Number of odds in current window
            # sc: O(1)
            oddNums = 0

            # Total valid subarrays found
            res = 0

            # tc: O(n)
            while right < n:

                # Add num, check if odd
                # tc: O(1)
                if nums[right] % 2 == 1:
                    oddNums += 1

                # Shrink from left while window is invalid:
                # Shrink from left until oddCount <= k
                # tc: O(1) amortized
                while k < oddNums:
                    
                    # Remove left, check if odd
                    if nums[left] % 2 == 1:
                        oddNums -= 1

                    # Iterate window
                    left += 1

                # Valid window found:
                #   - all subarrays within this valid subarray are valid
                #   - all subarrays are counted depending on where their right ends
                #   - shrink from left allows the length to get count of subarrays                # tc: O(1)
                res += right - left + 1

                # Iterate window
                right += 1

            # tc: O(n)
            # sc: O(n)
            return res

        # exactly(k) = atMost(k) - atMost(k-1)
        res = atMost(k) - atMost(k-1)

        # overall: tc O(n)
        # overall: sc O(1)
        return res

Solution 2: [Sliding Window] Single Pass With Start Gap [TC Opt] - Sliding Window/Variable Size Window

    def numberOfSubarrays(self, nums: List[int], k: int) -> int:

        # Sliding Window (Variable Size Window)

        # Window Representation:
        #   - Maintain window [lp, rp]
        #   - counter: number of odd numbers in current window
        #   - start_gap: number of valid left boundaries for current window

        # Idea:
        #   - Expand right to add new elements
        #   - Once counter == k, shrink from left counting how many positions
        #     keep counter == k (each is a valid left boundary)
        #   - start_gap persists across iterations: once we've found valid
        #     left boundaries for a window with k odds, any future right
        #     expansion that doesn't add another odd number still has the
        #     same valid left boundaries
        #   - Add start_gap to total at every right, not just when counter == k

        # Why start_gap persists:
        #   nums = [1, 1, 2, 1, 1], k = 3
        #
        #   rp=3 (odd): counter=3 == k, start_gap reset to 0
        #   [1, 1, 2, 1, 1]
        #    L           R
        #   shrink: lp=0 (odd) -> counter=2, start_gap=1
        #   [1, 1, 2, 1, 1]
        #       L        R
        #   counter != k, stop shrinking. total += 1
        #
        #   rp=4 (odd): counter=3 == k, start_gap reset to 0
        #   [1, 1, 2, 1, 1]
        #       L           R
        #   shrink: lp=1 (odd) -> counter=2, start_gap=1
        #   [1, 1, 2, 1, 1]
        #          L        R
        #   counter != k, stop shrinking. total += 1
        #
        #   total = 2
        #   valid subarrays: [1,1,2,1] and [1,1,2,1,1]

        # Single pass — no need to call atMost twice

        # Sliding Window Boundaries
        # sc: O(1)
        lp = 0

        # Number of odd numbers in current window
        counter = 0

        # Number of valid left boundaries for current window
        start_gap = 0

        # Total valid subarrays found
        total = 0

        # tc: O(n)
        for rp in range(len(nums)):

            # Add incoming element to window
            # tc: O(1)
            if nums[rp] % 2 != 0:
                counter += 1

            # Once counter == k, count valid left boundaries
            # Reset start_gap since the new odd resets the boundary count
            # tc: O(1) amortized
            if counter == k:
                start_gap = 0

                # Shrink from left while window is valid:
                # - counter == k
                # Each shrink that keeps counter == k is another valid left boundary
                while counter == k:
                    start_gap += 1
                    if nums[lp] % 2 != 0:
                        counter -= 1
                    lp += 1

            # start_gap persists: even if right adds an even number,
            # the same left boundaries remain valid from the previous valid window
            # tc: O(1)
            total += start_gap

        # overall: tc O(n)
        # overall: sc O(1)
        return total

Solution 3: [Sliding Window] Deque + Threshold (Reference: index-arithmetic variant) - Sliding Window/Variable Size Window

    def numberOfSubarrays(self, nums: List[int], k: int) -> int:

        # Sliding Window (Variable Size Window) + Deque

        # Window Representation:
        #   - ones_indicies: deque holding indices of the last k odd numbers seen
        #                    (once k odds are tracked, the oldest odd index gets evicted)
        #   - threshold: index of the odd number just evicted (the boundary
        #                marking how far left a valid window could start)
        #   - count_ones: number of odd numbers currently tracked (capped at k)

        # Idea:
        #   - Scan right pointer j across nums
        #   - Every time we see an odd number, record its index
        #   - If k odds are already tracked, evict the oldest odd index into
        #     threshold and drop count_ones by 1 (keeps at most k odds tracked)
        #   - Whenever count_ones == k, the window ending at j has exactly k odds;
        #     the number of valid left boundaries is the gap between the
        #     first tracked odd index and threshold
        #   - Add that gap to total at every j where count_ones == k
        #     (same idea as start_gap in Solution 2, computed via index
        #     arithmetic instead of a running counter)

        # Why ones_indicies[0] - threshold works:
        #   - threshold marks the last position that, if included in the window,
        #     would push the odd count above k
        #   - Any left boundary in (threshold, ones_indicies[0]] keeps exactly
        #     k odds in [left, j]: starting any earlier includes one more odd
        #     (pushing past k), and starting later than the first tracked odd
        #     doesn't change the odd count
        #   - So there are (ones_indicies[0] - threshold) valid left starts

        # Window Example:
        #   nums = [1, 1, 2, 1, 1], k = 3
        #
        #   j=0 (odd): ones=[0], count_ones=1
        #   j=1 (odd): ones=[0,1], count_ones=2
        #   j=2 (even): count_ones still 2, skip
        #   j=3 (odd): ones=[0,1,3], count_ones=3 == k
        #              total += ones[0] - threshold = 0 - (-1) = 1   -> total=1
        #   j=4 (odd): count_ones == k already, evict oldest:
        #              threshold = ones.popleft() = 0, count_ones=2
        #              append 4 -> ones=[1,3,4], count_ones=3
        #              total += ones[0] - threshold = 1 - 0 = 1      -> total=2
        #
        #   total = 2
        #   valid subarrays: [1,1,2,1] and [1,1,2,1,1]

        # Total valid subarrays found
        # sc: O(1)
        total = 0

        # Number of odd numbers currently tracked (capped at k)
        # sc: O(1)
        count_ones = 0

        # Indices of the last (at most) k odd numbers seen
        # sc: O(k)
        ones_indicies = deque()

        # Index just left of the earliest odd number still valid as a
        # window start; -1 means no odd has been evicted yet
        # sc: O(1)
        threshold = -1

        # tc: O(n)
        for j in range(len(nums)):

            # Track index of odd numbers only
            # tc: O(1)
            if nums[j] % 2 == 1:
                ones_indicies.append(j)

                # Already have k odds tracked - evict the oldest one
                # so the deque never holds more than k indices
                # tc: O(1) amortized
                if count_ones == k:
                    threshold = ones_indicies.popleft()
                    count_ones -= 1

                count_ones += 1

            # Window ending at j has exactly k odds tracked -
            # count how many left boundaries keep it that way
            # tc: O(1)
            if count_ones == k:
                total += ones_indicies[0] - threshold

        # overall: tc O(n)
        # overall: sc O(k)
        return total

930. Binary Subarrays With Sum ::3:: - Medium

Topics: Array, Hash Table, Sliding Window, Prefix Sum

Intro

Given a binary array nums and an integer goal, return the number of non-empty subarrays with a sum goal. A subarray is a contiguous part of the array.

Example InputOutput
nums = [1,0,1,0,1], goal = 24
nums = [0,0,0,0,0], goal = 015

Constraints:

1 ≤ nums.length ≤ 3 * 10^4

nums[i] is either 0 or 1

0 ≤ goal ≤ nums.length

Abstraction

Find the number of subarrays whose sum equal the target.

Pseudocode

  oh! pseudocode hasn't been written yet, try another card! :)

Solution 1: [Prefix Sum] Prefix Sum With HashMap [Any Input] - Sliding Window/Variable Size Window

    def numSubarraysWithSum(self, nums: List[int], goal: int) -> int:
        
        # Prefix Sum + HashMap

        # Representation:
        #   - prefix_count[s] = number of times prefix sum s has occurred
        #   - curr_sum = running prefix sum up to current index

        # Idea:
        #   - For each index, check how many previous prefix sums
        #     equal curr_sum - goal
        #   - If prefix_count[curr_sum - goal] exists, those subarrays
        #     all sum to goal
        #   - prefix_count[0] = 1 accounts for subarrays starting at index 0

        # goal = 2
        # nums = [1, 0, 1, 0, 1]
        
        # nums = [1, 0, 1, 0, 1]
        # vaild: [.......]
        #        L       R
        #        currSum = 2


        #        exludingSum = 1
        #        [....]       
        # nums = [1, 0, 1, 0, 1]
        # valid:        [......]
        #               L      R
        #               currSum = 3

        # goal - currSum = exludingSum
        # 3 - 2 = 1


        n = len(nums)
        
        # Empty check
        # tc: O(1)
        if n == 0:
            return 0

        # HashMap: prefixSum -> number of occurrences
        # sc: O(n)
        prefixSumCounts = defaultdict(int)

        # Base case: empty prefix has sum 0, seen once
        # tc: O(1)
        prefixSumCounts[0] = 1

        # Running prefix sum
        currSum = 0

        # Total valid subarrays found
        res = 0

        # right boundary of prefixSum [0, right]
        right = 0

        # tc: O(n)
        while right < n:

            # Expand prefix sum
            # tc: O(1)
            currSum += nums[right]

            # If: prefixSum is higher than goal,
            # check if we can restrict left, so that we exclude enough numbers
            # to get our prefixSum back down to goal
            # tc: O(1)
            if currSum >= goal:

                # Check Target:
                # if at any point when iterating left to right,
                # the prefixSum added up to this target
                excludingSum = currSum - goal

                # Check Times:
                # number of times we encountered the target,
                # this tells us how many places we can shift left
                # to exclude numbers to get our prefixSum back down to goal
                excludingSumTimes = prefixSumCounts[excludingSum]

                # Add Times:
                # each location we can move left to,
                # is another prefixSum window that can be restricted via left to achieve goal
                res += excludingSumTimes

            # Record current prefixSum to occurrence count
            # tc: O(1)
            prefixSumCounts[currSum] += 1

            # Move right pointer
            right += 1

        # overall: tc O(n)
        # overall: sc O(n)
        return res

Solution 2: [Prefix Sum] Prefix Sum With Bounded Array [Binary Input Opt] - Sliding Window/Variable Size Window

    def numSubarraysWithSum(self, nums: List[int], goal: int) -> int:
        
        # Prefix Sum + Array of (N+1)

        # Representation:
        #   - prefix_count[s] = number of times prefix sum s has occurred
        #   - curr_sum = running prefix sum up to current index

        # Idea:
        #   - For each index, check how many previous prefix sums
        #     equal curr_sum - goal
        #   - If prefix_count[curr_sum - goal] exists, those subarrays
        #     all sum to goal
        #   - prefix_count[0] = 1 accounts for subarrays starting at index 0

        # goal = 2
        # nums = [1, 0, 1, 0, 1]
        
        # nums = [1, 0, 1, 0, 1]
        # vaild: [.......]
        #        L       R
        #        currSum = 2


        #        exludingSum = 1
        #        [....]       
        # nums = [1, 0, 1, 0, 1]
        # valid:        [......]
        #               L      R
        #               currSum = 3

        # goal - currSum = exludingSum
        # 3 - 2 = 1

        n = len(nums)
        
        # Empty check
        # tc: O(1)
        if n == 0:
            return 0

        # Tracks for up to all possible sums (everything set to 1)
        # how many times we've seen a certain sum
        # sc: O(n)
        prefixSumCount = [0] * (n + 1)

        # Base case: we encounter the sum 0, for the first time
        # tc: O(1)
        prefixSumCount[0] = 1

        # Sum from left up to right
        currSum = 0

        # Total valid subarrays found
        res = 0

        # tc: O(n)
        for num in nums:

            # Expand prefix sum
            # tc: O(1)
            currSum += num

            # If: currSum is higher than goal,
            # check if we can restrict left, so that we exclude enough numbers
            # to get our currSum back down to goal
            # tc: O(1)
            if currSum >= goal:
                
                # Check Target: 
                # if at any point when iterating left to right,
                # the currSum added up to this target
                exludingSum = currSum - goal 

                # Check Times:
                # check number of times we encountered the target,
                # this tells us how many places we can shift left to exlucde numbers to get our currSum back down to goal
                exludingSumTimes = prefixSumCount[exludingSum]

                # Add Times: 
                # each location we can move left to, 
                # is another currSum window that can be restricted via left to achieve goal
                res += exludingSumTimes

            # Record current prefix sum to use in future
            # tc: O(1)
            prefixSumCount[currSum] += 1

        # overall: tc O(n)
        # overall: sc O(n)
        return res

Solution 3: [Sliding Window] Sliding Window With atMost() of Goal Minus atMost() of Goal Minus 1 Helper Only Shrink Left When Surpassed Goal [Binary Input Opt] - Sliding Window/Variable Size Window

    def numSubarraysWithSum(self, nums: List[int], goal: int) -> int:
               
        # Variable Size Window (Expand Then Shrink While Invalid Window)

        # Substring Representation:
        #   - Maintain window [left, right]
        #   - curr_sum tracks sum of current window

        # Idea:
        #   - Directly counting subarrays with sum == goal is hard
        #     with two pointers because we can't know when to stop shrinking
        #   - Instead, count subarrays with sum <= goal via atMost()
        #   - exactly(goal) = atMost(goal) - atMost(goal-1)
        #   - Inside atMost: expand right, shrink left when sum > goal,
        #     right - left + 1 counts all valid subarrays ending at right

        # Monotonic Shrink Advantage:
        # Since we know that when we shrink, the sum decreases predictably,
        # we can safely shrink the window from the left without missing any valid subarrays, 
        # which allows us to the do the goal(i) and goal(i-1) trick   

        # goal = 2
        # nums = [1, 0, 1, 0, 1]
        
        # --------------------------------------
        # atMost(2): all windows with sum <= 2
        # nums = [1, 0, 1, 0, 1]
        #         LR                currSum=1, res += 1 (1 subarray  ending at R)
        #         L  R              currSum=1, res += 2 (2 subarrays ending at R)
        #         L     R           currSum=2, res += 3 (3 subarrays ending at R)
        #         L        R        currSum=2, res += 4 (4 subarrays ending at R)
        #            L        R     currSum=3 -> shrink -> currSum=2, res += 4 (4 subarrays ending at R)
        # atMost(2) total = 1+2+3+4+4 = 14
        #
        # --------------------------------------
        # atMost(1): all windows with sum <= 1
        # nums = [1, 0, 1, 0, 1]
        #         LR                currSum=1, res += 1 (1 subarray  ending at R)
        #         L  R              currSum=1, res += 2 (2 subarrays ending at R)
        #            L  R           currSum=2 -> shrink -> currSum=1, res += 2 (2 subarrays ending at R)
        #            L     R        currSum=1, res += 3 (3 subarrays ending at R)
        #               L     R     currSum=2 -> shrink -> currSum=1, res += 2 (2 subarrays ending at R)
        # atMost(1) total = 1+2+2+3+2 = 10
        #
        # --------------------------------------
        # exactly(2) = atMost(2) - atMost(1) = 14 - 10 = 4
        #   [1, 0, 1]
        #   [1, 0, 1, 0]
        #   [0, 1, 0, 1]
        #   [1, 0, 1]

        # Only valid because nums contains only 0 and 1:
        # shrinking window always decreases sum predictably
        # tc: O(1)
        def atMost(goal) -> int:

            # Guard against negative goal
            if goal < 0:
                return 0

            # Sliding Window Variables
            # sc: O(1)
            left = 0
            right = 0

            currSum = 0
            res = 0

            # tc: O(n)
            for right in range(len(nums)):

                # Expand window
                currSum += nums[right]

                # Shrink from left while window is invalid:
                # Shrink window until sum <= goal
                while goal < currSum:
                    currSum -= nums[left]
                    left += 1

                # All subarrays ending at right with sum <= goal
                # tc: O(1)
                windowLen = right - left + 1

                # Add to total res
                res += windowLen

            return res

        # exactly(goal) = atMost(goal) - atMost(goal-1)

        # overall: tc O(n)
        # overall: sc O(1)
        return atMost(goal) - atMost(goal-1)