Strange Printer
HardPrompt
There is a strange printer with the following two special properties:
- The printer can only print a sequence of the same character each time.
- At each turn, the printer can print new characters starting from and ending at any place and will cover the original existing characters.
Given a string s, return the minimum number of turns the printer needed to print it.
Example 1:
Input: s = "aaabbb"
Output: 2
Explanation: Print "aaa" first and then print "bbb".Example 2:
Input: s = "aba"
Output: 2
Explanation: Print "aaa" first and then print "b" from the second place of the string, which will cover the existing character 'a'.
Constraints:
1 <= s.length <= 100sconsists of lowercase English letters.
Approaches
2 approaches with complexity analysis and trade-offs.
This approach attempts to solve the problem by exploring all possible sequences of print operations in a recursive manner. For any given substring, it tries two main strategies: printing the first character by itself and then solving for the rest, or finding a matching character later in the string to form a combined print, which splits the problem into two subproblems. This method directly translates the problem's combinatorial nature into code.
Algorithm
- Define a recursive function
solve(s, i, j)that computes the minimum turns for the substrings[i...j]. - Base Case: If
i > j, the substring is empty, return 0. Ifi == j, the substring has one character, return 1. - Recursive Step:
- Calculate a result by printing the first character
s[i]alone and then solving for the rest of the string:res = 1 + solve(s, i + 1, j). - Iterate with a variable
kfromi + 1toj. Ifs.charAt(k)is the same ass.charAt(i), it means we can potentially use one print operation for both. This splits the problem into two independent subproblems:s[i+1...k-1]ands[k...j]. The cost for this strategy issolve(s, i + 1, k - 1) + solve(s, k, j). - Update
resto be the minimum of its current value and the value from the split.
- Calculate a result by printing the first character
- Return the final
res. - The initial call would be
solve(s, 0, s.length() - 1).
Walkthrough
The brute-force recursive solution defines a function, let's call it solve(i, j), that calculates the minimum turns needed to print the substring s[i...j]. The base cases for the recursion are simple: an empty substring (i > j) requires 0 turns, and a single-character substring (i == j) requires 1 turn.
For a general substring s[i...j], the function explores all valid moves. A default move is to print the character s[i] in a single turn and then recursively solve for the remaining substring s[i+1...j]. The cost of this move is 1 + solve(i+1, j).
To optimize, the function looks for any character s[k] (where k is between i+1 and j) that is identical to s[i]. If such a character is found, it represents an opportunity to use a single print operation to cover both s[i] and s[k]. This splits the problem into solving for the parts in between (s[i+1...k-1]) and the part after (s[k...j]). The cost would be the sum of the solutions to these subproblems. The function calculates the minimum cost among all these possibilities.
Since this method re-computes solutions for the same substrings repeatedly, its performance is very poor.
Complexity
Time
Exponential, likely in the order of O(2^N * N). For each subproblem, we might branch multiple times, leading to an explosion in the number of calls.
Space
O(N), where N is the length of the string. This space is used by the recursion call stack.
Trade-offs
Pros
Simple to understand and implement as it directly models the recursive structure of the problem.
Cons
Extremely inefficient due to a massive number of redundant computations for the same subproblems.
Will result in a 'Time Limit Exceeded' (TLE) error on most platforms for constraints of N > 15-20.
This is the standard and efficient approach for this problem, utilizing dynamic programming with memoization (a top-down DP approach). It improves upon the brute-force method by storing the results of subproblems in a 2D array, thereby avoiding redundant calculations. Before starting, the input string is compressed by removing consecutive duplicates (e.g., "aaabbb" becomes "ab"), which reduces the problem size without affecting the answer.
Algorithm
- First, preprocess the input string
sto remove any consecutive duplicate characters. This is a safe optimization because printing 'aaa' is the same as printing 'a' in terms of turn count. Let the compressed string bes_primeof lengthn. - Create a 2D array
memo[n][n]to store the results of subproblems, initialized to a value indicating 'not computed' (e.g., 0). - Define a recursive helper function
solve(i, j):- Base Cases: If
i > j, return 0. Ifi == j, return 1. - Memoization Check: If
memo[i][j]has been computed, return the stored value. - Recursive Calculation:
- Initialize the result for
s_prime[i...j]with a safe upper bound:res = 1 + solve(i + 1, j). This corresponds to printings_prime[i]and then solving for the rest. - Iterate
kfromi + 1toj. Ifs_prime.charAt(k) == s_prime.charAt(i), we have an opportunity to merge operations. Update the result:res = min(res, solve(i + 1, k - 1) + solve(k, j)).
- Initialize the result for
- Store the final
resinmemo[i][j]and return it.
- Base Cases: If
- The main function calls
solve(0, n - 1)to get the result for the entire compressed string.
Walkthrough
The core of this approach is a recursive function that computes the minimum turns for a substring s[i...j], but with a cache (memoization table) to store results. This prevents re-solving the same subproblem multiple times.
First, we preprocess the string. For instance, s = "aaabbb" becomes "ab". The minimum turns for both are the same. Let's call the new string s_prime.
We define a function solve(i, j) and a 2D array memo. memo[i][j] will store the result for s_prime[i...j].
The recurrence relation is as follows:
- A baseline strategy is to print
s_prime[i]in one turn and then solve for the rest,s_prime[i+1...j]. This gives1 + solve(i+1, j)turns. - We can do better if there's another character
s_prime[k](fork > i) that matchess_prime[i]. If so, we can use a single print operation fors_prime[i]that also covers the character at positionk. This effectively merges the problem of printings_prime[i...k-1]withs_prime[k...j]. The cost of this combined operation issolve(i+1, k-1) + solve(k, j). The intuition is that the turn printings_prime[i]can be the same turn that starts printing the subproblems_prime[k...j], saving one operation.
We take the minimum over all these possibilities. The result for each (i, j) pair is stored in memo[i][j] to be reused.
class Solution { private int[][] memo; private String s; public int strangePrinter(String s) { if (s == null || s.length() == 0) { return 0; } // Compress the string StringBuilder sb = new StringBuilder(); sb.append(s.charAt(0)); for (int i = 1; i < s.length(); i++) { if (s.charAt(i) != s.charAt(i - 1)) { sb.append(s.charAt(i)); } } this.s = sb.toString(); int n = this.s.length(); this.memo = new int[n][n]; return solve(0, n - 1); } private int solve(int i, int j) { if (i > j) { return 0; } if (i == j) { return 1; } if (memo[i][j] != 0) { return memo[i][j]; } // Option 1: Print s[i] and solve for the rest int res = 1 + solve(i + 1, j); // Option 2: Find s[k] == s[i] and merge operations for (int k = i + 1; k <= j; k++) { if (s.charAt(k) == s.charAt(i)) { res = Math.min(res, solve(i + 1, k - 1) + solve(k, j)); } } return memo[i][j] = res; }}An iterative, bottom-up DP solution can also be implemented with the same time and space complexity.
Complexity
Time
O(N^3), where N is the length of the (potentially compressed) string. There are O(N^2) states (subproblems), and each state takes O(N) time to compute due to the inner loop that searches for matching characters.
Space
O(N^2), where N is the length of the (potentially compressed) string. This space is required for the memoization table.
Trade-offs
Pros
Efficient enough to solve the problem within typical time limits due to the O(N^3) complexity.
Guaranteed to find the optimal solution.
The preprocessing step of compressing the string is a simple and effective optimization.
Cons
The O(N^3) time complexity might be too slow if the constraints on N were significantly larger.
The recurrence relation's logic can be non-trivial to derive and prove correct.
Solutions
Solution
class Solution { public int strangePrinter ( String s ) { final int inf = 1 << 30 ; int n = s . length (); int [][] f = new int [ n ][ n ]; for ( var g : f ) { Arrays . fill ( g , inf ); } for ( int i = n - 1 ; i >= 0 ; -- i ) { f [ i ][ i ] = 1 ; for ( int j = i + 1 ; j < n ; ++ j ) { if ( s . charAt ( i ) == s . charAt ( j )) { f [ i ][ j ] = f [ i ][ j - 1 ]; } else { for ( int k = i ; k < j ; ++ k ) { f [ i ][ j ] = Math . min ( f [ i ][ j ], f [ i ][ k ] + f [ k + 1 ][ j ]); } } } } return f [ 0 ][ n - 1 ]; } }class Solution { public: int strangePrinter ( string s ) { int n = s . size (); int f [ n ][ n ]; memset ( f , 0x3f , sizeof ( f )); for ( int i = n - 1 ; ~ i ; -- i ) { f [ i ][ i ] = 1 ; for ( int j = i + 1 ; j < n ; ++ j ) { if ( s [ i ] == s [ j ]) { f [ i ][ j ] = f [ i ][ j - 1 ]; } else { for ( int k = i ; k < j ; ++ k ) { f [ i ][ j ] = min ( f [ i ][ j ], f [ i ][ k ] + f [ k + 1 ][ j ]); } } } } return f [ 0 ][ n - 1 ]; } };class Solution : def strangePrinter ( self , s : str ) -> int : n = len ( s ) f = [[ inf ] * n for _ in range ( n )] for i in range ( n - 1 , - 1 , - 1 ): f [ i ][ i ] = 1 for j in range ( i + 1 , n ): if s [ i ] == s [ j ]: f [ i ][ j ] = f [ i ][ j - 1 ] else : for k in range ( i , j ): f [ i ][ j ] = min ( f [ i ][ j ], f [ i ][ k ] + f [ k + 1 ][ j ]) return f [ 0 ][ - 1 ]Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.