動(dòng)態(tài)規(guī)劃入門:LeetCode 5.最長(zhǎng)回文子串

題目:

給定一個(gè)字符串 s税肪,找到 s 中最長(zhǎng)的回文子串。你可以假設(shè) s 的最大長(zhǎng)度為 1000斩披。鏈接

示例 1:

輸入: "babad"
輸出: "bab"
注意: "aba" 也是一個(gè)有效答案产禾。

示例 2:

輸入: "cbbd"
輸出: "bb"

動(dòng)態(tài)規(guī)劃標(biāo)簽下頻率最高的題目潦闲。


相關(guān)企業(yè)

解題過程

暴力解法

沒太多好說的,復(fù)雜度O(N3)哨免,Swift不會(huì)超時(shí)茎活,Python超時(shí)。

Swift:
class Solution {
    func longestPalindrome(_ s: String) -> String {
        let count = s.count
        if count < 2 { return s }
        // 記錄子串起始位置
        var range = (0, 0)
        let sArr = Array(s)
        for i in 0..<count-1 {
            for j in i+1..<count {
                // 優(yōu)先判定當(dāng)前的子串長(zhǎng)度是否是最長(zhǎng)的琢唾,以便減少計(jì)算
                if j - i > range.1 - range.0 && isPalindrome(sArr, range: (i, j)) {
                    range = (i, j)
                }
            }
        }
        
        return String(sArr[range.0...range.1])
    }
    
    func isPalindrome(_ sArr: [String.Element], range: (Int, Int)) -> Bool {
        var left = range.0, right = range.1
        while left < right {
            if sArr[left] != sArr[right] {
                return false
            }
            left += 1
            right -= 1
        }
        return true
    }
}

Python3:
class Solution:
    def longestPalindrome(self, s: str) -> str:
        size = len(s)
        if size < 2:
            return s
        begin = 0
        end = 0
        for i in range(size - 1):
            for j in range(1, size):
                if j - i > end - begin and self.__isPalindrome(s, i, j):
                    begin = i
                    end = j
        return s[begin:end+1]

    def __isPalindrome(self, s, left, right):
        while left < right:
            if s[left] != s[right]:
                return False
            left += 1
            right -= 1
        return True

動(dòng)態(tài)規(guī)劃方法

動(dòng)態(tài)規(guī)劃的一般思考方法:

  • 定義狀態(tài)
  • 思考狀態(tài)轉(zhuǎn)移方程
  • 確定邊界
  • 結(jié)果輸出

針對(duì)當(dāng)前題目來講载荔,可以把狀態(tài)設(shè)為范圍子串是否為回文子串,用一張二維表來表示慧耍,即行(i)為子串左側(cè)范圍身辨,列(j)為子串右側(cè)范圍丐谋,為是否回文芍碧。

狀態(tài),標(biāo)綠代表回文子串

之后需要思考狀態(tài)轉(zhuǎn)移方程号俐,根據(jù)回文子串的定義泌豆,可以知道最左側(cè)和最右側(cè)的字符必須相同,至于子串是否相同則交給dp來判斷吏饿,所以我們可以得出轉(zhuǎn)移方程如下:

dp[i][j] = s[i] == s[j] && dp[i + 1][j - 1]

以此可以知道踪危,當(dāng)s[i] == s[j]時(shí),字符串是否回文是由子串是否回文決定的猪落,進(jìn)一步就要思考dp的邊界值來初始化dp基礎(chǔ)值贞远。
還是通過回文字符串的定義來看,當(dāng)字符串的長(zhǎng)度為1時(shí)笨忌,該字符串必定回文蓝仲。那么可以得出公式:

if j - i + 1 < 2 && j - 1 - (i + 1)  + 1 < 2  {
    dp[i][j] = true
}
簡(jiǎn)化為:
if j - i < 3  {
    dp[i][j] = true
}

至此,已經(jīng)推斷出動(dòng)態(tài)規(guī)劃所需的大部分條件官疲,我們要考慮怎樣使用這些條件輸出期望的結(jié)果袱结。


確定子串?是否回文

