Facebook Pixel

650. 2 Keys Keyboard

Problem Description

You start with a notepad that has a single character 'A' on the screen. You can perform two operations:

  1. Copy All: Copy all characters currently on the screen (you cannot copy just a portion)
  2. Paste: Paste the characters that were most recently copied

Your goal is to get exactly n 'A' characters on the screen using the minimum number of operations.

For example, if n = 3:

  • Start with: A (1 character)
  • Operation 1: Copy All (clipboard now has A)
  • Operation 2: Paste (screen now has AA, 2 characters)
  • Operation 3: Paste (screen now has AAA, 3 characters)
  • Total: 3 operations

The solution uses dynamic programming with memoization to find the minimum operations. The key insight is that to reach n characters, we can factorize n and use its divisors. If n is divisible by i, we can first get n/i characters, then copy once and paste i-1 times (total i operations) to multiply the count by i.

The recursive function dfs(n) returns the minimum operations to get n characters:

  • Base case: If n = 1, no operations needed (return 0)
  • For each divisor i of n, we can get n/i characters first, then perform i operations (1 copy + i-1 pastes) to reach n
  • The answer is the minimum among all possible paths
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key observation is that reaching n characters is fundamentally about multiplication. When we copy and paste, we're essentially multiplying the current number of characters.

Think about how we get from 1 to n characters. Every sequence of operations can be broken down into groups where each group consists of one Copy followed by several Pastes. If we Copy when we have x characters and then Paste k times, we end up with x * (k+1) characters.

This means to reach n, we need to find a way to express n as a product of factors. For instance, to get 12 characters, we could:

  • Get to 3 characters, then Copy + Paste 3 times (multiply by 4): 3 × 4 = 12
  • Get to 4 characters, then Copy + Paste 2 times (multiply by 3): 4 × 3 = 12
  • Get to 6 characters, then Copy + Paste 1 time (multiply by 2): 6 × 2 = 12

The number of operations for each multiplication by factor f is exactly f (1 Copy + f-1 Pastes). This leads us to think about prime factorization. If n = p1 × p2 × p3..., then the minimum operations would be p1 + p2 + p3 + ...

However, we don't always want to use prime factors. Sometimes grouping primes together gives better results. For example, for n = 6 = 2 × 3, we could use 2 + 3 = 5 operations, but we could also treat 6 as 6 × 1, though that would need 6 operations.

This naturally suggests a recursive approach: for each divisor i of n, we can first get to n/i characters (recursively), then multiply by i using i operations. We try all divisors and pick the minimum. The problem decomposes into smaller subproblems, making dynamic programming with memoization an ideal solution strategy.

Learn more about Math and Dynamic Programming patterns.

Solution Approach

The solution implements a top-down dynamic programming approach with memoization using Python's @cache decorator.

