Facebook Pixel

664. Strange Printer

Problem Description

You have a strange printer with two special properties:

  1. The printer can only print a sequence of the same character each time (e.g., "aaa" or "bbb", but not "abc")
  2. When printing, the printer can start and end at any position in the string, and the new characters will cover (overwrite) any existing characters at those positions

Given a string s, find the minimum number of printing operations needed to print the entire string.

For example, if you want to print "aba":

  • One way: Print "aaa" first (1 operation), then print "b" at position 1 to get "aba" (2 operations total)
  • This takes 2 operations minimum

The key insight is that when you print, you can strategically overwrite parts of previously printed characters to minimize the total number of operations. The printer can print any continuous segment of the same character and place it anywhere in the string, covering what was there before.

The solution uses dynamic programming where f[i][j] represents the minimum number of operations needed to print the substring from index i to index j. The approach considers:

  • If the characters at positions i and j are the same (s[i] == s[j]), we can print them together in one operation, effectively reducing the problem to printing s[i..j-1]
  • If they're different, we need to find the optimal split point k where we print s[i..k] and s[k+1..j] separately, choosing the split that minimizes total operations
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key observation is that when we print a character at position i, we can extend that same print operation to cover any other position j where s[i] == s[j] without any additional cost. This means if the first and last characters of a substring are the same, we can handle them together in a single print operation.

Think about printing "aba". If we decide to print 'a' at position 0, we might as well extend that print to also cover position 2 (since both are 'a'), giving us "a_a" in one operation. Then we only need one more operation to fill in the 'b' at position 1.

This naturally leads to a range-based thinking: for any substring s[i..j], we need to determine the minimum operations to print it. This screams dynamic programming with intervals.

When looking at a substring s[i..j]:

  • If s[i] == s[j], we can "piggyback" the printing of s[j] onto the operation that prints s[i]. This means printing s[i..j] takes the same number of operations as printing s[i..j-1] (we get s[j] for free).
  • If s[i] != s[j], they must be printed in separate operations. We need to find the best way to split the substring into two parts. We try all possible split points k and choose the one that minimizes f[i][k] + f[k+1][j].

The base case is straightforward: printing a single character always takes exactly 1 operation.

By building up from smaller substrings to larger ones (processing shorter ranges before longer ones), we ensure that when we need to look up f[i][k] or f[k+1][j], these values have already been computed. This is why we iterate with i decreasing and j increasing - it guarantees we process all necessary subproblems first.

Learn more about Dynamic Programming patterns.

Solution Approach

The solution uses a 2D dynamic programming approach with a table f[i][j] representing the minimum operations needed to print substring s[i..j].

Initialization:

  • Create a 2D array f of size n × n where n = len(s), initialized with infinity values
  • The diagonal elements f[i][i] = 1 since printing a single character always takes 1 operation

Iteration Order:

  • Iterate i from n-1 down to 0 (right to left)
  • For each i, iterate j from i+1 to n-1 (left to right)
  • This order ensures that when computing f[i][j], all required subproblems are already solved

Transition Logic: For each f[i][j]:

  1. Case 1: When s[i] == s[j]

    • Set f[i][j] = f[i][j-1]
    • Since characters at both ends are the same, we can print them together in one operation
    • The cost is the same as printing s[i..j-1] since s[j] comes for free
  2. Case 2: When s[i] != s[j]

    • Try all possible split points k where i ≤ k < j
    • For each split: f[i][j] = min(f[i][j], f[i][k] + f[k+1][j])
    • This represents printing s[i..k] first, then printing s[k+1..j] separately
    • Choose the split that gives the minimum total operations

State Transition Formula:

f[i][j] = {
    1,                                           if i = j
    f[i][j-1],                                   if s[i] = s[j]
    min(f[i][k] + f[k+1][j]) for k in [i,j),    if s[i] ≠ s[j]
}

Final Answer: Return f[0][n-1], which represents the minimum operations to print the entire string from index 0 to n-1.

The time complexity is O(n³) due to three nested loops (i, j, and k), and space complexity is O(n²) for the DP table.

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 the example string s = "aba" to understand how the dynamic programming solution works.

Step 1: Initialize the DP table We create a 3×3 table f[i][j] and set diagonal values to 1:

    0   1   2
0 [ 1   ∞   ∞ ]
1 [ ∞   1   ∞ ]
2 [ ∞   ∞   1 ]

Step 2: Process substring "ab" (i=0, j=1)

  • s[0]='a' and s[1]='b' are different
  • We try split point k=0: f[0][0] + f[1][1] = 1 + 1 = 2
  • So f[0][1] = 2

Step 3: Process substring "ba" (i=1, j=2)

  • s[1]='b' and s[2]='a' are different
  • We try split point k=1: f[1][1] + f[2][2] = 1 + 1 = 2
  • So f[1][2] = 2

