寶寶也能看懂的 leetcode 周賽 - 170 - 4

1312. 讓字符串成爲迴文串的最少插入次數

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))

那麼接下來按照動態規劃的慣例,咱們使用一個數組來記錄遞推的過程和中間值。具體流程以下:

  1. 申明一個二維數組。
  2. 初始化長度爲 1 時候的每一個字符串所須要的開銷,即 0,由於一個字符自身就是迴文字符串。
  3. 根據上面的遞推公式,逐層的推出每一層的值。
  4. 最終取出 [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%。

總結

這道題的風格可能更偏向於科班一點,特別是其中關於動態規劃的部分,包含着滿滿的套路感。不過我以爲這裏面比較重要的部分在與,整個推導過程當中前者和後者的關係,也就是如何基於當前值衍生出下一個值。一旦有了這個推導公式,咱們便能較爲容易的寫出對應的代碼實現了。

相關連接

qrcode_green.jpeg

相關文章
相關標籤/搜索