日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

每天一道LeetCode-----最长回文子串/序列,从头开始的最长回文子串长度

發布時間:2024/4/19 编程问答 30 豆豆
生活随笔 收集整理的這篇文章主要介紹了 每天一道LeetCode-----最长回文子串/序列,从头开始的最长回文子串长度 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

Longest Palindromic Substring

原題鏈接 Longest Palindromic Substring

意思是找到最長的回文子串,注意子串和子序列的區別


蠻力法就將每個可能的子串都找出來判斷是否是回文子串,找最大的那一個,速度上慢的嚇人,光找子串的速度就到O(n2)了,所以此處需要找到一個簡單的方法找到回文子串。
既然回文子串是從中心開始向兩邊延伸時,左右兩邊相同,那么就可以從每個字符開始都向兩邊延伸,看能找到的最長回文子串是多長就行了。

這就出現了一個問題,回文子串的中心是什么地方,對于abcba來說,中心是c。那么對于abccba呢,中心是cc中間的位置,這個位置沒有明確的下標。二者的區別在于一個數量上一個是奇數,一個是偶數。為了統一,這里有一種技巧,就是將源字符串s統一轉化成奇數個數,方法是虛擬的加上一些字符,注意是虛擬加,實際上并不改變源s,假設s為babad,那么加上虛擬字符的s是#b#a#b#a#d#,這樣,不管什么字符串全都是奇數個數了,總數為2 * s.size() + 1,而這里還有一個巧妙的地方,就是加上虛擬字符后的字符的索引除以2正好是在源s中沒有加上虛擬字符的索引,這里把加上虛擬字符后的s成為extend_s

# b # a # b # a # d # extend_s 0 1 2 3 4 5 6 7 8 9 10 extend_s的下標索引 0 0 1 1 2 2 3 3 4 4 5 除以2后的結果b a b a d s 0 1 2 3 4 s的下標索引

所以在程序中遍歷extend_s時只需要除以2,就可得得到s中對應位置的字符。

擴展s是為了解決奇數偶數的問題,這個問題存在在什么地方呢。比方說要找的結果是abccba,那么在切割的時候是從cc中間切分的,而對于abcba而言,切分是在c字符上切分的,這個還好說,切分的位置有正常的索引下標,可是abccba的切分位置是中間,下標是x.5?,不存在的。所以擴展后的s剛好解決的就是這個難題

a b c c b a 源字符串s 0 1 2 3 4 5 下標索引|切分位置# a # b # c # c # b # a # 擴展字符串extend_s 0 1 2 3 4 5 6 7 8 9 10 11 12 下標索引|切分位置 -----------------------------------------------------------------a b c b a 源字符串s 0 1 2 3 4 下標索引|切分位置# a # b # c # b # a # 擴展字符串extend_s 0 1 2 3 4 5 6 7 8 9 10 下標索引|切分位置

通過擴展,不管是奇數個數的字符串還是偶數個數的字符串,都有一個明確的下標表示切分位置,這個位置正是回文子串的中心。程序就簡單多了,從extend_s的每一個字符開始,向兩邊擴展,相等則繼續擴展,不相等則停止擴展。需要注意的是,程序中遍歷的仍然是源字符串s,只是在下標上做了點手腳,所以區分#還是字母的方法就看下標是奇數還是偶數,#的下標永遠是偶數。代碼如下

