Hi 你們好,我是張小豬。歡迎來到『寶寶也能看懂』系列之 leetcode 周賽題解。git
這裏是第 170 期的第 4 題,也是題目列表中的第 1312 題 -- 『讓字符串成爲迴文串的最少插入次數』github
給你一個字符串 s
,每一次操做你均可以在字符串的任意位置插入任意字符。算法
請你返回讓 s
成爲迴文串的 最少操做次數。shell
「迴文串」是正讀和反讀都相同的字符串。segmentfault
示例 1:數組
輸入:s = "zzazz" 輸出:0 解釋:字符串 "zzazz" 已是迴文串了,因此不須要作任何插入操做。
示例 2:優化
輸入:s = "mbadm" 輸出:2 解釋:字符串可變爲 "mbdadbm" 或者 "mdbabdm" 。
示例 3:spa
輸入:s = "leetcode" 輸出:5 解釋:插入 5 個字符後字符串變爲 "leetcodocteel" 。
示例 4:code
輸入:s = "g" 輸出:0
示例 5:blog
輸入:s = "no" 輸出:1
提示:
1 <= s.length <= 500
s
中全部字符都是小寫字母。HARD
一看到迴文字符串,腦中第一反應就是那個馬拉車算法,而後瞬間就不想作這道題了,小豬鬧脾氣,哼 T_T
不過冷靜下來以後仍是先仔細看一下題的內容。題目要求爲給定一個字符串,咱們能夠向裏面任意位置插入任何字符,最終要使得該字符串變成一個迴文字符串。須要獲得這個最小的被插入的字符數量。
其實個人第一反應是要不要找到當前的最長迴文子字符串,而後基於它來判斷兩邊的差別從而獲得結果。但是這樣會有一個問題,那就是基於最長迴文子字符串做爲中點來進行填充,真的是最優解麼?對於這個疑問咱們能夠看一下這個例子。
對於 "abccdec" 這個字符串,咱們能夠獲得最長的迴文子字符串是 "cc"。若是咱們基於它做爲中點,那麼因爲左右徹底不同,須要 5 個字符來進行補齊,例如 "abcedccdecba"。但其實咱們能夠作到只用 4 個字符進行補齊,即 "abcedcdecba"。這是因爲對於奇數長度的字符串,中點其實不須要進行補齊,而咱們也利用了左右一對匹配的 "c" 字符,因此能夠減小一個補齊字符的需求。
那麼咱們嘗試換一個思路,回到這個字符串自己的特性。若是它最後要變成一個迴文字符串,那麼它最終的最左側和最右側的字符必定要是相同的。因此反推回來,若是當前最左側和最右側的字符同樣,即可繼續遍歷。可是若是不同呢?這時候咱們能夠進行填補。但是填補的方式有兩種:
對於這兩種填補方式,它們的填補消耗都是 1 個字符,咱們也沒法肯定哪種是最優解。因此只有繼續推導,直到最終遍歷完成後即可獲得全局最優解。
基於上述思路,咱們不難想到這裏能夠利用動態規劃的方式來進行實現,或者說動態規劃是對於這種思路方式的一種比較不錯的實現。
那麼什麼是動態規劃呢?詳細的內容仍是期待新坑吧,哈哈哈哈,小豬這裏就不展開啦。咱們這裏先基於這道題目的內容進行分析和說明就行了。
如上述思路中提到的內容,若是咱們想知道區間 [left, right]
範圍裏的最優解,那麼可能存在兩種狀況,即 s[left] === s[right]
或者 s[left] !== s[right]
。針對這兩種狀況,咱們能夠獲得兩種對應的結果,即 0 + [left + 1, right - 1]
和 1 + min([left + 1, right], [left, right - 1])
。若是寫成一個遞推公式的話能夠是:
f(left, right) = s[left] === s[right] ? f(left + 1, right - 1) : 1 + min(f(left + 1, right), f(left, right - 1))
那麼接下來按照動態規劃的慣例,咱們使用一個數組來記錄遞推的過程和中間值。具體流程以下:
[0, s.length - 1]
對應的值就是咱們的結果。根據上述流程,咱們能夠實現相似下面的代碼:
const minInsertions = s => { const LEN = s.length; const dp = []; for (let i = 0; i < LEN; ++i) { dp[i] = new Uint16Array(LEN); dp[i][i + 1] = s[i] === s[i + 1] ? 0 : 1; } for (let i = 2; i < s.length; ++i) { for (j = 0; j < s.length - i; ++j) { dp[j][j + i] = s[j] === s[j + i] ? dp[j + 1][j + i - 1] : 1 + Math.min(dp[j + 1][j + i], dp[j][j + i - 1]); } } return dp[0][s.length - 1]; };
上面的代碼時間複雜度 O(n^2),空間複雜度也是 O(n^2)。那麼其實按照經驗,咱們能夠嘗試一下把空間複雜度壓縮到 O(n),即不是用二維數組,只是用一維數組來記錄遞推的中間值。
不過這裏要注意的是,因爲咱們沒法保存全部歷史的中間值,因此咱們的遍歷遞推方向作出了一點調整。具體的代碼以下:
const minInsertions = s => { const LEN = s.length; const dp = new Uint16Array(LEN); for (let i = LEN - 2; i >= 0; i--) { let prev = 0; for (let j = i; j < LEN; j++) { const tmp = dp[j]; dp[j] = s[i] == s[j] ? prev : 1 + Math.min(dp[j], dp[j - 1]); prev = tmp; } } return dp[s.length - 1]; };
這段代碼跑出了 60ms,暫時 beats 100%。
這道題的風格可能更偏向於科班一點,特別是其中關於動態規劃的部分,包含着滿滿的套路感。不過我以爲這裏面比較重要的部分在與,整個推導過程當中前者和後者的關係,也就是如何基於當前值衍生出下一個值。一旦有了這個推導公式,咱們便能較爲容易的寫出對應的代碼實現了。