LeetCode: -Dynamic Programming-Strange Printer[664]

题目

一个打印机,每一轮能打印一个同一个字符的序列,能在下一轮在任何位置开始任何位置结束打印一个其他的字符。后打印的将覆蓋先打印的。
给定一个字符串,求最小的打印轮次。

示例:

Input: "aaabbb"
Output: 2
Explanation: 先打印 "aaa" 再打印 "bbb".
Input: "aba"
Output: 2
Explanation: 先打印 "aaa" 再从第二个位置打印 "b" 会将已经存在的 'a' 覆蓋.

分析

二维DP问题
1、dp[i][j] 表示范围 [i, j) 内的最小轮次. 运用memoization 和 DFS来避免计算不必要的子问题.
2、贪婪的选择: 对于任意一个序列, 首先打印首字符. 比如 s[0] = ‘a’. 如果存在最优的解决方案, 不是将s[0] 打印的范围是 [0, k)作为第一步, 我们可以将这一个移到第一步,并且保证其他步为原来的顺序. 如果有字符在s[0]的原来的顺序之前打印了,且范围在k之前,我们将该字符的打印开始位置移动到k开始.

根据不同的方法将 s[0]= ‘a’ 和其他部分的 ‘a’结合起来,我们有:

dp[i][j] = min(dp[i][j], dp[start][k]+dp[k][end]), 对于每个 k 有 s[k] == s[i],

这里的start和end分别是开始的一个字符和最后的一个字符,不是s[i].

例如

给定一个序列 "aaa bcd aaa def aaa ccd aaa", 我们有三种选择来结合 s[0] = 'a' 和其他部分的 'a'.
bcd + aaa aaa def aaa ccd aaa,与之一样的是 bcd + aaa def aaa ccd
bcd aaa def + aaa ccd
bcd aaa def aaa ccd + aaa

代码

class Solution {
public:
    int strangePrinter(string s) {
        int n = s.size();
        vector<vector<int>> dp(n+1, vector<int>(n+1, 0));
        return helper(s, dp, 0, n);
    }
private:
    int helper(string& str, vector<vector<int>>& dp, int s, int e) {
        if (s >= e) return 0;
        if (dp[s][e]) return dp[s][e];
        // 处理 str[s] 的首尾字符
        // 注意范围是左闭右开 [s,e) [l,r)
        int l = s, r = e;
        while (l < e && str[l] == str[s]) l++;
        while (r > l && str[r-1] == str[s]) r--;
        dp[s][e] = 1+helper(str, dp, l, r);
        for (int i = l; i < r; i++) {
            if (str[i] == str[s]) {
                dp[s][e] = min(dp[s][e], helper(str,dp,l,i)+helper(str,dp,i,r));
                while (i < e && str[i] == str[s]) i++;
            }   
        }
        return dp[s][e];
    }
};
点赞