class Solution { public:string longestPalindrome(string s) {if(s.size() <= 1)return s;string ans("");for(int i = 0; i < 2 * s.size() + 1; ++i){ /* 以extend_s[i]為中心,向兩邊擴展,這個extend_s[i]既有可能是#,也有可能是字母 */find_palindrome(i - 1, i + 1, s, ans);}return ans;} private:void find_palindrome(int lhs_idx, int rhs_idx, std::string& s, std::string& ans){/* 判斷邊界 */while(lhs_idx >= 0 && rhs_idx < 2 * s.size() + 1){/* #的下標永遠是偶數,所以判斷是否是奇數,然后比較s中的兩個字符 */if(lhs_idx % 2 != 0 && rhs_idx % 2 != 0 && s[lhs_idx / 2] != s[rhs_idx / 2]){break;}--lhs_idx;++rhs_idx;}/* * 這是為了解決邊界0的問題,因為如果找到左邊界(0)時仍然相等,那么lhs_idx會是-1* 因為找到的回文子串應該是s[lhs_idx / 2 + 1, rhs_idx / 2 - 1]* 由于lhs_idx是-1,-1/2==0,正常結果應該是s[0, rhs_idx / 2 - 1],* 會導致lhs_idx / 2 + 1 == 1,不為0,使結果長度變小* 下面會說另一種解決辦法*/if(lhs_idx == -1)lhs_idx = -2;if(static_cast<int>(ans.size()) < rhs_idx / 2 - lhs_idx / 2 - 1){ans = s.substr(lhs_idx / 2 + 1, rhs_idx / 2 - lhs_idx / 2 - 1);}} };

因為左邊界會有使結果長度變小的風險,上述程序中手動將lhs_idx從-1改為-2使lhs_idx/2為-1不為0。另一種解決辦法是在extend_s頭部再添加一個字符@代表頭部,也可以解決上述問題。



Palindromic Substrings

原題鏈接Palindromic Substrings

和上面的題差不多,找所有可能的回文子串的數量,即使回文子串一樣,但是在源s中的起止位置不同,也算作不同的回文子串,方法和上面一樣,也需要添加虛擬字符,記得數個數。

class Solution { public:int countSubstrings(string s) {int cnt = 0;for(int i = 0; i < 2 * s.size() + 1; ++i){/* 如果不是虛擬字符,+1 */if(i % 2 != 0){++cnt;}find_palindrome(i - 1, i + 1, s, cnt);}return cnt;}private:void find_palindrome(int lhs_idx, int rhs_idx, std::string& s, int& cnt){while(lhs_idx >=0 && rhs_idx < 2 * s.size() + 1){if(lhs_idx % 2 != 0 && rhs_idx % 2 != 0){if(s[lhs_idx / 2] == s[rhs_idx / 2])++cnt;elsebreak;}--lhs_idx;++rhs_idx;}} };

Longest Palindromic Subsequence

原題鏈接Longest Palindromic Subsequence

和上面不同的是,這個是求最長的回文子序列,子序列是將源字符串中的任意多個字符刪除后生成的新字符串,結果串中挨著的兩個字符在源字符串中不一定是挨著的,因為二者中間可能有被刪掉的字符。


