每天一道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]內最長的回文子序列長度,那么有兩種情況
典型的動態規劃,直接上代碼(這里采用非遞歸方式)
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中從頭開始最長的回文子串,然后將后面的多余部分添加到頭部,只有這樣才有可能是最小的。
所以要求就轉換成了,尋找源字符串s最長的回文子串,要求這個子串從源s的起始位置開始。
這就很麻煩了,既不能使用上面的方法從每個字符開始向兩邊擴展,也不能直接從頭開始判斷每個子串是否是回文串(太麻煩,復雜度過高),因為如果用這種方法,就需要
這樣來來回回遍歷了好幾遍,況且如果源s重復度過高(每一個都和s[0]相等),那么遍歷的次數就更多了,顯然不合適。
解決這一問題用到了KMP算法(第一次見KMP可以求回文串)
首先復習一下KMP吧,先別著急想怎么用KMP解決這道題。
KMP,字符串處理算法,典型的問題是在一個源字符串中判斷是否包含某個子串,相比于普通的一個一個比較,KMP可以實現多字符跳過,不需要比較沒有的字符,大大提高的運行效率。
KMP算法的核心在于根據源字符串構建前綴數組
前綴數組中的元素含義是上一個和當前字符相等的位置 + 1,同時當前字符的前幾個字符也都有這些位置,并且位置不間斷一直到1,這個位置+1同時也表示第幾個字符(從1開始),舉個例子
構造的代碼就比較容易寫了,如下
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-----最长回文子串/序列,从头开始的最长回文子串长度的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 每天一道LeetCode-----两个有
- 下一篇: 每天一道LeetCode-----只可能