There is a strange printer with the following two special requirements:
- 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 places, and will cover the original existing characters.
Given a string consists of lower English letters only, your job is to count the minimum number of turns the printer needed in order to print it.
Example 1:
Input: "aaabbb" Output: 2 Explanation: Print "aaa" first and then print "bbb".
Example 2:
Input: "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'.
Hint: Length of the given string will not exceed 100.
這道題說有一種奇怪的打印機每次只能打印一排相同的字符,然后可以在任意起點和終點位置之間打印新的字符,用來覆蓋原有的字符。現在給了我們一個新的字符串,問我們需要幾次可以正確的打印出來。題目中給了兩個非常簡單的例子,主要是幫助我們理解的。博主最開始想的方法是一種類似貪婪算法,先是找出出現次數最多的字符,然后算需要多少次變換能將所有其他字符都變成那個出現最多次的字符,結果fail了。然后又試了一種類似剝洋蔥的方法,從首尾都分別找連續相同的字符,如果首尾字符相同,則兩部分一起移去,否則就移去連續相同個數多的子序列,這種基於貪婪算法的解法還是fail了,所以這道題是典型的只能動態規划Dynamic Programming,而不能用貪婪算法Greedy Algorithm的題。這道題的解題思路跟之前那道Remove Boxes很相似,博主在那個帖子中做了詳細的講解,是根據fun4leetcode大神的帖子寫的,大神的思路對解這道題也相當有幫助。其實這道題並沒有之前那道Remove Boxes難,移除盒子的題有隱含的條件需要加到重現關系中,大大地增加了題目的難度,非常地難想出來,這道題沒有隱含條件都是個Hard題,那道題妥妥應該是Super Hard。
好,話不多說,來分析這道題吧。思考的線索和思路很重要,不理解核心精髓,當背題俠是沒用的,稍微變個形式又不會了,博主就經常是這樣的-.-!!!。既然說了要用DP來做,先整個二維dp數組唄,其中dp[i][j]表示打印出字符串[i, j]范圍內字符的最小步數,難點就是找遞推公式啦。遇到乍看去沒啥思路的題,博主一般會先從簡單的例子開始,看能不能分析出規律,從而找到解題的線索。首先如果只有一個字符,比如字符串是"a"的話,那么直接一次打印出來就行了。如果字符串是"ab"的話,那么我們要么先打印出"aa",再改成"ab",或者先打印出"bb",再改成"ab"。同理,如果字符串是"abc"的話,就需要三次打印。那么一個很明顯的特征是,如果沒有重復的字符,打印的次數就是字符的個數。燃鵝這題的難點就是要處理有相同字符的情況,比如字符串是"aba"的時候,我們先打"aaa"的話,兩步就搞定了,如果先打"bbb"的話,就需要三步。我們再來看一個字符串"abcb",我們知道需要需要三步,我們看如果把這個字符串分成兩個部分"a"和"bcb",它們分別的步數是1和2,加起來的3是整個的步數。而對於字符串"abba",如果分成"a"和"bba",它們分別的步數也是1和2,但是總步數卻是2。這是因為分出的"a"和"bba"中的最后一個字符相同。對於字符串"abbac",因為位置0上的a和位置3上的a相同,那么整個字符串的步數相當於"bb"和"ac"的步數之和,為3。那么分析到這,是不是有點眉目了?我們關心的是字符相等的地方,對於[i, j]范圍的字符,我們從i+1位置上的字符開始遍歷到j,如果和i位置上的字符相等,我們就以此位置為界,將[i+1, j]范圍內的字符拆為兩個部分,將二者的dp值加起來,和原dp值相比,取較小的那個。所以我們的遞推式如下:
dp[i][j] = min(dp[i][j], dp[i + 1][k - 1] + dp[k][j] (s[k] == s[i] and i + 1 <= k <= j)
要注意一些初始化的值,dp[i][i]是1,因為一個字符嘛,打印1次,還是就是在遍歷k之前,dp[i][j]初始化為 1 + dp[i + 1][j],為啥呢,可以看成在[i + 1, j]的范圍上多加了一個s[i]字符,最壞的情況就是加上的是一個不曾出現過的字符,步數頂多加1步,注意我們的i是從后往前遍歷的,當然你可以從前往后遍歷,參數對應好就行了,參見代碼如下:
解法一:
class Solution { public: int strangePrinter(string s) { int n = s.size(); vector<vector<int>> dp(n, vector<int>(n, 0)); for (int i = n - 1; i >= 0; --i) { for (int j = i; j < n; ++j) { dp[i][j] = (i == j) ? 1 : (1 + dp[i + 1][j]); for (int k = i + 1; k <= j; ++k) { if (s[k] == s[i]) dp[i][j] = min(dp[i][j], dp[i + 1][k - 1] + dp[k][j]); } } } return (n == 0) ? 0 : dp[0][n - 1]; } };
理解了上面的DP的方法,那么也可以用遞歸的形式來寫,記憶數組memo就相當於dp數組,整個思路完全一樣,參見代碼如下:
解法二:
class Solution { public: int strangePrinter(string s) { int n = s.size(); vector<vector<int>> memo(n, vector<int>(n, 0)); return helper(s, 0, n - 1, memo); } int helper(string s, int i, int j, vector<vector<int>>& memo) { if (i > j) return 0; if (memo[i][j]) return memo[i][j]; memo[i][j] = helper(s, i + 1, j, memo) + 1; for (int k = i + 1; k <= j; ++k) { if (s[k] == s[i]) { memo[i][j] = min(memo[i][j], helper(s, i + 1, k - 1, memo) + helper(s, k, j, memo)); } } return memo[i][j]; } };
類似題目:
參考資料:
https://discuss.leetcode.com/topic/100137/java-solution-dp
https://discuss.leetcode.com/topic/100212/c-29ms-dp-solution
https://discuss.leetcode.com/topic/100135/java-o-n-3-short-dp-solution