如果需要確定子串是否回文途凫,依賴的是它的最大子串垢夹,即(1, 4)是否回文,這就涉及到一個(gè)概念無后效性维费,無后效性是動(dòng)態(tài)規(guī)劃的一個(gè)必須條件果元,動(dòng)態(tài)規(guī)劃是典型的空間換時(shí)間,要想知道當(dāng)前狀態(tài)的值犀盟,必須知道前置狀態(tài)的值而晒,所以前置狀態(tài)必須提前計(jì)算出來,這就是所謂的無后效性且蓬。
根據(jù)當(dāng)前題目中的狀態(tài)定義欣硼,要知道一個(gè)指,必須依賴它左下角的值,所以我們遍歷二維表的時(shí)候诈胜,需要先經(jīng)過(1, 4)再到(0, 5)豹障。以下舉兩個(gè)例子:

遍歷順序1

  • 按照順序1的循環(huán)結(jié)構(gòu):
for j in 1..<count {
    for i in 0..<j { ... }
}
遍歷順序2
  • 按照順序2的循環(huán)結(jié)構(gòu):
for i in (0..<count).reversed() {
    for j in i..<count { ... }
}

至于遍歷順序的選擇,只要滿足無后效性的條件焦匈,效率上差別并不是太大⊙現(xiàn)在動(dòng)態(tài)規(guī)劃問題思考的四個(gè)步驟都已經(jīng)完成,完整代碼如下:

Swift:
class Solution {
    func longestPalindrome(_ s: String) -> String {
        let count = s.count
        if count < 2 { return s }
        // 定義狀態(tài)缓熟,dp[i][j]表示字符串下標(biāo)i...j的子串
        var dp = [[Bool]]()
        for _ in 0..<count {
            dp.append(Array(repeating: false, count: count))
        }
        let sArr = Array(s)
        var range = (0, 0)
        // 狀態(tài)轉(zhuǎn)移方程:dp[i][j] = sArr[i] == sArr[j] && dp[i + 1][j - 1]
        for j in 1..<count {
            for i in 0..<j {
                // 方程第一個(gè)條件
                if sArr[i] == sArr[j] {
                    // 邊界條件:子串長(zhǎng)度 < 2累魔,則必定是回文
                    if j - 1 - (i + 1) + 1 < 2 {
                        dp[i][j] = true
                    } else {
                        // 方程第二個(gè)條件
                        dp[i][j] = dp[i + 1][j - 1]
                    }
                } else {
                    dp[i][j] = false
                }
                // 考慮輸出
                if dp[i][j] && j - i > range.1 - range.0 {
                    range = (i, j)
                }
            }
        }
        return String(sArr[range.0...range.1])       
    }
}


Python3:
class Solution:
    def longestPalindrome(self, s: str) -> str:
        size = len(s)
        if size < 2:
            return s
        dp = [[False for _ in range(size)] for _ in range(size)]
        begin = 0
        end = 0
        for j in range(1, size):
            for i in range(0, j):
                # 轉(zhuǎn)移方程:dp[i][j] = s[i] == s[j] && dp[i + 1][j - 1]
                if s[i] == s[j]:
                    if j - i < 3:
                        dp[i][j] = True
                    else:
                        dp[i][j] = dp[i + 1][j - 1]
                else:
                    dp[i][j] = False
                
                if dp[i][j] and j - i > end - begin:
                    begin = i
                    end = j

        return s[begin:end+1]

效率上Swift高不少

中心擴(kuò)散方法

充分利用回文字符串的特性,可以將一個(gè)回文字符串分為中心區(qū)域+兩側(cè)區(qū)域够滑,中心區(qū)域為相同的字符組成垦写,兩側(cè)區(qū)域由對(duì)應(yīng)相同的字符組成,即為回文字符串彰触。

中心區(qū)域和兩側(cè)區(qū)域

不需要關(guān)心中心區(qū)域到底有多少字符梯投,只要是相同的字符組成的必然是回文的,在此基礎(chǔ)上盡可能地向兩側(cè)擴(kuò)展况毅。

查找中心區(qū)域
while end + 1 < count && sArr[start] == sArr[end + 1] {
    end += 1
}
擴(kuò)展兩側(cè)區(qū)域
while start - 1 >= 0 && end + 1 < count && sArr[start - 1] == sArr[end + 1] {
    start -= 1
    end += 1
}

提高效率的核心做法就是把循壞快進(jìn)分蓖,當(dāng)查到核心區(qū)域的邊界時(shí),下一次循環(huán)直接從邊界位置的下一個(gè)開始即可尔许。
完整代碼如下:

