foreach-ui logo
codeLanguages
account_treeDSA

Quick Actions

quizlock Random Quiz
trending_uplock Progress
  • 1
  • 2
  • 3
  • 4
  • quiz
Algorithms
  • Solve trapping rainwater using left/right max tracking
  • Apply sliding window with frequency counters for substring problems
  • Master minimum window substring with expand-contract strategy
  • Handle duplicates with at-most-k constraint using comparison with k positions back

Advanced Two Pointers Problems

You've seen two pointers solve pairs, triplets, and linked list problems. Now let's tackle problems that seem impossible at first glance—and watch two pointers handle them elegantly.

Trapping Rainwater

Here's a classic problem: you have an array representing elevation heights [0,1,0,2,1,0,1,3,2,1,2,1]. How much rainwater gets trapped between the bars?

     █
   █ ██ █
 █ ████████
0102101 3212 1

Water gets trapped in the valleys. At position 2 (height 0), water fills up to height 2 (limited by the 2 on the left and the 3 on the right), trapping 2 units.

The naive approach calculates the max height to the left and right for each position, then the water at that position is min(left_max, right_max) - height. That's O(n²) or O(n) with extra arrays.

Two pointers solves this in O(n) time with O(1) space:

def trap(heights):
    if not heights:
        return 0
    
    left, right = 0, len(heights) - 1
    left_max = right_max = 0
    water = 0
    
    while left < right:
        if heights[left] < heights[right]:
            # Water at left is limited by left_max
            if heights[left] >= left_max:
                left_max = heights[left]
            else:
                water += left_max - heights[left]
            left += 1
        else:
            # Water at right is limited by right_max
            if heights[right] >= right_max:
                right_max = heights[right]
            else:
                water += right_max - heights[right]
            right -= 1
    
    return water

Why does this work? The key insight: water level at a position is limited by the minimum of the max heights on both sides. When we process the left pointer, we know heights[right] is at least as tall as heights[left], so the right side won't be the limiting factor. We can safely calculate water based on left_max.

For [0,1,0,2,1,0,1,3,2,1,2,1]:

left=0, right=11, left_max=0, right_max=0, water=0
  heights[0]=0 < heights[11]=1
  left_max=0, water+=0, left=1

left=1, right=11, left_max=0, right_max=0, water=0
  heights[1]=1 < heights[11]=1
  Actually equal, so go to else branch
  right_max=1, water+=0, right=10

left=1, right=10, left_max=0, right_max=1, water=0
  heights[1]=1 < heights[10]=2
  left_max=1, water+=0, left=2

left=2, right=10, left_max=1, right_max=1, water=0
  heights[2]=0 < heights[10]=2
  water += 1-0 = 1, left=3

left=3, right=10, left_max=1, right_max=1, water=1
  heights[3]=2 > heights[10]=2
  Equal, else branch
  right_max=2, water+=0, right=9

... continue until left meets right
Final water: 6

Longest Substring Without Repeating Characters

Find the length of the longest substring with no repeating characters. For "abcabcbb", it's "abc" (length 3).

Sliding window approach:

def longest_unique_substring(s):
    if not s:
        return 0
    
    char_set = set()
    left = 0
    max_length = 0
    
    for right in range(len(s)):
        # Shrink window from left while we have a duplicate
        while s[right] in char_set:
            char_set.remove(s[left])
            left += 1
        
        char_set.add(s[right])
        max_length = max(max_length, right - left + 1)
    
    return max_length

For "abcabcbb":

right=0 (a): char_set={a}, max_length=1
right=1 (b): char_set={a,b}, max_length=2
right=2 (c): char_set={a,b,c}, max_length=3
right=3 (a): 'a' in char_set!
  Remove s[0]='a', left=1, char_set={b,c}
  Add 'a', char_set={b,c,a}, max_length=3
right=4 (b): 'b' in char_set!
  Remove s[1]='b', left=2, char_set={c,a}
  Add 'b', char_set={c,a,b}, max_length=3