這個就不能用上述方法解決了,因為如果s[lhs_idx / 2] != s[rhs_idx / 2],那么s[lhs_idx / 2]還有可能和lhs_idx / 2前面的某個字符相等,構成子序列,所以上述方法行不通。
涉及到子序列的題可以這樣想,假設len[i, j]表示范圍[i, j]內最長的回文子序列長度,那么有兩種情況

  • s[i] == s[j],那么len[i, j]一定等于2 + len[i + 1, j - 1]
  • s[i] != s[j],那么len[i, j] = max(len[i + 1, j], len[i, j - 1])
  • 典型的動態規劃,直接上代碼(這里采用非遞歸方式)

    class Solution { public:int longestPalindromeSubseq(string s) {if(s.size() <= 1)return s.size();std::vector<std::vector<int> > dp(s.size(), std::vector<int>(s.size(), 0));for(int i = 0; i < s.size(); ++i)dp[i][i] = 1;/* * 遞歸的動態規劃是從最大范圍遞歸到單個字符然后逐層返回* 非遞歸就是實現逐層返回的方法,從單個字符擴展到最大范圍*/for(int i = s.size() - 1; i >= 0; --i){for(int j = i + 1; j < s.size(); ++j){if(s[i] == s[j]){dp[i][j] = 2;if(i + 1 <= j - 1)dp[i][j] += dp[i + 1][j - 1];}else{dp[i][j] = std::max(dp[i][j - 1], dp[i + 1][j]);}}}return dp[0][s.size() - 1];} };

    設計到子序列的問題,或者可以轉換成上述兩種可能的問題都是動態規劃的典型問題,動態規劃在算法中使用頻率很高,常見問題是IO背包,很重要!!!!



    Shortest Palindrome

    原題鏈接Shortest Palindrome

    意思是給定一個源字符串s,在s的頭部添加盡可能少的字符,使s成為一個回文字符串。
    既然在頭部添加,那么s[0]一定是擴展后的中心嗎,不一定,題中第一個例子就是反例。
    那么在頭部前面添加的一定就是源字符串s較后面的幾個字符,這樣才能達到最少,也就是說需要找到源s中從頭開始最長的回文子串,然后將后面的多余部分添加到頭部,只有這樣才有可能是最小的。

    a a c e c a a a|分割,前面是s最長的回文子串,且從頭開始 只需要把多出的a也添加到頭部即可a b c c b a d a f g h|分割,理由同上

    所以要求就轉換成了,尋找源字符串s最長的回文子串,要求這個子串從源s的起始位置開始。
    這就很麻煩了,既不能使用上面的方法從每個字符開始向兩邊擴展,也不能直接從頭開始判斷每個子串是否是回文串(太麻煩,復雜度過高),因為如果用這種方法,就需要

  • 固定頭部,從尾部開始找第一個和s[0]相等的位置back
  • 判斷s[0, back]是否是個回文串,如果是,結束,如果不是,接著找第二個和s[0]相等的位置
  • 這樣來來回回遍歷了好幾遍,況且如果源s重復度過高(每一個都和s[0]相等),那么遍歷的次數就更多了,顯然不合適。

    解決這一問題用到了KMP算法(第一次見KMP可以求回文串)
    首先復習一下KMP吧,先別著急想怎么用KMP解決這道題。
    KMP,字符串處理算法,典型的問題是在一個源字符串中判斷是否包含某個子串,相比于普通的一個一個比較,KMP可以實現多字符跳過,不需要比較沒有的字符,大大提高的運行效率。
    KMP算法的核心在于根據源字符串構建前綴數組
    前綴數組中的元素含義是上一個和當前字符相等的位置 + 1,同時當前字符的前幾個字符也都有這些位置,并且位置不間斷一直到1,這個位置+1同時也表示第幾個字符(從1開始),舉個例子

    0 1 2 3 4 5 6 7 8 9 10 下標索引 a b c a b c a a b a c 源字符串s 0 0 0 1 2 3 4 1 2 1 0 前綴數組/* 解釋: s[0] == a,前面沒有字符,初始為0 s[1] == b, 第一個字符a和b不相等,為0 s[2] == c,同上 s[3] == a,和第一個字符a相等,為0 + 1 = 1,表示s[3]前面和自己相等的字符是第1個字符 s[4] == b,和前面s[3]記錄的位置1(第1個字符)后面的位置2(第2個字符)b相等,為2 s[5] == c,同上,和第3個字符c相等,為3 s[6] == a,同上,和第4個字符a相等,為4 s[7] == a,和前面s[6]記錄的位置4(第4個字符)后面的位置5(第5個字符)b不相等找第4個字符a記錄的位置1(第1個字符)后面的位置2(第2個字符)b不相等找第2個字符b記錄的位置0(第0個字符)后面的位置1(第1個字符)a相等,為1 s[8] == b,和前面s[7]記錄的位置1(第1個字符)后面的位置2(第2個字符)b相等,為2 s[9] == a,和前面s[8]記錄的位置2(第2個字符)后面的位置3(第3個字符)c不相等,找第2個字符b記錄的位置0(第0個字符)后面的位置1(第1個字符)a相等,為1 s[10]== c,和前面s[9]記錄的位置1(第1個字符)后面的位置2(第2個字符)b不相等,找第2個字符b記錄的位置0(第0個字符)后面的位置1(第1個字符)a不相等,找第1個字符a記錄的位置0(第0個字符)后面的位置1(第1個字符)a不不相等,....有個規定,到達0時如果和第1個字符仍然不相等,記為0 */

    構造的代碼就比較容易寫了,如下

    void treat_prefix(std::string& extend_s, int *prefix) {int l = 0;prefix[0] = 0;for(int i = 1; i < extend_s.size(); ++i){/* 其實每次l都等于prefix[i - 1],只是在跳的過程中一直prefix[prefix[i - 1] - 1] *//* 如果一直不相等,一直往前跳,直到為0 */while(l > 0 && extend_s[i] != extend_s[l])l = prefix[l - 1];if(extend_s[i] == extend_s[l])++l;prefix[i] = l;} }bool kmp(std::string& source, std::string target) {int *prefix = new int[target.size()];treat_prefix(target, prefix);int l = 0;for(int i = 0; i < source.size(); ++i){/** 跟source[i]比較,如果你不和我l位置的字符相等* 那我跳到前面,看看咱倆相不相等* 因為kmp算法的前綴數組保證,跳到前面后,target[0, l - 1]這部分仍然是和source[i - l - 1, i - 1]相等的,不需要重復比較*/while(l > 0 && source[i] != target[l])l = prefix[l - 1];if(source[i] == target[l])++l;if(l == target.size())return true;;}return false; }

    其實上面的從0開始的最長回文子串只用到了prefix數組,方法是先將源字符串s擴展,擴展方式如下

    std::string reverse_s(s.rbegin(), s.rend()); //求s的翻轉字符串 s = s + "#" + reverse_s;

    假設s為catacb,那么擴展后的s為

    catacb#bcatac

    對此調用生成前綴數組的函數

    0 1 2 3 4 5 6 7 8 9 10 11 12 下標索引 c a t a c b # b c a t a c 擴展后的s 0 0 0 0 1 0 0 0 1 2 3 4 5 prefix數組假設源字符串(未擴展)從0開始的最長回文子串為從s[i]s[j], 那么子串s[i]s[j]和翻轉后從s[j]s[i]肯定相等,因為回文字符串的特性,翻轉后仍然相等 那么假設擴展后這段回文子串是s[i]s[j](#前面)和s[p]s[q](#后面),那么這兩個子串一定相同 所以通過前綴函數的構造后面的字符串的prefix中的位置一定分別對應s[i]s[j],那么最后一個字符的prefix元素就是最長回文子串的長度

    根據以上推論,代碼就比較容易了

    class Solution { public:string shortestPalindrome(string s) {std::string reverse_s(s.rbegin(), s.rend());std::string extend_s = s + "#" + reverse_s;int *prefix = new int[extend_s.size()];treat_prefix(extend_s, prefix);/* 獲取最長回文子串的長度,然后把后面多余的部分也添加到前面 */int max_palindrome_len = prefix[extend_s.size() - 1];std::string ans = s;while(max_palindrome_len < s.size()){ans = s[max_palindrome_len++] + ans;}delete []prefix;return ans;}private:void treat_prefix(std::string& extend_s, int *prefix){int l = 0;prefix[0] = 0;for(int i = 1; i < extend_s.size(); ++i){while(l > 0 && extend_s[i] != extend_s[l])l = prefix[l - 1];if(extend_s[i] == extend_s[l])++l;prefix[i] = l;}} };

    這道題主要就是利用KMP算法,算是個積累知識,處理回文字符串時多一種考慮的可能

    與50位技術專家面對面20年技術見證,附贈技術全景圖

    總結

    以上是生活随笔為你收集整理的每天一道LeetCode-----最长回文子串/序列,从头开始的最长回文子串长度的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。