Two pointers give you a range [L, R]. A sliding window is that range plus a payload: whatever you need to know about the cells inside it. The payload is usually a sum, a frequency map, a count of distinct values, or a max-deque.
The motion is always the same. R walks the array once. After each include, you shrink `L` while the window is illegal. Each index enters once and leaves once, so the scan is O(n) times the cost of updating the payload — O(1) for a sum, O(1) amortized for a hash map.
function maxLength(nums, k) {
let L = 0;
let sum = 0;
let best = 0;
for (let R = 0; R < nums.length; R++) {
sum += nums[R]; // expand: include nums[R]
while (L <= R && sum > k) { // shrink while illegal
sum -= nums[L];
L++;
}
best = Math.max(best, R - L + 1);
}
return best;
}The payload is the only thing that changes between window problems. Swap the running sum for a frequency map and you get 'longest substring with at most k distinct characters': increment freq[s[R]] on expand, decrement and maybe delete on shrink, and the illegal test is freq.size > k.
Same skeleton. Different payload. That is the pattern you should recognize in the first twenty seconds.
function longestAtMostKDistinct(s, k) {
const freq = new Map();
let L = 0;
let best = 0;
for (let R = 0; R < s.length; R++) {
freq.set(s[R], (freq.get(s[R]) ?? 0) + 1);
while (freq.size > k) {
const c = s[L++];
const n = freq.get(c) - 1;
if (n === 0) freq.delete(c);
else freq.set(c, n);
}
best = Math.max(best, R - L + 1);
}
return best;
}Checks 0/3