right=5 (c): 'c' in char_set!
  Remove s[2]='c', left=3, char_set={a,b}
  Add 'c', char_set={a,b,c}, max_length=3
right=6 (b): 'b' in char_set!
  Remove s[3]='a', left=4, char_set={b,c}
  Still 'b' in char_set!
  Remove s[4]='b', left=5, char_set={c}
  Add 'b', char_set={c,b}, max_length=3
right=7 (b): 'b' in char_set!
  Remove s[5]='c', left=6, char_set={b}
  Still 'b' in char_set!
  Remove s[6]='b', left=7, char_set={}
  Add 'b', char_set={b}, max_length=3

Result: 3

The right pointer explores new characters. When we hit a duplicate, the left pointer contracts until the duplicate is removed. The window between them is always a valid substring.

Minimum Window Substring

This is harder. Given string s and string t, find the smallest substring of s that contains all characters from t.

For s="ADOBECODEBANC" and t="ABC", the answer is "BANC".

This combines frequency counting with sliding window:

def min_window(s, t):
    if not t or not s:
        return ""
    
    from collections import Counter
    
    target_count = Counter(t)
    window_count = Counter()
    
    required = len(target_count)  # Unique characters in t
    formed = 0  # How many unique chars in window have desired frequency
    
    left = 0
    min_len = float('inf')
    result = ""
    
    for right in range(len(s)):
        char = s[right]
        window_count[char] += 1
        
        # If this character's count matches target, increment formed
        if char in target_count and window_count[char] == target_count[char]:
            formed += 1
        
        # Contract window from left while it's valid
        while left <= right and formed == required:
            # Update result if this window is smaller
            if right - left + 1 < min_len:
                min_len = right - left + 1
                result = s[left:right + 1]
            
            # Remove leftmost character from window
            left_char = s[left]
            window_count[left_char] -= 1
            if left_char in target_count and window_count[left_char] < target_count[left_char]:
                formed -= 1
            
            left += 1
    
    return result

For s="ADOBECODEBANC", t="ABC":

Expand right until we have all chars (A, B, C):
  right gets to E in ADOBECODE
  Window: "ADOBEC" contains all chars

Contract left while still valid:
  Remove A: "DOBEC" - still has A? No, invalid
  
Expand right again:
  Continue to B: "DOBECOB" - invalid still
  Continue to A: "DOBECOBA" - valid again
  
Contract:
  "OBECOBA", "BECOBA", "ECOBA", "COBA" - invalid
  
Continue expanding to N, C:
  "ODEBANC" - valid
  
Contract:
  "DEBANC", "EBANC", "BANC" - still valid (smallest so far)
  "ANC" - invalid
  
Result: "BANC"

The technique: expand right to make the window valid, then contract left while maintaining validity, tracking the minimum.

3Sum Closest

Find three numbers that sum closest to a target. For nums=[−1,2,1,−4], target=1, the answer is 2 (−1+2+1=2).

This is 3Sum with a twist—instead of finding exact matches, track the closest sum:

def three_sum_closest(nums, target):
    nums.sort()
    closest = float('inf')
    
    for i in range(len(nums) - 2):
        left, right = i + 1, len(nums) - 1
        
        while left < right:
            current = nums[i] + nums[left] + nums[right]
            
            # Update closest if this is closer
            if abs(current - target) < abs(closest - target):
                closest = current
            
            # Move pointers based on comparison with target
            if current < target:
                left += 1
            elif current > target:
                right -= 1
            else:
                return current  # Exact match
    
    return closest

For [−1,2,1,−4], target=1:

Sort: [−4,−1,1,2]

i=0 (−4):
  left=1 (−1), right=3 (2)
  sum = −4+(−1)+2 = −3, |−3−1|=4, closest=−3
  −3 < 1, left++
  
  left=2 (1), right=3 (2)
  sum = −4+1+2 = −1, |−1−1|=2, closest=−1
  −1 < 1, left++
  
  left=3, left>=right, done

