Facebook Pixel

1860. Incremental Memory Leak

MediumMathSimulation
Leetcode Link

Problem Description

You have two memory sticks with memory1 and memory2 bits of available memory respectively. A faulty program is running that consumes an increasing amount of memory each second.

The program allocates memory following these rules:

  • At the i-th second (starting from second 1), the program needs to allocate i bits of memory
  • The allocation goes to the memory stick with more available memory
  • If both sticks have equal available memory, allocate from the first memory stick (memory1)
  • If neither stick has at least i bits available, the program crashes

The process continues second by second:

  • Second 1: allocate 1 bit to the stick with more memory
  • Second 2: allocate 2 bits to the stick with more memory
  • Second 3: allocate 3 bits to the stick with more memory
  • And so on...

You need to return an array [crashTime, memory1_crash, memory2_crash] where:

  • crashTime is the second when the program crashes (when neither stick can accommodate the required memory)
  • memory1_crash is the remaining memory in stick 1 when the crash occurs
  • memory2_crash is the remaining memory in stick 2 when the crash occurs

For example, if memory1 = 2 and memory2 = 2:

  • Second 1: Allocate 1 bit to memory1 (both equal, choose first). Result: memory1 = 1, memory2 = 2
  • Second 2: Allocate 2 bits to memory2 (it has more). Result: memory1 = 1, memory2 = 0
  • Second 3: Need 3 bits but neither stick has enough. Program crashes.
  • Return: [3, 1, 0]
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The problem asks us to simulate a memory allocation process that continues until failure. Since we need to track exactly when the program crashes and the state of both memory sticks at that moment, a direct simulation is the most straightforward approach.

Let's think about what happens at each second:

  • We need i bits at second i
  • We always choose the stick with more available memory (or stick 1 if tied)
  • We continue until we can't allocate anymore

The key insight is that we don't need any complex optimization - we can simply simulate the process second by second. At each second i, we:

  1. Check if we can allocate i bits (at least one stick must have ≥ i bits)
  2. If yes, subtract i from the appropriate stick
  3. If no, we've found our crash time

The simulation naturally terminates because the memory requirement grows linearly (1, 2, 3, 4, ...) while available memory only decreases. Eventually, we'll reach a point where i > max(memory1, memory2), meaning neither stick can accommodate the allocation.

The mathematical bound helps us understand why this simulation is efficient. If we sum up all memory consumed until second t-1, we get 1 + 2 + 3 + ... + (t-1) = t×(t-1)/2. This sum must be less than or equal to the total initial memory memory1 + memory2. This means the crash time t is bounded by approximately √(2×(memory1 + memory2)), ensuring our simulation won't run for too long even with large memory values.

The greedy choice of always picking the stick with more memory is optimal for maximizing the program's runtime, as it helps balance the memory usage between the two sticks, preventing one from depleting too quickly.

Learn more about Math patterns.

Solution Approach

The solution implements a direct simulation of the memory allocation process. We start from second 1 and continue allocating memory until the program crashes.

Implementation Steps:

  1. Initialize the counter: Start with i = 1 to represent the current second (and the amount of memory to allocate).

  2. Main simulation loop: Continue while we can allocate memory

    • The condition i <= max(memory1, memory2) checks if at least one stick has enough memory for the current allocation
    • If neither stick has i bits available, we've reached the crash point
  3. Memory allocation logic: At each second, determine which stick gets the allocation

    • If memory1 >= memory2: Allocate to stick 1 (this handles both the "more memory" case and the tie-breaker rule)
    • Otherwise: Allocate to stick 2
    • Subtract i bits from the chosen stick
  4. Increment and continue: Move to the next second by incrementing i

  5. Return the result: When the loop exits (program crashes), return [i, memory1, memory2]

    • i is the crash time (the second when allocation failed)
    • memory1 and memory2 are the remaining bits in each stick

Time Complexity Analysis:

As mentioned in the reference approach, the total memory consumed up to second t-1 is:

Sum from i=1 to t-1 of i = t × (t-1) / 2 ≤ memory1 + memory2

This means the crash time t is bounded by O(√(memory1 + memory2)), making the time complexity O(√n) where n is the total initial memory.

Space Complexity: O(1) as we only use a constant amount of extra space for the counter variable.

The beauty of this solution lies in its simplicity - we don't need any complex data structures or algorithms, just a straightforward simulation that mirrors the problem description exactly.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through an example with memory1 = 8 and memory2 = 11.

Initial state: memory1 = 8, memory2 = 11

Second 1: Need to allocate 1 bit

  • Compare: memory1 (8) < memory2 (11)
  • Allocate to memory2: 11 - 1 = 10
  • State: memory1 = 8, memory2 = 10