Implementation Details:

  1. Recursive Function dfs(n): This function calculates the minimum operations needed to get exactly n characters.

  2. Base Case: When n = 1, we already have one 'A' on the screen, so no operations are needed. Return 0.

  3. Recursive Case: For any n > 1, we iterate through all possible divisors from 2 to √n:

    • Start with i = 2 and initialize ans = n (worst case: copy once, then paste n-1 times)
    • Check if i is a divisor of n (i.e., n % i == 0)
    • If yes, we can:
      • First get n // i characters (recursive call: dfs(n // i))
      • Then multiply by i using i operations (1 copy + i-1 pastes)
      • Total operations: dfs(n // i) + i
    • Keep track of the minimum among all valid divisors
  4. Optimization: We only check divisors up to √n because:

    • If i is a divisor, then n/i is also a divisor
    • One of them must be ≤ √n
    • The recursive call dfs(n // i) will handle the larger divisors
  5. Memoization: The @cache decorator automatically stores results of previous computations, preventing redundant calculations for the same value of n.

Example Walkthrough for n = 6:

  • dfs(6) checks divisors: 2 and 3
  • For divisor 2: dfs(3) + 2 = 3 + 2 = 5
  • For divisor 3: dfs(2) + 3 = 2 + 3 = 5
  • Both paths give 5 operations, which is optimal

The algorithm effectively finds the optimal factorization of n that minimizes the sum of factors, leveraging dynamic programming to avoid recomputing subproblems.

Ready to land your dream job?

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

Start Evaluator

Example Walkthrough

Let's trace through the solution for n = 8 to understand how the algorithm finds the minimum operations.

Starting with n = 8:

We need to get from 1 'A' to 8 'A's. The algorithm explores different factorization paths:

  1. First, dfs(8) is called:

    • Initialize ans = 8 (worst case: copy once, paste 7 times)
    • Check divisor i = 2: Since 8 % 2 = 0, it's valid
      • We can get to 8 by first reaching 4 characters, then multiplying by 2
      • Cost: dfs(4) + 2
    • Check divisor i = 3: 8 % 3 ≠ 0, skip
    • Check divisor i = 4: Since 8 % 4 = 0, it's valid
      • We can get to 8 by first reaching 2 characters, then multiplying by 4
      • Cost: dfs(2) + 4
  2. Computing dfs(4):

    • Initialize ans = 4 (worst case)
    • Check divisor i = 2: Since 4 % 2 = 0, it's valid
      • Cost: dfs(2) + 2
  3. Computing dfs(2):

    • Initialize ans = 2 (worst case)
    • No divisors to check (since we start from 2)
    • Return 2 (copy once, paste once)
  4. Backtracking with computed values:

    • dfs(2) = 2
    • dfs(4) = dfs(2) + 2 = 2 + 2 = 4
    • dfs(8) compares:
      • Path via divisor 2: dfs(4) + 2 = 4 + 2 = 6
      • Path via divisor 4: dfs(2) + 4 = 2 + 4 = 6
    • Both give 6 operations (optimal)

Verifying the solution (6 operations):

  • Start: A (1 character)
  • Op 1: Copy All → clipboard: A
  • Op 2: Paste → AA (2 characters)
  • Op 3: Copy All → clipboard: AA
  • Op 4: Paste → AAAA (4 characters)
  • Op 5: Copy All → clipboard: AAAA
  • Op 6: Paste → AAAAAAAA (8 characters)

The algorithm found that 8 = 2 × 2 × 2, requiring 2 + 2 + 2 = 6 operations, which corresponds to the sum of prime factors.

Solution Implementation

1from functools import cache
2from typing import Optional
3
4
5class Solution:
6    def minSteps(self, n: int) -> int:
7        """
8        Find the minimum number of steps to reach n.
9      
10        The algorithm works by finding the smallest factor decomposition,
11        where the sum of factors gives the minimum steps.
12      
13        Args:
14            n: Target number to reach
15          
16        Returns:
17            Minimum number of steps required
18        """
19      
20        @cache
21        def dfs(current_n: int) -> int:
22            """
23            Recursively find minimum steps using factorization.
24          
25            Args:
26                current_n: Current number to factorize
27              
28            Returns:
29                Minimum steps for current_n
30            """
31            # Base case: reaching 1 requires 0 steps
32            if current_n == 1:
33                return 0
34          
35            # Initialize factor at 2 and result as worst case (n itself)
36            factor = 2
37            min_steps = current_n
38          
39            # Try all possible factors up to sqrt(current_n)
40            while factor * factor <= current_n:
41                # If factor divides current_n evenly
42                if current_n % factor == 0:
43                    # Recursively solve for current_n/factor and add factor as cost
44                    min_steps = min(min_steps, dfs(current_n // factor) + factor)
45                factor += 1
46          
47            return min_steps
48      
49        return dfs(n)
50
1class Solution {
2    // Memoization array to store computed results for dynamic programming
3    private int[] memo;
4  
5    /**
6     * Finds the minimum number of operations to get exactly n 'A's on screen.
7     * Starting with one 'A', you can only Copy All or Paste.
8     * 
9     * @param n The target number of 'A's
10     * @return The minimum number of operations needed
11     */
12    public int minSteps(int n) {
13        // Initialize memoization array with size n+1 to handle indices 0 to n
14        memo = new int[n + 1];
15        // Fill with -1 to indicate uncomputed states
16        Arrays.fill(memo, -1);
17      
18        // Start the recursive computation
19        return dfs(n);
20    }
21  
22    /**
23     * Recursive helper function with memoization to calculate minimum steps.
24     * The key insight: to get n characters, we can get n/i characters first,
25     * then copy once and paste (i-1) times, totaling i operations.
26     * 
27     * @param n Current target number of 'A's
28     * @return Minimum operations needed to reach n 'A's
29     */
30    private int dfs(int n) {
31        // Base case: already have 1 'A', need 0 operations
32        if (n == 1) {
33            return 0;
34        }
35      
36        // Return memoized result if already computed
37        if (memo[n] != -1) {
38            return memo[n];
39        }
40      
41        // Initialize answer with worst case: copy once, then paste (n-1) times
42        int minOperations = n;
43      
44        // Try all possible divisors of n
45        // We only need to check up to sqrt(n) for efficiency
46        for (int divisor = 2; divisor * divisor <= n; ++divisor) {
47            if (n % divisor == 0) {
48                // If divisor divides n, we can:
49                // 1. First get n/divisor 'A's
50                // 2. Then copy once and paste (divisor-1) times = divisor operations
51                minOperations = Math.min(minOperations, dfs(n / divisor) + divisor);
52            }
53        }
54      
55        // Store the computed result in memoization array
56        memo[n] = minOperations;
57        return minOperations;
58    }
59}
60
1class Solution {
2public:
3    vector<int> memo;  // Memoization array to store computed results
4
5    int minSteps(int n) {
6        // Initialize memoization array with -1 (uncomputed state)
7        memo.assign(n + 1, -1);
8        return dfs(n);
9    }
10
11    int dfs(int n) {
12        // Base case: reaching 1 requires 0 steps
13        if (n == 1) {
14            return 0;
15        }
16      
17        // Return cached result if already computed
18        if (memo[n] != -1) {
19            return memo[n];
20        }
21      
22        // Initialize result with worst case (n steps: n-1 pastes after initial copy)
23        int result = n;
24      
25        // Try all possible factors of n
26        for (int factor = 2; factor * factor <= n; ++factor) {
27            if (n % factor == 0) {
28                // If factor divides n, we can reach n by:
29                // 1. First reaching n/factor
30                // 2. Then performing 1 copy-all and (factor-1) pastes = factor operations
31                result = min(result, dfs(n / factor) + factor);
32            }
33        }
34      
35        // Cache and return the result
36        memo[n] = result;
37        return result;
38    }
39};
40
1let memo: number[]; // Memoization array to store computed results
2
3function minSteps(n: number): number {
4    // Initialize memoization array with -1 (uncomputed state)
5    memo = new Array(n + 1).fill(-1);
6    return dfs(n);
7}
8
9function dfs(n: number): number {
10    // Base case: reaching 1 requires 0 steps
11    if (n === 1) {
12        return 0;
13    }
14  
15    // Return cached result if already computed
16    if (memo[n] !== -1) {
17        return memo[n];
18    }
19  
20    // Initialize result with worst case (n steps: n-1 pastes after initial copy)
21    let result = n;
22  
23    // Try all possible factors of n
24    for (let factor = 2; factor * factor <= n; factor++) {
25        if (n % factor === 0) {
26            // If factor divides n, we can reach n by:
27            // 1. First reaching n/factor
28            // 2. Then performing 1 copy-all and (factor-1) pastes = factor operations
29            result = Math.min(result, dfs(Math.floor(n / factor)) + factor);
30        }
31    }
32  
33    // Cache and return the result
34    memo[n] = result;
35    return result;
36}
37

Time and Space Complexity

Time Complexity: O(n^(3/2))

The algorithm uses memoized recursion to find the minimum steps. For each number n, it checks all potential divisors from 2 to √n.

  • The while loop runs O(√n) times for each recursive call
  • In the worst case (when n is prime), the recursion depth is O(log n)
  • Due to memoization with @cache, each unique value from 1 to n is computed only once
  • The total number of unique subproblems is at most O(n)
  • For each subproblem, we spend O(√n) time checking divisors
  • Therefore, the overall time complexity is O(n × √n) = O(n^(3/2))

Space Complexity: O(n)

The space complexity consists of:

  • The recursion call stack depth: O(log n) in the worst case (when repeatedly dividing by smallest prime factors)
  • The memoization cache storing results: O(n) in the worst case (could store results for all values from 1 to n)
  • Since O(n) > O(log n), the overall space complexity is O(n)

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

Common Pitfalls

1. Incorrect Understanding of Operations

A common mistake is misunderstanding how the copy-paste operations work. Some might think:

  • Each paste operation adds 1 character (incorrect)
  • You can selectively copy portions of the screen (incorrect)

Reality: When you paste, you add ALL characters from the clipboard. If you have "AA" on screen and copy it, then paste, you get "AAAA" (doubling), not "AAA".

Solution: Remember that copy-paste operations multiply the current count. To get from k characters to n characters where n = k * m, you need 1 copy + (m-1) pastes = m total operations.

2. Not Considering All Divisors Properly

The code only checks divisors up to √n, which is correct, but developers might forget why this works:

# Incorrect approach - checking all numbers up to n
for factor in range(2, current_n + 1):
    if current_n % factor == 0:
        min_steps = min(min_steps, dfs(current_n // factor) + factor)

This would be inefficient and redundant because when we find divisor i, the recursive call dfs(n // i) will handle the complementary divisor.

Solution: Trust the optimization - checking up to √n is sufficient because the recursive structure handles larger divisors through smaller subproblems.

3. Forgetting the Base Case or Setting It Incorrectly

Some might set the base case as:

if current_n == 1:
    return 1  # Wrong! We start with 1 'A' already

Solution: The base case should return 0 because we start with one 'A' on the screen - no operations needed to have 1 character.

4. Integer Overflow in Other Languages

While Python handles large integers automatically, in languages like Java or C++, calculating operations for large values of n might cause overflow:

// In Java, this might overflow for large n
int minSteps = n;  // If n is close to Integer.MAX_VALUE

Solution: Use appropriate data types (long in Java, long long in C++) or add bounds checking.

5. Inefficient Iteration Through Factors

Some might try to optimize by skipping even numbers after checking 2:

# Attempting to optimize but creating bugs
factor = 2
while factor * factor <= current_n:
    if current_n % factor == 0:
        min_steps = min(min_steps, dfs(current_n // factor) + factor)
    factor = 1 if factor == 2 else 2  # Bug! Should be factor += 1 or factor += 2

Solution: Keep the iteration simple with factor += 1. The performance gain from skipping even numbers is minimal compared to the memoization benefit, and it reduces the chance of off-by-one errors.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

What's the output of running the following function using the following tree as input?

1def serialize(root):
2    res = []
3    def dfs(root):
4        if not root:
5            res.append('x')
6            return
7        res.append(root.val)
8        dfs(root.left)
9        dfs(root.right)
10    dfs(root)
11    return ' '.join(res)
12
1import java.util.StringJoiner;
2
3public static String serialize(Node root) {
4    StringJoiner res = new StringJoiner(" ");
5    serializeDFS(root, res);
6    return res.toString();
7}
8
9private static void serializeDFS(Node root, StringJoiner result) {
10    if (root == null) {
11        result.add("x");
12        return;
13    }
14    result.add(Integer.toString(root.val));
15    serializeDFS(root.left, result);
16    serializeDFS(root.right, result);
17}
18
1function serialize(root) {
2    let res = [];
3    serialize_dfs(root, res);
4    return res.join(" ");
5}
6
7function serialize_dfs(root, res) {
8    if (!root) {
9        res.push("x");
10        return;
11    }
12    res.push(root.val);
13    serialize_dfs(root.left, res);
14    serialize_dfs(root.right, res);
15}
16

Recommended Readings

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

Load More