Skip to content

Commit 28ded9f

Browse files
committed
Complete Chapter 18 (Design & Caches): LRU, LFU, sharded LRU, iterators, increment stack, O(1) random set — book complete at 18 chapters
1 parent 4ed00ca commit 28ded9f

10 files changed

Lines changed: 1953 additions & 0 deletions

‎CodingInterviewFightClub/src/SUMMARY.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,3 +194,13 @@
194194
- [17.5 Shortest Path Visiting All Nodes](ch17-advanced-graphs/shortest-path-visiting-all-nodes.md)
195195
- [17.6 Reorder Routes To City Zero](ch17-advanced-graphs/reorder-routes-to-make-all-paths-lead-to-city-zero.md)
196196
- [17.7 Evaluate Division](ch17-advanced-graphs/evaluate-division.md)
197+
198+
- [18. Design & Caches](ch18-design-caches/index.md)
199+
- [18.0 Pattern Primer: Composing Structures](ch18-design-caches/pattern-primer.md)
200+
- [18.1 LRU Cache](ch18-design-caches/lru-cache.md)
201+
- [18.2 LFU Cache](ch18-design-caches/lfu-cache.md)
202+
- [18.3 Thread-Safe Sharded LRU](ch18-design-caches/thread-safe-lru-cache.md)
203+
- [18.4 Peeking Iterator](ch18-design-caches/peeking-iterator.md)
204+
- [18.5 Flatten Nested List Iterator](ch18-design-caches/flatten-nested-list-iterator.md)
205+
- [18.6 Design A Stack With Increment Operations](ch18-design-caches/design-a-stack-with-increment-operations.md)
206+
- [18.7 Insert Delete GetRandom O(1)](ch18-design-caches/insert-delete-getrandom.md)
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
# 18.6 Design A Stack With Increment Operations
2+
3+
> **Source:** [`src/main/kotlin/stack/DesignAStackWithIncrementOperations.kt`](https://github.com/arpanpathak/AdvancedAlgorithmPatterns/blob/main/src/main/kotlin/stack/DesignAStackWithIncrementOperations.kt)
4+
> **Pattern:** lazy increment array · **Core page**
5+
6+
## The Problem
7+
8+
Design a stack with `push(x)`, `pop()` and `increment(k, val)` — the last adds `val` to the **bottom k** elements. All operations target **O(1)**.
9+
10+
- Constraints: up to $10^5$ operations; `maxSize` ≤ $10^5$.
11+
12+
## Examples
13+
14+
```
15+
CustomStack(3); push(1); push(2); pop() -> 2; push(2); push(3); push(4) (ignored, full);
16+
increment(5,100); increment(2,100); pop() -> 103; pop() -> 202; pop() -> 201; pop() -> -1
17+
```
18+
19+
## Intuition — don't touch the bottom k elements; *record* the increment and apply it at pop
20+
21+
`increment(k, val)` naively adds `val` to `min(k, size)` elements — O(k) per call. The lazy trick: an **`increments` array parallel to the stack** where `increments[i]` = "the amount to add to the element at index i **when it's popped**". Then `increment(k, val)` is a single write:
22+
23+
```
24+
increments[min(k, size) - 1] += val # O(1) — the bottom k elements are all covered by this
25+
```
26+
27+
**Why does one write cover k elements?** The *prefix* property: an increment at index `k-1` applies to everything below it — because the "carry" moves downward. On `pop()`, the popped element gets `increments[top]`, and the carry is **passed down**:
28+
29+
```
30+
pop():
31+
value = stack.removeLast() + increments[index]
32+
if index > 0: increments[index - 1] += increments[index] # carry to the element below
33+
increments[index] = 0 # reset
34+
return value
35+
```
36+
37+
**Why is the carry correct?** `increment(k, v)` added `v` to elements `0..k-1`. When the element at `k-1` is popped, the elements below (`0..k-2`) still owe `v` — so the increment carries down one slot, where the next pop applies it. Each increment is paid once, at the pop of the highest covered element, then propagated — amortized O(1) per operation.
38+
39+
**The bottom-k semantics:** `increment(k, val)` with `k > size` covers the whole stack — the repo clamps with `min(k, stack.size)`. The "bottom" is index 0, so the write lands at index `clampedK - 1`.
40+
41+
## Approach 1 — Eager increment (O(k) per call)
42+
43+
Loop and add to the bottom k elements: correct, but a sequence of increments is $O(k \cdot n)$.
44+
45+
## Approach 2 — Lazy increment with carry (the repo's version, optimal)
46+
47+
```kotlin
48+
class DesignAStackWithIncrementOperations(maxSize: Int) {
49+
var maxSize = maxSize
50+
var stack = ArrayDeque<Int>()
51+
var increments = IntArray(maxSize)
52+
53+
/** @param x element to push (ignored when full) */
54+
fun push(x: Int) {
55+
if (stack.size < maxSize)
56+
stack.addLast(x)
57+
}
58+
59+
/** @return the popped value (with any pending increments), or -1 when empty */
60+
fun pop(): Int {
61+
if (stack.isEmpty()) return -1
62+
63+
val index = stack.size - 1
64+
val value = stack.removeLast() + increments[index]
65+
66+
if (index > 0) {
67+
increments[index - 1] += increments[index] // carry the increment to the element below
68+
}
69+
increments[index] = 0 // reset after applying
70+
71+
return value
72+
}
73+
74+
/** @param k apply to the bottom k elements
75+
* @param val amount to add (lazily recorded) */
76+
fun increment(k: Int, `val`: Int) {
77+
val limit = minOf(k, stack.size) - 1
78+
if (limit >= 0) {
79+
increments[limit] += `val` // one write covers the bottom k
80+
}
81+
}
82+
}
83+
```
84+
85+
```java
86+
public class CustomStack {
87+
private final int[] stack;
88+
private final int[] increments;
89+
private int top = -1;
90+
91+
/** @param maxSize capacity */
92+
public CustomStack(int maxSize) {
93+
stack = new int[maxSize];
94+
increments = new int[maxSize];
95+
}
96+
97+
/** @param x element to push (ignored when full) */
98+
public void push(int x) {
99+
if (top + 1 < stack.length) stack[++top] = x;
100+
}
101+
102+
/** @return the popped value (with any pending increments), or -1 when empty */
103+
public int pop() {
104+
if (top == -1) return -1;
105+
int value = stack[top] + increments[top];
106+
if (top > 0) increments[top - 1] += increments[top]; // carry to the element below
107+
increments[top] = 0; // reset
108+
top--;
109+
return value;
110+
}
111+
112+
/** @param k apply to the bottom k elements @param val amount to add */
113+
public void increment(int k, int val) {
114+
int limit = Math.min(k, top + 1) - 1;
115+
if (limit >= 0) increments[limit] += val; // one write covers the bottom k
116+
}
117+
}
118+
```
119+
120+
```cpp
121+
#include <vector>
122+
123+
class CustomStack {
124+
std::vector<int> stack;
125+
std::vector<int> increments;
126+
int top = -1;
127+
128+
public:
129+
/** @param maxSize capacity */
130+
CustomStack(int maxSize) : stack(maxSize), increments(maxSize) {}
131+
132+
/** @param x element to push (ignored when full) */
133+
void push(int x) {
134+
if (top + 1 < (int)stack.size()) stack[++top] = x;
135+
}
136+
137+
/** @return the popped value (with any pending increments), or -1 when empty */
138+
int pop() {
139+
if (top == -1) return -1;
140+
int value = stack[top] + increments[top];
141+
if (top > 0) increments[top - 1] += increments[top]; // carry to the element below
142+
increments[top] = 0; // reset
143+
top--;
144+
return value;
145+
}
146+
147+
/** @param k apply to the bottom k elements @param val amount to add */
148+
void increment(int k, int val) {
149+
int limit = std::min(k, top + 1) - 1;
150+
if (limit >= 0) increments[limit] += val; // one write covers the bottom k
151+
}
152+
};
153+
```
154+
155+
```python
156+
class CustomStack:
157+
"""@param max_size: capacity"""
158+
159+
def __init__(self, max_size: int):
160+
self.stack = []
161+
self.inc = [0] * max_size
162+
self.max_size = max_size
163+
164+
def push(self, x: int) -> None:
165+
"""@param x: element to push (ignored when full)"""
166+
if len(self.stack) < self.max_size:
167+
self.stack.append(x)
168+
169+
def pop(self) -> int:
170+
"""@return: the popped value (with any pending increments), or -1 when empty"""
171+
if not self.stack:
172+
return -1
173+
i = len(self.stack) - 1
174+
value = self.stack.pop() + self.inc[i]
175+
if i > 0:
176+
self.inc[i - 1] += self.inc[i] # carry the increment to the element below
177+
self.inc[i] = 0 # reset after applying
178+
return value
179+
180+
def increment(self, k: int, val: int) -> None:
181+
"""@param k: apply to the bottom k elements @param val: amount to add"""
182+
limit = min(k, len(self.stack)) - 1
183+
if limit >= 0:
184+
self.inc[limit] += val # one write covers the bottom k
185+
```
186+
187+
```rust
188+
struct CustomStack {
189+
stack: Vec<i32>,
190+
inc: Vec<i32>,
191+
max_size: usize,
192+
}
193+
194+
impl CustomStack {
195+
/// @param max_size capacity
196+
fn new(max_size: i32) -> Self {
197+
CustomStack { stack: Vec::new(), inc: vec![0; max_size as usize], max_size: max_size as usize }
198+
}
199+
200+
/// @param x element to push (ignored when full)
201+
fn push(&mut self, x: i32) {
202+
if self.stack.len() < self.max_size {
203+
self.stack.push(x);
204+
}
205+
}
206+
207+
/// @return the popped value (with any pending increments), or -1 when empty
208+
fn pop(&mut self) -> i32 {
209+
let Some(x) = self.stack.pop() else { return -1; };
210+
let i = self.stack.len(); // index AFTER pop = position of x
211+
let value = x + self.inc[i];
212+
if i > 0 {
213+
self.inc[i - 1] += self.inc[i]; // carry the increment to the element below
214+
}
215+
self.inc[i] = 0; // reset after applying
216+
value
217+
}
218+
219+
/// @param k apply to the bottom k elements @param val amount to add
220+
fn increment(&mut self, k: i32, val: i32) {
221+
let limit = (k as usize).min(self.stack.len()).saturating_sub(1);
222+
self.inc[limit] += val; // one write covers the bottom k
223+
}
224+
}
225+
```
226+
227+
## Dry run
228+
229+
**Input:** the example sequence: `CustomStack(3)`.
230+
231+
```
232+
push(1): stack=[1]. push(2): stack=[1,2]. pop(): i=1, value=2+inc[1]=2. inc=[0,0,0]. -> 2
233+
push(2): [1,2]. push(3): [1,2,3]. push(4): full -> ignored.
234+
increment(5,100): limit = min(5,3)-1 = 2. inc[2] += 100 -> inc=[0,0,100]
235+
increment(2,100): limit = min(2,3)-1 = 1. inc[1] += 100 -> inc=[0,100,100]
236+
pop(): i=2, value=3+100=103. carry: inc[1] += 100 -> inc=[0,200,0]. -> 103 ✓
237+
pop(): i=1, value=2+200=202. carry: inc[0] += 200 -> inc=[200,0,0]. -> 202 ✓
238+
pop(): i=0, value=1+200=201. -> 201 ✓
239+
pop(): empty -> -1 ✓
240+
```
241+
242+
The two increments (`100` to bottom 5 = all, `100` to bottom 2) stack up as `inc[1]=200` and `inc[2]=100` — and the carry chain pays them in the right order: the top element gets only its own `100`, the middle gets `100+100=200` via the carry, the bottom gets the carried `200`. Each increment is one write; each pop is one read plus one carry.
243+
244+
## Complexity
245+
246+
**Time.** O(1) per operation:
247+
248+
$$
249+
T(n) = O(1) \text{ per operation}
250+
$$
251+
252+
**Space.** The stack + increment array:
253+
254+
$$
255+
S(n) = O(\text{maxSize})
256+
$$
257+
258+
## Variants & follow-ups
259+
260+
- **Range-update / difference-array family** — the same "record the update at the boundary, resolve on read" idea as the difference array in range-sum problems.
261+
- **LFU/LRU caches** ([18.1](lru-cache.md), [18.2](lfu-cache.md)) — deferred bookkeeping in another costume.
262+
- **Interview follow-up:** "Why does one write to `increments[limit]` cover *all* bottom k elements?" The carry moves *downward* on every pop: an increment at index `k-1` applies to that element, then propagates to `k-2` on its pop, then `k-3`, etc. The prefix of k elements is covered by a single write plus the chain — so `increment` is O(1), and each carry is a constant-time step paid once per pop.

0 commit comments

Comments
 (0)