pasricha_dhruv's blog

By pasricha_dhruv, history, 11 months ago, In English

Sliding Window

The sliding window technique is a powerful approach for solving problems involving subarrays. It works by maintaining a subarray (window) of elements and moving (sliding) this window across the data to compute results efficiently.


Introduction

When I first learned the sliding-window trick for sum, it felt like magic: out went an O(n·K) double loop, in came an O(n) solution. Then I tried to do the same for minimum and promptly got stuck. You can’t “subtract” a minimum!

In this tutorial you’ll learn:
1. How invertible operators (sum, XOR) let you slide in O(n)
2. Why non-invertible operators (min, max, GCD…) break the simple trick
3. A general O(n) approach using two stacks and aggregation


1. Invertible Operators: Sum & XOR

If you can undo your operation in O(1), the sliding window is trivial.

1.1 Fixed-Size Window Sum

vector<int> slideSum(const vector<int>& a, int K) {
    int n = a.size(), sum = 0;
    vector<int> ans;
    // initial window
    for (int i = 0; i < K; ++i)
        sum += a[i];
    ans.push_back(sum);
    // slide: add new, subtract old
    for (int i = K; i < n; ++i) {
        sum += a[i];          // enter
        sum -= a[i - K];      // leave
        ans.push_back(sum);
    }
    return ans;
}

1.2 Fixed-Size Window XOR

vector<int> slideXor(const vector<int>& a, int K) {
    int n = a.size(), xr = 0;
    vector<int> ans;
    // initial window
    for (int i = 0; i < K; ++i)
        xr ^= a[i];
    ans.push_back(xr);
    // slide: include new, exclude old
    for (int i = K; i < n; ++i) {
        xr ^= a[i];          // enter
        xr ^= a[i - K];      // leave
        ans.push_back(xr);
    }
    return ans;
}

Why it works: x ^ x = 0, re-XOR the outgoing element undoes it in O(1).


2. The Challenge: Non-Invertible Operators

For Min / Max, Bitwise OR / AND, GCD / LCM etc., there is no efficient “remove” step.

For Min / Max; We can use multisets, but that will make the solution O(N log(N))

For Bitwise OR / AND, GCD / LCM: We can't use multisets to get our answer, we will need some other data structures like sparse table / segment trees.

Let's look at an easier and efficient idea to handle all of these operations:

Insight: A sliding window acts as a queue
- push an element at the back when a new element enters
- pop an element from the front when the old one leaves

What if our queue could also answer “what’s the min?” in O(1)?


3. Implement a Queue with Two Stacks

A simple, classic trick to get amortized O(1) push/pop operations is using two stacks:

#include <stack>
using namespace std;

struct MyQueue {
    stack<int> in, out;

    // Push a new element into the queue.
    void push(int x) {
        in.push(x);
    }

    // Pop the oldest element from the queue.
    void pop() {
        if (out.empty()) {
            while (!in.empty()) {
                out.push(in.top());
                in.pop();
            }
        }
        out.pop();
    }

    // Return the oldest element.
    int front() {
        if (out.empty()) {
            while (!in.empty()) {
                out.push(in.top());
                in.pop();
            }
        }
        return out.top();
    }
};
  • The in stack collects new items.
  • When needing to pop or peek the oldest, items are transferred from in to out.

4. Augmented Stack for Aggregates (Min Stack Problem)

A simplified approach to track the minimum in a stack is to store each value along with the current minimum up to that point:

struct AggStack {
    // Each element is stored as (value, current_min)
    stack<pair<int, int>> st;
    
    // Push a new number; compute the new min.
    void push(int x) {
        int cur = st.empty() ? x : min(st.top().second, x);
        st.push({x, cur});
    }
    
    // Pop the top element.
    void pop() {
        st.pop();
    }
    
    // Return the current minimum.
    int agg() const {
        return st.top().second;
    }
};

Each push/pop here works in O(1).


5. Aggregated Queue & Sliding-Window Minimum

We now combine two augmented stacks to create an aggregated queue that can give the current minimum in O(1):

struct AggQueue {
    AggStack in, out;
    
    // Push a new number into the queue.
    void push(int x) {
        in.push(x);
    }
    
    // Pop the oldest number.
    void pop() {
        if (out.st.empty()) {
            while (!in.st.empty()) {
                int v = in.st.top().first;
                in.pop();
                out.push(v);
            }
        }
        out.pop();
    }
    
    // Query the current minimum.
    int query() const {
        if (in.st.empty()) return out.agg();
        if (out.st.empty()) return in.agg();
        return min(in.agg(), out.agg());
    }
};
vector<int> slideMin(const vector<int>& a, int K) {
    int n = a.size();
    vector<int> ans;
    AggQueue mq;  // Our aggregated queue maintains the minimum
    
    // Build the initial window of size K.
    for (int i = 0; i < K; i++) {
        mq.push(a[i]);
    }
    ans.push_back(mq.query());
    
    // Slide the window: add a new element and remove the oldest.
    for (int i = K; i < n; i++) {
        mq.push(a[i]);   // add new element
        mq.pop();        // remove element that's left the window
        ans.push_back(mq.query());
    }
    
    return ans;
}

Note: You can modify the AggStack and AggQueue to work with other aggregates (e.g., maximum, GCD) by replacing min with the appropriate function.