i=1 (−1):
  left=2 (1), right=3 (2)
  sum = −1+1+2 = 2, |2−1|=1, closest=2 (better!)
  2 > 1, right--
  
  left=2, left>=right, done

Result: 2

Remove Duplicates, Allow Two

You have a sorted array. Remove duplicates so each element appears at most twice. For [1,1,1,2,2,3], the result is [1,1,2,2,3,_].

The trick: compare with the element two positions back. If they're the same, it means we already have two copies.

def remove_duplicates_ii(nums):
    if len(nums) <= 2:
        return len(nums)
    
    slow = 2  # Position for next element
    
    for fast in range(2, len(nums)):
        # Only place if different from element 2 positions back
        if nums[fast] != nums[slow - 2]:
            nums[slow] = nums[fast]
            slow += 1
    
    return slow

For [1,1,1,2,2,3]:

slow=2, fast=2: nums[2]=1 == nums[0]=1? Yes, skip
slow=2, fast=3: nums[3]=2 != nums[1]=1, place
  nums[2]=2, slow=3, array=[1,1,2,2,2,3]
slow=3, fast=4: nums[4]=2 == nums[2]=2? Yes, skip
slow=3, fast=5: nums[5]=3 != nums[3]=2, place
  nums[3]=3, slow=4, array=[1,1,2,3,2,3]

Return 4, first 4 elements: [1,1,2,3]

Wait, that's wrong. Let me retrace:

Initial: [1,1,1,2,2,3]
slow=2

fast=2: nums[2]=1 vs nums[0]=1, equal, skip
fast=3: nums[3]=2 vs nums[1]=1, different
  nums[2]=2, slow=3
  Array: [1,1,2,2,2,3]
fast=4: nums[4]=2 vs nums[2]=2, equal, skip  
fast=5: nums[5]=3 vs nums[3]=2, different
  nums[3]=3, slow=4
  Array: [1,1,2,3,2,3]

Return 4
First 4 elements: [1,1,2,3]

Hmm, we want [1,1,2,2,3]. Let me check the logic again. We're comparing nums[fast] with nums[slow-2]. At slow=3, we compare with nums[1]=1. At slow=4, we compare with nums[2]=2. So when fast=4 (value 2), we compare with nums[2]=2, which are equal, so we skip. That means we only keep two 2s. This is correct!

Actually reviewing: when slow=2 and we place the first 2, slow becomes 3. Now nums[0]=1, nums[1]=1, nums[2]=2. When fast=4 (another 2), we compare with nums[slow-2]=nums[1]=1, which is different, so we'd place it.

Let me trace more carefully:

[1,1,1,2,2,3], slow=2

fast=2: nums[2]=1, nums[slow-2]=nums[0]=1, equal, skip
fast=3: nums[3]=2, nums[slow-2]=nums[0]=1, different, place
  nums[slow]=nums[2]=2, slow=3
  [1,1,2,2,2,3]
fast=4: nums[4]=2, nums[slow-2]=nums[1]=1, different, place
  nums[slow]=nums[3]=2, slow=4
  [1,1,2,2,2,3]
fast=5: nums[5]=3, nums[slow-2]=nums[2]=2, different, place
  nums[slow]=nums[4]=3, slow=5
  [1,1,2,2,3,3]

Return 5: [1,1,2,2,3]

Perfect! By comparing with the element 2 positions back, we ensure we never have more than 2 duplicates.

The Pattern

Advanced two pointer problems often combine:

Sliding window for subarray/substring problems where you expand and contract the window.

Frequency tracking with hash maps or counters to handle character counts.

Greedy decisions about which pointer to move based on current state.

Invariant maintenance ensuring the window or pointer relationship stays valid.

The key is recognizing which variant applies and adapting the basic two-pointer template to the specific constraints.

These aren't fundamentally different from the simpler problems—they just require handling more complex state and making smarter decisions about pointer movement.

© 2026 forEach. All rights reserved.

Privacy Policy•Terms of Service