Second 2: Need to allocate 2 bits

  • Compare: memory1 (8) < memory2 (10)
  • Allocate to memory2: 10 - 2 = 8
  • State: memory1 = 8, memory2 = 8

Second 3: Need to allocate 3 bits

  • Compare: memory1 (8) = memory2 (8) → tie, choose memory1
  • Allocate to memory1: 8 - 3 = 5
  • State: memory1 = 5, memory2 = 8

Second 4: Need to allocate 4 bits

  • Compare: memory1 (5) < memory2 (8)
  • Allocate to memory2: 8 - 4 = 4
  • State: memory1 = 5, memory2 = 4

Second 5: Need to allocate 5 bits

  • Compare: memory1 (5) > memory2 (4)
  • Allocate to memory1: 5 - 5 = 0
  • State: memory1 = 0, memory2 = 4

Second 6: Need to allocate 6 bits

  • Check: Can we allocate? max(0, 4) = 4, but we need 6 bits
  • Neither stick has enough memory → CRASH!

Result: Return [6, 0, 4]

  • Crash time: 6
  • memory1 remaining: 0
  • memory2 remaining: 4

This example demonstrates:

  1. How we always choose the stick with more memory
  2. The tie-breaker rule (second 3)
  3. How the program continues until neither stick can accommodate the required allocation
  4. The final state captures exactly when and with what memory values the crash occurs

Solution Implementation

1class Solution:
2    def memLeak(self, memory1: int, memory2: int) -> List[int]:
3        """
4        Simulates a memory leak where increasing amounts of memory are allocated
5        from the stick with more available memory at each time step.
6      
7        Args:
8            memory1: Initial memory available in first memory stick
9            memory2: Initial memory available in second memory stick
10          
11        Returns:
12            List containing [crash_time, remaining_memory1, remaining_memory2]
13        """
14        # Start with time step 1 (amount of memory to allocate)
15        time_step = 1
16      
17        # Continue allocating memory while we can satisfy the current allocation request
18        while time_step <= max(memory1, memory2):
19            # Allocate from the memory stick with more available memory
20            # If tied, allocate from memory1 (has priority)
21            if memory1 >= memory2:
22                memory1 -= time_step
23            else:
24                memory2 -= time_step
25          
26            # Move to next time step (next allocation size)
27            time_step += 1
28      
29        # Return the crash time and remaining memory in both sticks
30        return [time_step, memory1, memory2]
31
1class Solution {
2    /**
3     * Simulates memory allocation until insufficient memory is available.
4     * At each second i, allocates i units of memory from the stick with more available memory.
5     * If both have equal memory, allocates from memory1.
6     * 
7     * @param memory1 Initial available memory in the first memory stick
8     * @param memory2 Initial available memory in the second memory stick
9     * @return Array containing [crashTime, remainingMemory1, remainingMemory2]
10     */
11    public int[] memLeak(int memory1, int memory2) {
12        // Start allocation from second 1
13        int currentSecond = 1;
14      
15        // Continue allocating memory until neither stick can accommodate the current allocation size
16        while (currentSecond <= Math.max(memory1, memory2)) {
17            // Allocate from the memory stick with more available memory
18            // In case of a tie, allocate from memory1
19            if (memory1 >= memory2) {
20                memory1 -= currentSecond;
21            } else {
22                memory2 -= currentSecond;
23            }
24          
25            // Move to the next second
26            currentSecond++;
27        }
28      
29        // Return the crash time and remaining memory in both sticks
30        return new int[] {currentSecond, memory1, memory2};
31    }
32}
33
1class Solution {
2public:
3    vector<int> memLeak(int memory1, int memory2) {
4        // Start with the first allocation size (1 bit)
5        int allocationSize = 1;
6      
7        // Continue allocating memory while at least one stick has enough space
8        // for the current allocation size
9        while (allocationSize <= max(memory1, memory2)) {
10            // Allocate from the stick with more available memory
11            // In case of a tie, prefer memory1 (stick 1)
12            if (memory1 >= memory2) {
13                memory1 -= allocationSize;
14            } else {
15                memory2 -= allocationSize;
16            }
17          
18            // Increment allocation size for next iteration
19            allocationSize++;
20        }
21      
22        // Return the crash time and remaining memory in both sticks
23        // allocationSize represents the first allocation that couldn't be satisfied
24        return {allocationSize, memory1, memory2};
25    }
26};
27
1/**
2 * Simulates memory allocation between two memory banks
3 * Allocates memory in increasing chunks starting from 1 MB
4 * Always allocates from the memory bank with more available space
5 * 
6 * @param memory1 - Available memory in the first bank (in MB)
7 * @param memory2 - Available memory in the second bank (in MB)
8 * @returns Array containing [crashTime, remainingMemory1, remainingMemory2]
9 *          where crashTime is the first allocation size that cannot be fulfilled
10 */
11function memLeak(memory1: number, memory2: number): number[] {
12    // Current allocation size in MB, starting from 1
13    let allocationSize: number = 1;
14  
15    // Continue allocating until neither memory bank can fulfill the request
16    while (allocationSize <= Math.max(memory1, memory2)) {
17        // Allocate from the memory bank with more available space
18        if (memory1 >= memory2) {
19            // Deduct from memory1 if it has more or equal space
20            memory1 -= allocationSize;
21        } else {
22            // Deduct from memory2 if it has more space
23            memory2 -= allocationSize;
24        }
25      
26        // Increment allocation size for next iteration
27        allocationSize++;
28    }
29  
30    // Return the crash time and remaining memory in both banks
31    return [allocationSize, memory1, memory2];
32}
33

