664. Strange Printer
Problem Description
You have a strange printer with two special properties:
- The printer can only print a sequence of the same character each time (e.g., "aaa" or "bbb", but not "abc")
- 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
andj
are the same (s[i] == s[j]
), we can print them together in one operation, effectively reducing the problem to printings[i..j-1]
- If they're different, we need to find the optimal split point
k
where we prints[i..k]
ands[k+1..j]
separately, choosing the split that minimizes total operations
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 ofs[j]
onto the operation that printss[i]
. This means printings[i..j]
takes the same number of operations as printings[i..j-1]
(we gets[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 pointsk
and choose the one that minimizesf[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 sizen × n
wheren = 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
fromn-1
down to0
(right to left) - For each
i
, iteratej
fromi+1
ton-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]
:
-
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]
sinces[j]
comes for free
- Set
-
Case 2: When
s[i] != s[j]
- Try all possible split points
k
wherei ≤ 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 printings[k+1..j]
separately - Choose the split that gives the minimum total operations
- Try all possible split points
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 EvaluatorExample 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'
ands[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'
ands[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'
ands[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:
- First operation: Print "aaa" (covers positions 0, 1, 2)
- Second operation: Print "b" at position 1 (overwrites the middle 'a')
- 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
fromn-1
to0
, which isO(n)
- For each
i
, the middle loop iterates through all ending positionsj
fromi+1
ton-1
, which isO(n)
- For each pair
(i, j)
wheres[i] != s[j]
, the inner loop iterates through all possible split pointsk
fromi
toj-1
, which isO(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 sizen × n
that stores the minimum number of turns needed to print each substrings[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.
Depth first search is equivalent to which of the tree traversal order?
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!