Swift:
class Solution {
    func longestPalindrome(_ s: String) -> String {
        let count = s.count
        if count < 2 { return s }
        let sArr = Array(s)
        var range = (0, 0)
        var begin = 0
        while begin < count - 1 {
            var end = begin
            // 找出中心區(qū)域么鹤,后面進(jìn)行快進(jìn)操作
            while end + 1 < count && sArr[begin] == sArr[end + 1] {
                end += 1
            }
            let tmp = end
            // 從中心區(qū)域向兩側(cè)進(jìn)行擴(kuò)散
            while begin - 1 >= 0 && end + 1 < count && sArr[begin - 1] == sArr[end + 1] {
                begin -= 1
                end += 1
            }
            // 輸出值
            if end - begin > range.1 - range.0 {
                range = (begin, end)
            }
            // 開始快進(jìn)
            begin = tmp + 1
        }
        return String(sArr[range.0...range.1])
    }
}

Python3:
class Solution:
    def longestPalindrome(self, s: str) -> str:
        size = len(s)
        if size < 2:
            return s
        loc = [0, 0]
        begin = 0
        while begin < size - 1:
            end = begin
            while end + 1 < size and s[end + 1] == s[begin]:
                end += 1
            tmp = end
            while begin - 1 >= 0 and end + 1 < size and s[begin - 1] == s[end + 1]:
                begin -= 1
                end += 1
            if end - begin > loc[1] - loc[0]:
                loc[0] = begin
                loc[1] = end
            begin = tmp + 1
        return s[loc[0]:loc[1] + 1]

中心擴(kuò)散法效率高了很多

因?yàn)閯?dòng)態(tài)規(guī)劃方法說到底還是屬于暴力解法,所以效率上肯定要比題目特定的優(yōu)化解法慢味廊,不過動(dòng)規(guī)思路更為通用蒸甜,此篇也更多為了讓自己進(jìn)一步理解,希望對(duì)你也有所幫助毡们。
以下也可以用中心擴(kuò)散的思想來判定回文:
125. 驗(yàn)證回文串

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末迅皇,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子衙熔,更是在濱河造成了極大的恐慌登颓,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,496評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件红氯,死亡現(xiàn)場(chǎng)離奇詭異框咙,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)痢甘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門喇嘱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人塞栅,你說我怎么就攤上這事者铜。” “怎么了?”我有些...
    開封第一講書人閱讀 162,632評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵作烟,是天一觀的道長(zhǎng)愉粤。 經(jīng)常有香客問我,道長(zhǎng)拿撩,這世上最難降的妖魔是什么衣厘? 我笑而不...
    開封第一講書人閱讀 58,180評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮压恒,結(jié)果婚禮上影暴,老公的妹妹穿的比我還像新娘。我一直安慰自己探赫,他們只是感情好型宙,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,198評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著期吓,像睡著了一般早歇。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上讨勤,一...
    開封第一講書人閱讀 51,165評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音晨另,去河邊找鬼潭千。 笑死,一個(gè)胖子當(dāng)著我的面吹牛借尿,可吹牛的內(nèi)容都是我干的刨晴。 我是一名探鬼主播,決...
    沈念sama閱讀 40,052評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼路翻,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼狈癞!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起茂契,我...
    開封第一講書人閱讀 38,910評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤蝶桶,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后掉冶,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體真竖,經(jīng)...
    沈念sama閱讀 45,324評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,542評(píng)論 2 332
  • 正文 我和宋清朗相戀三年厌小,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了恢共。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,711評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡璧亚,死狀恐怖讨韭,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤透硝,帶...
    沈念sama閱讀 35,424評(píng)論 5 343
  • 正文 年R本政府宣布吉嚣,位于F島的核電站,受9級(jí)特大地震影響蹬铺,放射性物質(zhì)發(fā)生泄漏尝哆。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,017評(píng)論 3 326
  • 文/蒙蒙 一甜攀、第九天 我趴在偏房一處隱蔽的房頂上張望秋泄。 院中可真熱鬧,春花似錦规阀、人聲如沸恒序。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽歧胁。三九已至,卻和暖如春厉碟,著一層夾襖步出監(jiān)牢的瞬間喊巍,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工箍鼓, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留崭参,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,722評(píng)論 2 368
  • 正文 我出身青樓款咖,卻偏偏與公主長(zhǎng)得像何暮,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子铐殃,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,611評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容