LeetCode 回文字符串算法: 動態(tài)規(guī)劃算法 & 中心檢測法 & Manacher's Algorithm 馬拉車算法

關(guān)于我的 Leetcode 題目解答局嘁,代碼前往 Github:https://github.com/chenxiangcyr/leetcode-answers


問題:給出一個字符串 S,找到在 S 中的最長的回文子串巢掺。

LeetCode題目:5. Longest Palindromic Substring
Given a string s, find the longest palindromic substring in s. You may assume that the maximum length of s is 1000.

Example:
Input: "babad"
Output: "bab"
Note: "aba" is also a valid answer.

算法對比:

  • 暴力枚舉法
    • O(N3)
    • 遍歷所有子字符串,子串數(shù)為 N2,長度平均為 N/2
  • 動態(tài)規(guī)劃法
    • O(N2)
    • 兩層循環(huán)礁凡,外層循環(huán)從后往前掃叠赐,內(nèi)層循環(huán)從當前字符掃到結(jié)尾處,省略已經(jīng)判斷過的記錄
  • 中心檢測法
    • O(N2)
    • 分奇偶兩種情況撒汉,以 i 為中心不斷向兩邊擴展判斷,無需額外空間
  • 馬拉車算法
    • O(N)
    • 從左到右掃描涕滋,省略已經(jīng)判斷過的記錄睬辐,線性

動態(tài)規(guī)劃法

    // DP Solution
    public String longestPalindromeDP(String s) {
        if(s.isEmpty()) return "";
        
        int n = s.length();
        String result = null;
    
        // dp[i][j] 表示第i個字符到第j個字符是否為回文
        boolean[][] dp = new boolean[n][n];
    
        for (int i = n - 1; i >= 0; i--) {
            for (int j = i; j < n; j++) {
                dp[i][j] = s.charAt(i) == s.charAt(j) && (j - i < 3 || dp[i + 1][j - 1]);
            
                if (dp[i][j] && (result == null || j - i + 1 > result.length())) {
                    result = s.substring(i, j + 1);
                }
            }
        }
    
        return result;
    }

中心檢測法

    // 中心檢測法
    public String longestPalindromeCenter(String s) {
        int start = 0, end = 0;
        
        for (int i = 0; i < s.length(); i++) {
            /*
            一個回文字符串可以從中心向兩邊擴展,會有 2n - 1 個中心宾肺,而不是 n 個中心溯饵。
            因為中心可以存在于兩個字符中間,例如 abba锨用,中心在b和b中間丰刊。
            */
            
            // 以第i個字符為中心向兩邊擴展
            int len1 = expandAroundCenter(s, i, i);
            
            // 以第i個字符和第i+1個字符的中間為中心向兩邊擴展
            int len2 = expandAroundCenter(s, i, i + 1);
            
            int len = Math.max(len1, len2);
            
            if (len > end - start) {
                start = i - (len - 1) / 2;
                end = i + len / 2;
            }
        }
        
        return s.substring(start, end + 1);
    }

    private int expandAroundCenter(String s, int left, int right) {
        int L = left, R = right;
        
        while (L >= 0 && R < s.length() && s.charAt(L) == s.charAt(R)) {
            L--;
            R++;
        }
        
        return R - L - 1;
    }

Manacher's Algorithm 馬拉車算法

預處理1:解決字符串長度奇偶問題
馬拉車算法可以看成是中心檢測法的升級版本,在上面的表格中提到中心檢測法是需要區(qū)分奇偶兩種情況的黔酥,那么在馬拉車算法中首先要解決的就是這個問題藻三。

這里首先對字符串做一個預處理,在所有的空隙位置(包括首尾)插入同樣的符號跪者。無論原字符串是奇數(shù)還是偶數(shù)棵帽,通過這種做法,都會使得處理過的字符串變成奇數(shù)長度渣玲。

以插入#號為例:
123(長度為3) -> #1#2#3# (長度為7)
abccba (長度為6)-> #a#b#c#c#b#a#(長度為13)

我們把一個回文串中最左或最右位置的字符與其對稱軸的距離稱為回文半徑逗概。
馬拉車算法定義了一個回文半徑數(shù)組 p,用 p[i] 表示以第 i 個字符為對稱軸的回文串的回文半徑忘衍。
例如:

字符串 T = # a # b # a # a # b # a #
半徑數(shù)組P = 0 1 0 3 0 1 6 1 0 3 0 1 0

Looking at P, we immediately see that the longest palindrome is “abaaba”, as indicated by P6 = 6

為了進一步減少編碼的復雜度逾苫,可以在字符串的開始加入另一個特殊字符,這樣就不用特殊處理越界問題枚钓,比如$#a#b#a#

    // Manacher's Algorithm 馬拉車算法
    public String longestPalindromeManacher(String s) {
        if (s.length() <= 1) {
            return s;
        }
        
        // 解決字符串長度奇偶問題
        StringBuilder stringBuilder = new StringBuilder("$");
        for (char c : s.toCharArray()) {
            stringBuilder.append("#");
            stringBuilder.append(c);
        }
        stringBuilder.append("#");
        String str = stringBuilder.toString();

        int id = 0;
        int idMax = 0;
        int index = 0;
        int maxLength = 0;

        int p[] = new int[str.length()];

        // 遍歷每一個字符
        for (int curr = 1; curr < str.length(); curr++) {
            // j 是 curr 關(guān)于 id 的對稱點
            int j = 2 * id - curr;
            
            // 如果 idMax > curr铅搓,那么P[curr] >= MIN(P[j], idMax - curr)
            if (idMax > curr) {
                if (p[j] < idMax - curr)
                    p[curr] = p[j];
                else
                    p[curr] = idMax - curr;
            } else {
                p[curr] = 1;
            }
            
            while (curr + p[curr] < str.length() && str.charAt(curr + p[curr]) == str.charAt(curr - p[curr])) {
                p[curr]++;
            }

            if (curr + p[curr] > idMax) {
                id = curr;
                idMax = curr + p[curr];
            }

            if (p[curr] > maxLength) {
                maxLength = p[curr];
                index = curr;
            }
        }
        
        return s.substring((index - maxLength) / 2, (index + maxLength) / 2 - 1);
    }

其他回文字符串題目

LeetCode題目:131. Palindrome Partitioning
Given a string s, partition s such that every substring of the partition is a palindrome.
Return all possible palindrome partitioning of s.
Example:

Input: "aab"
Output:
[
["aa","b"],
["a","a","b"]
]

class Solution {
    public List<List<String>> allPartition = new ArrayList<List<String>>();
    public List<String> onePartition = new ArrayList<String>();
    
    public List<List<String>> partition(String s) {
        
        travel(s.toCharArray(), 0);
        
        return allPartition;
    }
    
    public void travel(char[] arr, int startIdx) {
        for(int i = startIdx; i < arr.length; i++) {
            if(isPalindrome(arr, startIdx, i)) {
                String str = new String(Arrays.copyOfRange(arr, startIdx, i + 1));
                
                onePartition.add(str);
            
                // to the end
                if(i == arr.length - 1) {
                    allPartition.add(new ArrayList(onePartition));
                }
                else {
                    travel(arr, i + 1);
                }

                // backtracking
                onePartition.remove(onePartition.size() - 1);
            }
        }
    }
    
    public boolean isPalindrome(char[] arr, int startIdx, int endIdx) {
        while(startIdx <= endIdx && arr[startIdx] == arr[endIdx]) {
            startIdx++;
            endIdx--;
        }
        
        return startIdx >= endIdx;
    }
}

LeetCode題目:132. Palindrome Partitioning II
Given a string s, partition s such that every substring of the partition is a palindrome.
Return the minimum cuts needed for a palindrome partitioning of s.
Example:

Input: "aab"
Output: 1
Explanation: The palindrome partitioning ["aa","b"] could be produced using 1 cut.

class Solution {
    public int minCut(String s) {
        return travel(s, 0,0, new HashMap<>());
    }
    
    public int travel(String s, int pos, int cut, Map<Integer,Integer> cache) {
        if(pos>=s.length()) {
            return cut - 1;
        }
        
        int min = Integer.MAX_VALUE;
        if(cache.containsKey(pos)) {
            return cut + cache.get(pos);
        }
        
        for(int end = pos + 1; end <= s.length(); ++end){
            String sub = s.substring(pos, end);
            
            if(isPalindrome(sub)) {
                min = Math.min(min, travel(s, end, cut+1, cache));
            }
        }
        
        cache.put(pos, min - cut);
        
        return min;
    }
    