Practice Problems

  • Vote: I like it
  • +97
  • Vote: I do not like it

»
11 months ago, hide # |
 
Vote: I like it 0 Vote: I do not like it

Auto comment: topic has been updated by pasricha_dhruv (previous revision, new revision, compare).

»
11 months ago, hide # |
Rev. 2  
Vote: I like it +14 Vote: I do not like it

Nice blog. I usually write min sliding window using a single deque and a time variable: you only store the last k (where k is the window's width) elements but store them in increasing order. This follows a simple idea: if an element at position i is non-greater than some element at an "older" position j < i, then tab[j] will never be a minimum anymore: for all the remaining windows, tab[i] will also be in the window and tab[j] >= tab[i]. Thus we can remove it. Thus, the window operates something like this:

struct MinWindow {
deque<pair<int, int>> s;
int time = 0;
int k = <set me in the constructor or whatever>;

void push(int x){
    //remove older elements that are >= x
    while(!s.empty() && s.back().first >= x) s.pop_back();
    // note that s is increasing after this emplace_back
    s.emplace_back(x, time);
    time++;
}

int get() {
    //remove elements older than k <- this can be maintained in push as well
    while(s.front().second < time - k) {
        s.pop_front();
    }
    return s.front().first;
}
};

For example, say that tab = [5,1,2,8,14,6,10,9,3,4,12,15,7,13,11] and the window width is 5. Then the state of the deque will be the following (say that I invoke get() each time too):

  1. (5, 0)
  2. (1, 1) <-- push(1) popped 5 as 5 >= 1
  3. (1, 1), (2, 2)
  4. (1, 1), (2, 2), (8, 3)
  5. (1, 1), (2, 2), (8, 3), (14, 4)
  6. (1, 1), (2, 2), (6, 5) <-- popped 8 and 14, as 8 >= 6 and 14 >= 6 but 2 < 6
  7. (2, 2), (6, 5), (10, 6) <-- get() popped 1 as it is "too old"
  8. (6, 5), (9, 7) <-- get() popped 2, and push(9) popped 10 >= 9
  9. (3, 8) <-- push(3) popped until deque was empty
  10. (3, 8), (4, 9)
  11. (3, 8), (4, 9), (12, 10)
  12. (3, 8), (4, 9), (12, 10), (15, 11)
  13. (3, 8), (4, 9), (7, 12) <-- push(7) popped 12 and 15
  14. (4, 9), (7, 12), (13, 13) <-- get() popped 3
  15. (7, 12), (11, 14) <-- push(11) popped 13 and get() popped 4.

Each time, the min in sliding window is the first of the front element. Also, note that the monotonicity invariant is maintained throughout the process.

I tried to think if this can be applied to other operations like gcd for example but I didn't figure it out (I didn't spend that much time thinking about it though).

  • »
    »
    10 months ago, hide # ^ |
     
    Vote: I like it 0 Vote: I do not like it

    Yes, this is also a great and elegant approach to compute the min/max in a sliding window efficiently.

    However, for operations like GCD, LCM, Bitwise OR/AND, this monotonic deque trick doesn’t directly apply, mainly because these operations are not monotonic in a way that allows elements to be safely "pruned" just based on comparisons with the new element.

    Still, it would be super cool if someone finds a lazy or approximate structure for such ops. Always looking for new ideas :)

»
10 months ago, hide # |
 
Vote: I like it 0 Vote: I do not like it

Would this work with bitwise OR? I tried implementing it similarly and it didn't work, maybe I misunderstood something?

struct AggregateStack
{
public:
  std::stack<std::pair<int, int>> stack;

  AggregateStack()
  {
  }

  void push(int x)
  {
    int curr_agg = stack.empty() ? x : stack.top().second | x;
    stack.push(std::make_pair(x, curr_agg));
  }

  void pop()
  {
    stack.pop();
  }

  auto aggregate() -> int
  {
    return stack.top().second;
  }
};

struct AggregateQueue
{
private:
  AggregateStack in, out;

public:
  AggregateQueue()
  {
  }

  void push(int x)
  {
    in.push(x);
  }

  void pop()
  {
    if (out.stack.empty())
    {
      while (!in.stack.empty())
      {
        int val = in.stack.top().first;
        in.pop();
        out.push(val);
      }
    }
  }

  auto query() -> int
  {
    if (in.stack.empty())
      return out.aggregate();

    if (out.stack.empty())
      return in.aggregate();

    return in.aggregate() | out.aggregate();
  }
};

I previously solved this using a frequency window for each bit, and unset a bit only if the frequency went to zero on removing a number from the window. Would something similar be needed here?

My CSES submission for reference

  • »
    »
    10 months ago, hide # ^ |
    Rev. 2  
    Vote: I like it -8 Vote: I do not like it

    I'm pretty sure it's because you didn't pop the out stack in AggregateQueue::pop()

    Also #define fn auto to make C++ look more like Rust 😉.

    What's up with these downvotes?

  • »
    »
    10 months ago, hide # ^ |
     
    Vote: I like it 0 Vote: I do not like it

    Yes, this will work for Bitwise OR too.

    Solution
»
9 months ago, hide # |
Rev. 2  
Vote: I like it 0 Vote: I do not like it

Awesome technique, i knew the two stack but never thought of other functions