The Strange Printer problem is as follows:
There is a strange printer with the following properties:
s
, return the minimum number of turns the printer needs to print s
from an initially blank string.
Constraints:
1 ≤ s.length ≤ 100
s
consists of lowercase English letters only.The solution must minimize the number of turns, and overlapping or redundant prints should be avoided for optimality.
At first glance, a brute-force approach might seem reasonable: for each character in the string, print it individually. However, this ignores the ability to print over substrings and to overwrite previous prints. The key insight is that if the same character appears in multiple places, it may be possible to print them together in a single turn if they are grouped, reducing the total number of turns.
The challenge lies in deciding the optimal way to group identical characters and minimize the number of printing turns, especially when the same character appears non-contiguously. A naive recursive solution would try all possible ways to split the string and print, but this quickly becomes inefficient. Recognizing overlapping subproblems and optimal substructure points us toward dynamic programming.
The main idea is to use dynamic programming to compute the minimum turns needed to print any substring, and then build up the solution for the entire string from these smaller substrings.
Let's break down the dynamic programming solution step-by-step:
dp[i][j]
represent the minimum number of turns needed to print the substring s[i:j+1]
.i == j
(a single character), only one turn is needed: dp[i][j] = 1
.s[i:j+1]
, consider two cases:
s[i]
over s[i:j+1]
in one turn, then print the rest optimally: dp[i][j] = 1 + dp[i+1][j]
.k
such that s[k] == s[i]
and i < k ≤ j
, try splitting at k
: dp[i][j] = min(dp[i][j], dp[i][k-1] + dp[k][j])
. This combines the turns where the same character is printed in both halves, potentially saving a turn.s
at the start, as printing over repeated characters doesn't require extra turns.dp
table from smaller to larger substrings.dp[0][n-1]
, where n
is the length of s
.
Let's consider the input s = "aba"
.
s[0]
(positions 0). Now, the string is "a__".
s[1]
(positions 1). The string becomes "ab_".
s[2]
. But notice: since the first and last characters are both 'a', we could have printed 'a' over positions 0 and 2 together if they were contiguous. But since they are not, we need separate turns.
For more complex cases, the DP table will try all possible splits and combine the minimum number of turns.
class Solution:
def strangePrinter(self, s: str) -> int:
# Remove consecutive duplicates for optimization
s2 = []
for c in s:
if not s2 or s2[-1] != c:
s2.append(c)
s = ''.join(s2)
n = len(s)
dp = [[0] * n for _ in range(n)]
for i in range(n):
dp[i][i] = 1
for l in range(2, n + 1): # length of substring
for i in range(n - l + 1):
j = i + l - 1
dp[i][j] = dp[i + 1][j] + 1
for k in range(i + 1, j + 1):
if s[i] == s[k]:
dp[i][j] = min(dp[i][j], dp[i][k - 1] + dp[k][j])
return dp[0][n - 1] if n else 0
class Solution {
public:
int strangePrinter(string s) {
string s2;
for (char c : s) {
if (s2.empty() || s2.back() != c) s2.push_back(c);
}
int n = s2.size();
vector<vector<int>> dp(n, vector<int>(n, 0));
for (int i = 0; i < n; ++i) dp[i][i] = 1;
for (int len = 2; len <= n; ++len) {
for (int i = 0; i + len - 1 < n; ++i) {
int j = i + len - 1;
dp[i][j] = dp[i + 1][j] + 1;
for (int k = i + 1; k <= j; ++k) {
if (s2[i] == s2[k]) {
dp[i][j] = min(dp[i][j], dp[i][k - 1] + dp[k][j]);
}
}
}
}
return n ? dp[0][n - 1] : 0;
}
};
class Solution {
public int strangePrinter(String s) {
StringBuilder sb = new StringBuilder();
for (char c : s.toCharArray()) {
if (sb.length() == 0 || sb.charAt(sb.length() - 1) != c) {
sb.append(c);
}
}
String s2 = sb.toString();
int n = s2.length();
int[][] dp = new int[n][n];
for (int i = 0; i < n; i++) dp[i][i] = 1;
for (int len = 2; len <= n; len++) {
for (int i = 0; i + len - 1 < n; i++) {
int j = i + len - 1;
dp[i][j] = dp[i + 1][j] + 1;
for (int k = i + 1; k <= j; k++) {
if (s2.charAt(i) == s2.charAt(k)) {
dp[i][j] = Math.min(dp[i][j], dp[i][k - 1] + dp[k][j]);
}
}
}
}
return n == 0 ? 0 : dp[0][n - 1];
}
}
var strangePrinter = function(s) {
// Remove consecutive duplicates
let s2 = '';
for (let c of s) {
if (s2.length === 0 || s2[s2.length - 1] !== c) s2 += c;
}
let n = s2.length;
if (n === 0) return 0;
let dp = Array.from({length: n}, () => Array(n).fill(0));
for (let i = 0; i < n; ++i) dp[i][i] = 1;
for (let len = 2; len <= n; ++len) {
for (let i = 0; i + len - 1 < n; ++i) {
let j = i + len - 1;
dp[i][j] = dp[i + 1][j] + 1;
for (let k = i + 1; k <= j; ++k) {
if (s2[i] === s2[k]) {
dp[i][j] = Math.min(dp[i][j], dp[i][k - 1] + dp[k][j]);
}
}
}
}
return dp[0][n - 1];
};
Brute-force Approach:
Removing consecutive duplicates further improves practical performance, though not the asymptotic complexity.
The Strange Printer problem is a classic example of dynamic programming with overlapping subproblems and optimal substructure. By defining a DP table for all substrings and using clever splitting based on repeated characters, we can efficiently compute the minimum number of turns needed to print any string. The approach avoids redundant work, leverages grouping of same characters, and demonstrates the power of DP for sequence transformation problems.