    public boolean isPalindrome(String s) {
        int startIdx = 0;
        int endIdx = s.length() - 1;
        
        while(startIdx <= endIdx && s.charAt(startIdx) == s.charAt(endIdx)) {
            startIdx++;
            endIdx--;
        }
        
        return startIdx >= endIdx;
    }
}

LeetCode題目:214. Shortest Palindrome
Given a string S, you are allowed to convert it to a palindrome by adding characters in front of it. Find and return the shortest palindrome you can find by performing this transformation.

For example:
Given "aacecaaa", return "aaacecaaa".
Given "abcd", return "dcbabcd".

代碼如下,時間復雜度O(n^2)

class Solution {
    public String shortestPalindrome(String s) {
        if(s == null || s.length() <= 1) return s;
        
        // 找到s中包含第一個字符的最長回文子串
        int right = s.length() - 1;
        while(right > 0 && !isPalindrome(s, 0, right)) {
            right--;
        }
        // 最壞情況 right = 0搀捷,例如abc
        
        StringBuilder sb = new StringBuilder();
        
        for(int i = s.length() - 1; i > right; i--) {
            sb.append(s.charAt(i));
        }
        sb.append(s);
        
        return sb.toString();
    }
    
    public boolean isPalindrome(String s, int start, int end) {
        while(start <= end) {
            if(s.charAt(start) != s.charAt(end)) {
                return false;
            }
            start++;
            end--;
        }
        
        return true;
    }
}

對于此題星掰,有時間復雜度為O(n)的算法,利用了 KMP 算法嫩舟,思路參考https://leetcode.com/articles/shortest-palindrome/氢烘,代碼如下:

public class Solution {
class Solution {
    public String shortestPalindrome(String s) {
        String temp = s + "#" + new StringBuilder(s).reverse().toString();
        int[] table = getTable(temp);

        return new StringBuilder(s.substring(table[table.length - 1])).reverse().toString() + s;
    }

    public int[] getTable(String s){
        int[] table = new int[s.length()];
        table[0] = 0;
        
        for(int i = 1; i < s.length(); i++)
        {
            int t = table[i - 1];
            
            while(t > 0 && s.charAt(i) != s.charAt(t)) {
                t = table[t - 1];
            }
            
            if(s.charAt(i) == s.charAt(t)) {
                t++;
            }
            
            table[i] = t;
        }
        
        return table;
    }
}

LeetCode題目:125. Valid Palindrome
Given a string, determine if it is a palindrome, considering only alphanumeric characters and ignoring cases.

For example,
"A man, a plan, a canal: Panama" is a palindrome.
"race a car" is not a palindrome.

class Solution {
    public boolean isPalindrome(String s) {
        // we define empty string as valid palindrome
        if(s.isEmpty()) {
            return true;
        }
        
        char[] arr = s.toCharArray();
        
        int i = 0;
        int j = arr.length - 1;
        
        while(i < j) {
            Character ci = new Character(arr[i]);
            Character cj = new Character(arr[j]);
            
            // considering only alphanumeric characters
            if(!Character.isDigit(ci) && !Character.isLetter(ci)) {
                i++;
                continue;
            }
            
            if(!Character.isDigit(cj) && !Character.isLetter(cj)) {
                j--;
                continue;
            }
            
            // ignoring cases
            if(Character.toUpperCase(ci) != Character.toUpperCase(cj)) {
                return false;
            }
            
            i++;
            j--;
        }
        
        return true;
    }
}

LeetCode題目:680. Valid Palindrome II
Given a non-empty string s, you may delete at most one character. Judge whether you can make it a palindrome.

Example 1:
Input: "aba"
Output: True

Example 2:
Input: "abca"
Output: True
Explanation: You could delete the character 'c'.

Note:

  • The string will only contain lowercase characters a-z. The maximum length of the string is 50000.
class Solution {
    public boolean validPalindrome(String s) {
        if(s.isEmpty()) return true;
        
        int i = 0;
        int j = s.length() - 1;
        
        while(i < j) {
            if(s.charAt(i) != s.charAt(j)) {
                return validPalindrome(s, i + 1, j) || validPalindrome(s, i, j - 1);
            }
            i++;
            j--;
        }
        
        return true;
    }
    