Step 4: Process substring "aba" (i=0, j=2)

  • s[0]='a' and s[2]='a' are the same!
  • According to our rule: f[0][2] = f[0][1] = 2
  • This means we can print both 'a's in one operation, getting "a_a", then print 'b' separately

Final DP table:

    0   1   2
0 [ 1   2   2 ]
1 [ ∞   1   2 ]
2 [ ∞   ∞   1 ]

Answer: f[0][2] = 2 operations minimum

How the actual printing works:

  1. First operation: Print "aaa" (covers positions 0, 1, 2)
  2. Second operation: Print "b" at position 1 (overwrites the middle 'a')
  3. Result: "aba"

The key insight demonstrated here is that when s[0]='a' equals s[2]='a', we can handle both in a single print operation, which is why f[0][2] = f[0][1] rather than trying to split the string.

Solution Implementation

1class Solution:
2    def strangePrinter(self, s: str) -> int:
3        n = len(s)
4      
5        # dp[i][j] represents minimum number of turns to print substring s[i:j+1]
6        dp = [[float('inf')] * n for _ in range(n)]
7      
8        # Iterate through all possible substrings from bottom-right to top-left
9        for i in range(n - 1, -1, -1):
10            # Single character needs only 1 turn to print
11            dp[i][i] = 1
12          
13            # Check all substrings starting at position i
14            for j in range(i + 1, n):
15                if s[i] == s[j]:
16                    # If first and last characters are same,
17                    # we can print them together in the same turn
18                    # So the cost is same as printing s[i:j]
19                    dp[i][j] = dp[i][j - 1]
20                else:
21                    # If first and last characters are different,
22                    # try splitting at each position k and find minimum
23                    for k in range(i, j):
24                        # Split into s[i:k+1] and s[k+1:j+1]
25                        dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j])
26      
27        # Return minimum turns needed to print entire string
28        return dp[0][n - 1]
29
1class Solution {
2    public int strangePrinter(String s) {
3        // Initialize a large value representing infinity for comparison
4        final int INF = 1 << 30;
5      
6        // Get the length of the input string
7        int n = s.length();
8      
9        // dp[i][j] represents minimum number of turns to print substring s[i...j]
10        int[][] dp = new int[n][n];
11      
12        // Initialize all values to infinity
13        for (int[] row : dp) {
14            Arrays.fill(row, INF);
15        }
16      
17        // Build the DP table from bottom-right to top-left
18        // Process smaller substrings before larger ones
19        for (int i = n - 1; i >= 0; i--) {
20            // Single character always takes 1 turn to print
21            dp[i][i] = 1;
22          
23            // Process all substrings starting at position i
24            for (int j = i + 1; j < n; j++) {
25                // If characters at both ends are the same
26                if (s.charAt(i) == s.charAt(j)) {
27                    // We can print s[i] when printing s[j], so same cost as s[i...j-1]
28                    dp[i][j] = dp[i][j - 1];
29                } else {
30                    // Characters at ends are different, try all split points
31                    for (int k = i; k < j; k++) {
32                        // Split at position k and find minimum cost
33                        // Cost = print s[i...k] + print s[k+1...j]
34                        dp[i][j] = Math.min(dp[i][j], dp[i][k] + dp[k + 1][j]);
35                    }
36                }
37            }
38        }
39      
40        // Return minimum turns needed to print entire string
41        return dp[0][n - 1];
42    }
43}
44
1class Solution {
2public:
3    int strangePrinter(string s) {
4        int n = s.size();
5      
6        // dp[i][j] represents the minimum number of turns to print substring s[i..j]
7        vector<vector<int>> dp(n, vector<int>(n, INT_MAX));
8      
9        // Iterate from right to left for starting position
10        for (int i = n - 1; i >= 0; --i) {
11            // Base case: single character needs only 1 turn to print
12            dp[i][i] = 1;
13          
14            // Iterate through all possible ending positions after i
15            for (int j = i + 1; j < n; ++j) {
16                // If characters at positions i and j are the same,
17                // we can print them together in the same turn as printing s[i..j-1]
18                if (s[i] == s[j]) {
19                    dp[i][j] = dp[i][j - 1];
20                } else {
21                    // If characters are different, try all possible split points
22                    // and find the minimum number of turns
23                    for (int k = i; k < j; ++k) {
24                        // Split the range [i, j] at position k
25                        // Print [i, k] and [k+1, j] separately
26                        dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j]);
27                    }
28                }
29            }
30        }
31      
32        // Return the minimum turns needed to print the entire string
33        return dp[0][n - 1];
34    }
35};
36
1/**
2 * Calculates the minimum number of turns needed to print a string
3 * using a strange printer that can print sequences of the same character
4 * @param s - The input string to be printed
5 * @returns The minimum number of printing operations needed
6 */
7function strangePrinter(s: string): number {
8    const stringLength: number = s.length;
9  
10    // Initialize DP table where dp[i][j] represents minimum operations
11    // to print substring from index i to j
12    // Initialize with large value (1 << 30 represents a very large number)
13    const dp: number[][] = Array.from(
14        { length: stringLength }, 
15        () => new Array<number>(stringLength).fill(1 << 30)
16    );
17  
18    // Iterate through all possible substring lengths, starting from the end
19    for (let startIndex = stringLength - 1; startIndex >= 0; startIndex--) {
20        // Base case: printing a single character requires 1 operation
21        dp[startIndex][startIndex] = 1;
22      
23        // Consider all substrings starting at startIndex
24        for (let endIndex = startIndex + 1; endIndex < stringLength; endIndex++) {
25            // Optimization: if characters at start and end are the same,
26            // we can print them together in the same operation
27            if (s[startIndex] === s[endIndex]) {
28                dp[startIndex][endIndex] = dp[startIndex][endIndex - 1];
29            } else {
30                // Try all possible split points to find minimum operations
31                for (let splitPoint = startIndex; splitPoint < endIndex; splitPoint++) {
32                    dp[startIndex][endIndex] = Math.min(
33                        dp[startIndex][endIndex],
34                        dp[startIndex][splitPoint] + dp[splitPoint + 1][endIndex]
35                    );
36                }
37            }
38        }
39    }
40  
41    // Return the minimum operations needed to print the entire string
42    return dp[0][stringLength - 1];
43}
44

