Initially on a notepad only one character 'A' is present. You can perform two operations on this notepad for each step:
Copy All
: You can copy all the characters present on the notepad (partial copy is not allowed).Paste
: You can paste the characters which are copied last time.
Given a number n
. You have to get exactly n
'A' on the notepad by performing the minimum number of steps permitted. Output the minimum number of steps to get n
'A'.
Example 1:
Input: 3 Output: 3 Explanation: Intitally, we have one character 'A'. In step 1, we use Copy All operation. In step 2, we use Paste operation to get 'AA'. In step 3, we use Paste operation to get 'AAA'.
Note:
- The
n
will be in the range [1, 1000].
這道題只給了我們兩個按鍵,如果只能選擇兩個按鍵,那么博主一定會要復制和粘貼,此二鍵在手,天下我有!!!果然,這道題就是給了復制和粘貼這兩個按鍵,然后給了一個A,目標時利用這兩個鍵來打印出n個A,注意復制的時候時全部復制,不能選擇部分來復制,然后復制和粘貼都算操作步驟,問打印出n個A需要多少步操作。對於這種有明顯的遞推特征的題,要有隱約的感覺,一定要嘗試遞歸和 DP。遞歸解法一般接近於暴力搜索,但是有時候是可以優化的,從而能夠通過 OJ。而一旦遞歸不行的話,那么一般來說 DP 這個大殺器都能解的。還有一點,對於這種題,找規律最重要,DP 要找出狀態轉移方程,而如果無法發現內在的聯系,那么狀態轉移方程就比較難寫出來了。所以,從簡單的例子開始分析,試圖找規律:
當n = 1時,已經有一個A了,不需要其他操作,返回0
當n = 2時,需要復制一次,粘貼一次,返回2
當n = 3時,需要復制一次,粘貼兩次,返回3
當n = 4時,這就有兩種做法,一種是需要復制一次,粘貼三次,共4步,另一種是先復制一次,粘貼一次,得到 AA,然后再復制一次,粘貼一次,得到 AAAA,兩種方法都是返回4
當n = 5時,需要復制一次,粘貼四次,返回5
當n = 6時,需要復制一次,粘貼兩次,得到 AAA,再復制一次,粘貼一次,得到 AAAAAA,共5步,返回5
通過分析上面這6個簡單的例子,已經可以總結出一些規律了,首先對於任意一個n(除了1以外),最差的情況就是用n步,不會再多於n步,但是有可能是會小於n步的,比如 n=6 時,就只用了5步,仔細分析一下,發現時先拼成了 AAA,再復制粘貼成了 AAAAAA。那么什么情況下可以利用這種方法來減少步驟呢,分析發現,小模塊的長度必須要能整除n,這樣才能拆分。對於 n=6,我們其實還可先拼出 AA,然后再復制一次,粘貼兩次,得到的還是5。分析到這里,解題的思路應該比較清晰了,找出n的所有因子,然后這個因子可以當作模塊的個數,再算出模塊的長度 n/i,調用遞歸,加上模塊的個數i來更新結果 res 即可,參見代碼如下:
解法一:
class Solution { public: int minSteps(int n) { if (n == 1) return 0; int res = n; for (int i = n - 1; i > 1; --i) { if (n % i == 0) { res = min(res, minSteps(n / i) + i); } } return res; } };
下面這種方法是用 DP 來做的,我們可以看出來,其實就是上面遞歸解法的迭代形式,思路沒有任何區別,參見代碼如下:
解法二:
class Solution { public: int minSteps(int n) { vector<int> dp(n + 1, 0); for (int i = 2; i <= n; ++i) { dp[i] = i; for (int j = i - 1; j > 1; --j) { if (i % j == 0) { dp[i] = min(dp[i], dp[j] + i / j); } } } return dp[n]; } };
上面的解法其實可以做一丟丟的優化,在遍歷j的時候,只要發現了第一個能整除i的j,直接可以用 dp[j] + i/j 來更新 dp[i],然后直接 break 掉j的循環,之后的j就不用再考慮了,可能是因為因數越大,其需要的按鍵數就越少吧,參見代碼如下:
解法三:
class Solution { public: int minSteps(int n) { vector<int> dp(n + 1); for (int i = 2; i <= n; ++i) { dp[i] = i; for (int j = i - 1; j > 1; --j) { if (i % j == 0) { dp[i] = dp[j] + i / j; break; } } } return dp[n]; } };
下面我們來看一種省空間的方法,不需要記錄每一個中間值,而是通過改變n的值來實時累加結果res,參見代碼如下:
解法四:
class Solution { public: int minSteps(int n) { int res = 0; for (int i = 2; i <= n; ++i) { while (n % i == 0) { res += i; n /= i; } } return res; } };
Github 同步地址:
https://github.com/grandyang/leetcode/issues/650
類似題目:
Broken Calculator
參考資料:
https://leetcode.com/problems/2-keys-keyboard/
https://leetcode.com/problems/2-keys-keyboard/discuss/105899/Java-DP-Solution