    public boolean validPalindrome(String s, int from, int to) {
        while(from < to) {
            if(s.charAt(from) != s.charAt(to)) {
                return false;
            }
            from++;
            to--;
        }
        
        return true;
    }
}

LeetCode題目:266. Palindrome Permutation
Given a string, determine if a permutation of the string could form a palindrome.

For example,
"code" -> False, "aab" -> True, "carerac" -> True.

class Solution {
    public boolean canPermutePalindrome(String s) {
        // key: char, value: count
        Map<Character, Integer> map = new HashMap<>();
        
        for(char c : s.toCharArray()) {
            map.put(c, map.getOrDefault(c, 0) + 1);
        }
        
        // 出現(xiàn)次數(shù)為奇數(shù)次的字符的個數(shù)
        int oddOccur = 0;
        
        for(Integer count : map.values()) {
            // 出現(xiàn)了奇數(shù)次
            if(count % 2 == 1) {
                oddOccur++;
            }
            
            if(oddOccur > 1) {
                return false;
            }
        }
        
        return true;
    }
}

引用:
Manacher's Algorithm 馬拉車算法
[Swift 算法] 馬拉車算法
Longest Palindromic Substring Part II
Manacher's ALGORITHM: O(n)時間求字符串的最長回文子串

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市家厌,隨后出現(xiàn)的幾起案子播玖,更是在濱河造成了極大的恐慌,老刑警劉巖饭于,帶你破解...
    沈念sama閱讀 211,042評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蜀踏,死亡現(xiàn)場離奇詭異维蒙,居然都是意外死亡,警方通過查閱死者的電腦和手機脓斩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評論 2 384
  • 文/潘曉璐 我一進店門木西,熙熙樓的掌柜王于貴愁眉苦臉地迎上來畴栖,“玉大人随静,你說我怎么就攤上這事÷鹧龋” “怎么了燎猛?”我有些...
    開封第一講書人閱讀 156,674評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長照皆。 經(jīng)常有香客問我重绷,道長,這世上最難降的妖魔是什么膜毁? 我笑而不...
    開封第一講書人閱讀 56,340評論 1 283
  • 正文 為了忘掉前任昭卓,我火速辦了婚禮,結(jié)果婚禮上瘟滨,老公的妹妹穿的比我還像新娘候醒。我一直安慰自己,他們只是感情好杂瘸,可當我...
    茶點故事閱讀 65,404評論 5 384
  • 文/花漫 我一把揭開白布倒淫。 她就那樣靜靜地躺著,像睡著了一般败玉。 火紅的嫁衣襯著肌膚如雪敌土。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,749評論 1 289
  • 那天运翼,我揣著相機與錄音返干,去河邊找鬼。 笑死血淌,一個胖子當著我的面吹牛矩欠,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播六剥,決...
    沈念sama閱讀 38,902評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼晚顷,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了疗疟?” 一聲冷哼從身側(cè)響起该默,我...
    開封第一講書人閱讀 37,662評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎策彤,沒想到半個月后栓袖,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體匣摘,經(jīng)...
    沈念sama閱讀 44,110評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年裹刮,在試婚紗的時候發(fā)現(xiàn)自己被綠了音榜。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,577評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡捧弃,死狀恐怖赠叼,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情违霞,我是刑警寧澤嘴办,帶...
    沈念sama閱讀 34,258評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站买鸽,受9級特大地震影響涧郊,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜眼五,卻給世界環(huán)境...
    茶點故事閱讀 39,848評論 3 312
  • 文/蒙蒙 一妆艘、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧看幼,春花似錦批旺、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至茅诱,卻和暖如春逗物,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背瑟俭。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評論 1 264
  • 我被黑心中介騙來泰國打工翎卓, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人摆寄。 一個月前我還...
    沈念sama閱讀 46,271評論 2 360
  • 正文 我出身青樓失暴,卻偏偏與公主長得像,于是被迫代替她去往敵國和親微饥。 傳聞我的和親對象是個殘疾皇子逗扒,可洞房花燭夜當晚...
    茶點故事閱讀 43,452評論 2 348

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