Time and Space Complexity

The time complexity is O(n^3) where n is the length of string s. This is because:

  • The outer loop iterates through all starting positions i from n-1 to 0, which is O(n)
  • For each i, the middle loop iterates through all ending positions j from i+1 to n-1, which is O(n)
  • For each pair (i, j) where s[i] != s[j], the inner loop iterates through all possible split points k from i to j-1, which is O(n)
  • Overall, we have three nested loops resulting in O(n^3) time complexity

The space complexity is O(n^2) where n is the length of string s. This is due to:

  • The 2D DP table f of size n × n that stores the minimum number of turns needed to print each substring s[i:j+1]
  • No additional significant space is used beyond this table

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

Common Pitfalls

1. Incorrect DP Table Initialization

A common mistake is initializing the DP table with 0 or leaving it uninitialized, which leads to incorrect minimum calculations.

Wrong approach:

dp = [[0] * n for _ in range(n)]  # Wrong: 0 initialization

Correct approach:

dp = [[float('inf')] * n for _ in range(n)]  # Correct: infinity initialization

2. Wrong Iteration Order

Many people iterate in the wrong direction, causing access to unsolved subproblems.

Wrong approach:

for i in range(n):
    for j in range(i + 1, n):  # Wrong: subproblems not yet solved

Correct approach:

for i in range(n - 1, -1, -1):  # Right to left
    for j in range(i + 1, n):       # Left to right

3. Mishandling the s[i] == s[j] Case

A critical error is setting dp[i][j] = dp[i+1][j-1] when characters match, which is intuitive but incorrect.

Wrong approach:

if s[i] == s[j]:
    dp[i][j] = dp[i+1][j-1]  # Wrong: doesn't account for the printing strategy

Correct approach:

if s[i] == s[j]:
    dp[i][j] = dp[i][j-1]  # Correct: we can print s[j] together with s[i]

4. Off-by-One Errors in Split Point Iteration

Incorrectly setting the range for the split point k can miss optimal solutions or cause index errors.

Wrong approach:

for k in range(i+1, j+1):  # Wrong: misses k=i case
    dp[i][j] = min(dp[i][j], dp[i][k-1] + dp[k][j])

Correct approach:

for k in range(i, j):  # Correct: k ranges from i to j-1
    dp[i][j] = min(dp[i][j], dp[i][k] + dp[k+1][j])

5. Not Handling Edge Cases

Forgetting to handle single character substrings or empty strings.

Solution: Always set dp[i][i] = 1 inside the loop to ensure single characters are handled correctly:

for i in range(n - 1, -1, -1):
    dp[i][i] = 1  # Essential: single character takes 1 operation
    for j in range(i + 1, n):
        # rest of the logic

6. Attempting to Optimize Prematurely

Some try to remove consecutive duplicate characters first, but this can complicate the logic unnecessarily and introduce bugs.

Better approach: Keep the solution simple and let the DP handle all cases uniformly. The algorithm naturally handles consecutive duplicates efficiently through the s[i] == s[j] case.

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

Depth first search is equivalent to which of the tree traversal order?


Recommended Readings

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

Load More