Time and Space Complexity

Time Complexity: O(√(memory1 + memory2))

The algorithm simulates a memory leak by repeatedly allocating memory blocks of increasing size (1, 2, 3, ..., i) from the memory stick with more available memory. The loop continues while i <= max(memory1, memory2).

To understand why the complexity is O(√(memory1 + memory2)):

  • The total memory allocated after n iterations is 1 + 2 + 3 + ... + n = n(n+1)/2
  • The loop stops approximately when n(n+1)/2 ≈ memory1 + memory2
  • Solving for n: n² ≈ 2(memory1 + memory2)
  • Therefore, n ≈ √(2(memory1 + memory2)), which is O(√(memory1 + memory2))

Space Complexity: O(1)

The algorithm uses only a constant amount of extra space. It modifies the input values in-place and uses only one additional variable i to track the current iteration, regardless of the input size.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Incorrect Tie-Breaking Logic

A common mistake is misunderstanding when to allocate to memory1 vs memory2, especially in the tie-breaking scenario.

Incorrect Implementation:

# WRONG: This reverses the tie-breaking rule
if memory2 > memory1:
    memory2 -= time_step
elif memory1 > memory2:
    memory1 -= time_step
else:  # Equal case
    memory1 -= time_step

Why it's wrong: This code allocates to memory2 when it has strictly more memory, which seems logical but doesn't match the problem's requirement. The problem states to allocate to the stick with "more available memory" but when equal, use memory1.

Correct Implementation:

# CORRECT: memory1 gets allocation when >= memory2
if memory1 >= memory2:
    memory1 -= time_step
else:
    memory2 -= time_step

2. Off-by-One Error in Loop Condition

Another pitfall is using the wrong comparison operator in the loop condition.

Incorrect Implementation:

# WRONG: Uses strict inequality
while time_step < max(memory1, memory2):
    # allocation logic
    time_step += 1

Why it's wrong: This would exit the loop one iteration too early. If max(memory1, memory2) = 5 and time_step = 5, we should still try to allocate 5 bits, but this condition would exit before attempting it.

Correct Implementation:

# CORRECT: Uses <= to include the boundary case
while time_step <= max(memory1, memory2):
    # allocation logic
    time_step += 1

3. Starting from Wrong Time Step

Incorrect Implementation:

# WRONG: Starting from 0
time_step = 0
while time_step <= max(memory1, memory2):
    time_step += 1  # Increment first
    if memory1 >= memory2:
        memory1 -= time_step
    else:
        memory2 -= time_step

Why it's wrong: The problem clearly states that at the i-th second (starting from second 1), we allocate i bits. Starting from 0 and incrementing first would allocate 1 bit at second 0, which doesn't match the problem description.

4. Checking Individual Memory Instead of Maximum

Incorrect Implementation:

# WRONG: Checking if both have enough memory
while memory1 >= time_step or memory2 >= time_step:
    if memory1 >= memory2:
        memory1 -= time_step
    else:
        memory2 -= time_step
    time_step += 1

Why it's wrong: This is actually correct logically, but using max(memory1, memory2) is cleaner and more efficient. However, a real mistake would be:

# WRONG: Requires both to have enough memory
while memory1 >= time_step and memory2 >= time_step:
    # allocation logic

This would crash too early - we only need ONE stick to have enough memory to continue.

Best Practice Solution:

def memLeak(self, memory1: int, memory2: int) -> List[int]:
    time_step = 1
  
    # Continue while at least one stick can handle the allocation
    while time_step <= max(memory1, memory2):
        # Allocate to stick with more memory (memory1 if tied)
        if memory1 >= memory2:
            memory1 -= time_step
        else:
            memory2 -= time_step
      
        time_step += 1
  
    return [time_step, memory1, memory2]
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of these pictures shows the visit order of a depth-first search?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More