Want Help Cracking FAANG?

(Then click this)

×
Back to Question Bank

664. Strange Printer - Leetcode Solution

Problem Description

The Strange Printer problem is as follows:

There is a strange printer with the following properties:

  • It can only print a sequence of the same character in a single turn.
  • In each turn, you can select any substring of the given string and print a single character over the entire substring. This operation will overwrite any existing characters in that substring.
Given a string 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.

Thought Process

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.

Solution Approach

Let's break down the dynamic programming solution step-by-step:

  1. Define Subproblems:
    • Let dp[i][j] represent the minimum number of turns needed to print the substring s[i:j+1].
  2. Base Case:
    • If i == j (a single character), only one turn is needed: dp[i][j] = 1.
  3. Transition:
    • For substring s[i:j+1], consider two cases:
      • Print s[i] over s[i:j+1] in one turn, then print the rest optimally: dp[i][j] = 1 + dp[i+1][j].
      • If there is a 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.
  4. Optimization:
    • Remove consecutive duplicate characters in s at the start, as printing over repeated characters doesn't require extra turns.
  5. Implementation:
    • Iterate over all possible substring lengths, filling the dp table from smaller to larger substrings.
    • The answer is dp[0][n-1], where n is the length of s.

Example Walkthrough

Let's consider the input s = "aba".

  1. Step 1: We want to print "aba" in the minimum number of turns.
  2. Step 2: First, print 'a' at s[0] (positions 0). Now, the string is "a__".
  3. Step 3: Next, print 'b' at s[1] (positions 1). The string becomes "ab_".
  4. Step 4: Now, print 'a' at 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.
  5. Result: The minimum number of turns is 2: one for the first 'a', one for 'b', and then the last 'a' can be included in the first 'a' print if we split the string correctly (by dynamic programming). The DP solution finds this optimal grouping.

For more complex cases, the DP table will try all possible splits and combine the minimum number of turns.

Code Implementation

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];
};
      

Time and Space Complexity

Brute-force Approach:

  • Would try all possible ways to split and print substrings, leading to exponential time complexity: O(2n) or worse.
Optimized DP Approach:
  • Time Complexity: O(n3), where n is the length of the processed string. For each substring (O(n2)), we may check all possible split points (O(n)).
  • Space Complexity: O(n2), for the DP table storing results for all substrings.

Removing consecutive duplicates further improves practical performance, though not the asymptotic complexity.

Summary

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.