Java花式解決'分割回文串 ii'問(wèn)題詳解
前言
最學(xué)習(xí)動(dòng)態(tài)規(guī)劃思想的路上,遇見(jiàn)了‘分割回文串問(wèn)題',如臨大敵啊,題目聽(tīng)起來(lái)蠻簡(jiǎn)單,思考起來(lái)卻也沒(méi)那么容易,比解決問(wèn)題更頭疼的是如何將解決方法進(jìn)行優(yōu)化,使得時(shí)間空間復(fù)雜度盡量的小,經(jīng)過(guò)了反復(fù)的掙扎思考,終于總結(jié)出來(lái)了這一篇 分割回文串 ii 的文章,花式解決該問(wèn)題,總有一款適合你。
題目
給出一個(gè)字符串s,分割s使得分割出的每一個(gè)子串都是回文串
計(jì)算將字符串s分割成回文分割結(jié)果的最小切割數(shù)
例如: 給定字符串s=“aab”,
返回1,因?yàn)榛匚姆指罱Y(jié)果[“aa”,“b”]是切割一次生成的。
思路分析
首先,已知字符串s的長(zhǎng)度為len,想要求前l(fā)en個(gè)字符串被切割成回文串所需要的最小切割數(shù),就會(huì)很自然的想到求前i個(gè)字符形成的字符串切割成回文串需要的最小切割數(shù),即為狀態(tài)F(i)。
然后,會(huì)想到前i個(gè)字符形成的字符串分割變成回文串需要的最大切割數(shù)為i-1。例如字符串"aab",切2刀形成長(zhǎng)度為1的回文串"a",“a”,“b”。
再然后,關(guān)鍵在于求解最小切割數(shù)的過(guò)程,這里采取暴力求解,定義變量j,使之小于i,在我們已知狀態(tài)F(j)的情況下(即前j個(gè)字符形成的字符串的最小切割次數(shù)),如果[j+1,i]是回文串,那么再來(lái)上一刀就可以求出當(dāng)前用最少的切割次數(shù)。那么此時(shí)F(i)= min(F(i),F(xiàn)(j)+1),意思就是在上一次求得的前i個(gè)字符串的分割次數(shù)和這一次求得的次數(shù)進(jìn)行對(duì)比,取最小值。(注:這里的[j+1,i]指的是字符串第j+1個(gè)字符到第i個(gè)字符的意思,并非字符串下標(biāo)索引,寫(xiě)代碼時(shí),轉(zhuǎn)換成索引就應(yīng)該是求下標(biāo)為j-1的字符到下標(biāo)為i的字符形成的字符串是否為回文串)
緊接著,就該實(shí)現(xiàn)判斷是否為回文串的方法,簡(jiǎn)單的思想就是,為該方法提供字符串s,提供子串的起始下標(biāo)start與終點(diǎn)下標(biāo)end,start<end的條件下,使start向后走,end向前走,但凡對(duì)應(yīng)到字符串s中的字符不一樣,就說(shuō)明不是回文串,返回false,如果成功遍歷完了循環(huán),說(shuō)明是回文串,返回true。
最后,將F(len)的值返回。
動(dòng)歸四角度:
1.狀態(tài)定義F(i):字符串s的前i個(gè)字符的最小分割次數(shù);
2.狀態(tài)間的轉(zhuǎn)移方程定義:F(i)=min(F(i),F(xiàn)(j)+1),0 <= j < i,且F(j)為已知狀態(tài),當(dāng)[j+1,i]為回文串時(shí),執(zhí)行此狀態(tài)轉(zhuǎn)移方程
3.狀態(tài)的初始化:F(i) = i-1,注意F(0)為-1;
例如,當(dāng)字符串為"aa",因?yàn)閇1,2]為回文串,F(xiàn)(2)= min(F(2),F(xiàn)(0)+1)=min(1,0)= 0,得到正確答案
4.返回結(jié)果 :F(s.length());
案例說(shuō)明
為了方便理解,這里采取了更長(zhǎng)的字符串"aabaa",一步步帶你走過(guò)程。
初級(jí)代碼
import java.util.*; public class Solution { //判斷是否為回文串 public boolean func(String s,int start,int end) { while(start<end) { if(s.charAt(start)!=s.charAt(end)) { return false; } start++; end--; } return true; } public int minCut (String s) { int len = s.length();//字符串的長(zhǎng)度 if(len == 0) return 0;//當(dāng)長(zhǎng)度為0時(shí)直接返回0 int[] count = new int[len + 1];//用于記錄狀態(tài) //狀態(tài)初始化 for(int i = 0;i <= len;i ++) { count[i] = i-1; } for(int i = 1;i <= len;i++) { for(int j = 0;j <= i-1;j++) { if(func(s,j,i-1)) { count[i] = Math.min(count[i],count[j]+1);//狀態(tài)轉(zhuǎn)移方程 } } } return count[len];//返回結(jié)果 } }
在整個(gè)進(jìn)行狀態(tài)計(jì)算的過(guò)程中,兩層for循環(huán)時(shí)間復(fù)雜度為O(N2),判斷是否為回文串的方法時(shí)間復(fù)雜度為O(N),因此總的來(lái)說(shuō),總的時(shí)間復(fù)雜度為O(N3)
代碼升級(jí)
可以看出來(lái),用上面的代碼時(shí)間復(fù)雜度還是比較高的,因此代碼還需升級(jí)才是
1.回文串動(dòng)歸
首先,關(guān)于回文串的判斷方法,每次判斷是否要進(jìn)行狀態(tài)轉(zhuǎn)移方程時(shí)都要調(diào)用回文串方法,這真的有必要嗎,或許也可以使用動(dòng)態(tài)規(guī)劃的思想將每種字符子串是否為回文串的狀態(tài)記錄下來(lái)。
狀態(tài)四角度:
1.狀態(tài)定義F(i,j):字符區(qū)間[i,j]是否為回文串
2.狀態(tài)間的轉(zhuǎn)移方程定義F(i,j):
如果i == j,表示單字符,F(xiàn)(i,j) = true;
如果j == i+1,表明倆字符是緊挨著的,如果在總字符串s中對(duì)應(yīng)的字符相同,F(xiàn)(i,j)= true,反之F(i,j) = false;
其他的情況中,F(xiàn)(i,j) = (s.charAt(i) == s.charAt(j)) && F(i+1,j-1);
該轉(zhuǎn)移方程的意思為字符首尾字符相同,且去掉字符區(qū)間的首位字符后的字符區(qū)間的狀態(tài)F(i+1,j-1)仍然為回文串才證明[i,j]字符串區(qū)間為回文串即F(i,j)= true
3.狀態(tài)的初始化:F(i,j) = false
4.返回結(jié)果狀態(tài)二維布爾類(lèi)型數(shù)組
注:由于在狀態(tài)轉(zhuǎn)移的過(guò)程中,求F(i,j)會(huì)只用到之前已經(jīng)計(jì)算過(guò)的狀態(tài)F(i+1,j-1),這就意味著i需要從后向前遍歷,使用的是已經(jīng)更新過(guò)結(jié)果的值
import java.util.*; public class Solution { //判斷是否為回文串 public boolean[][] func2 (String s) { int len = s.length();//字符串的長(zhǎng)度 boolean[][] ret = new boolean[len][len]; //記錄狀態(tài)的二維數(shù)組,默認(rèn)值為false //由于i<=j<len,所以ret數(shù)組實(shí)際只更新了一半 for(int i = len;i >= 0;i--) { for(int j = i;j<len;j++) { if(i == j) { ret[i][j] = true; //單字符比為回文串 }else if(j == i+1) { if(s.charAt(i) == s.charAt(j)) { ret[i][j] = true; //相鄰字符相同為回文串 }else{ ret[i][j] = false;//相鄰字符不同就不是回文串 } }else{ ret[i][j] = (s.charAt(i) == s.charAt(j)) && ret[i+1][j-1]; //其余轉(zhuǎn)移情況 } } } return ret;//返回結(jié)果 } public int minCut (String s) { int len = s.length(); if(len == 0) return 0; int[] count = new int[len + 1]; //狀態(tài)初始化 for(int i = 0;i <= len;i ++) { count[i] = i-1; } boolean[][] ret = func2(s);//調(diào)用判斷回文串方法,獲得所有字符子串的是否為回文串的情況 for(int i = 1;i <= len;i++) { for(int j = 0;j <= i-1;j++) { //直接在ret數(shù)組中找結(jié)果,避免反復(fù)調(diào)用回文串判斷方法 if(ret[j][i-1]) { count[i] = Math.min(count[i],count[j]+1);//狀態(tài)轉(zhuǎn)移方程 } } } return count[len];//返回結(jié)果 } }
在該方法中,判斷回文串的方法時(shí)間復(fù)雜度為O(N2),但因?yàn)樵谥鞣椒ㄖ兄徽{(diào)用了一次,且回文串判斷方法中只更新了一般的值,因此總的時(shí)間復(fù)雜度為O(N2)~O(2*N2)
2.綜合動(dòng)歸
可以看的出來(lái)上面的代碼還是比較長(zhǎng)的,回文串判斷方法用到了兩層循環(huán),主方法也用到了兩層循環(huán),這不也是優(yōu)化的方向蠻,或許可以把它們放在同一個(gè)兩層循環(huán)中。
注:由于回文串判斷方法中的i是一定要從后向前遍歷的,因此主函數(shù)的初識(shí)值就需要調(diào)整為count[i] = len - i - 1,返回的結(jié)果為F(0)
import java.util.*; public class Solution { public int minCut(String s) { int len = s.length();//字符串的長(zhǎng)度 if(len == 0) return 0; int []count = new int[len+1]; //存放最小分割次數(shù)狀態(tài)的數(shù)組 boolean [][]p = new boolean[len][len];//存放[i,j]字符區(qū)間是否為回文串的二維數(shù)組 for(int i = 0; i <= len; i++) count[i] = len - i - 1;//狀態(tài)初始化 for(int i = len-1;i >= 0;i--){ for(int j = i;j < len;j++){ //j-i<2 條件成立且第一個(gè)條件成立包含著單個(gè)字符串和相鄰字符串的情況 //p[i+1][j-1] 為 ture 且第一個(gè)條件成立則代表著其他的回文串狀態(tài)轉(zhuǎn)移類(lèi)型 //以上情況有一項(xiàng)成立則F(i,j)為 ture if(s.charAt(i) == s.charAt(j) && (j-i<2||p[i+1][j-1])){ p[i][j] = true; count[i] = Math.min(count[i],count[j+1]+1);//狀態(tài)轉(zhuǎn)移方程 } } } return count[0];//返回結(jié)果 } }
通過(guò)這樣的方法,直接將時(shí)間復(fù)雜度降到了O(N2)
3.奇思妙想
上面幾種方法,需要將回文串的判斷狀態(tài)都記錄下來(lái),且判斷回文串的方法都是從子字符串的兩頭向中間進(jìn)行判斷,或許有一種方法,可以直接不用記錄下來(lái)每種子字符串的是否為回文串的狀態(tài),并且從中間向兩頭進(jìn)行判斷回文串。
可以設(shè)置兩個(gè)變量i和j,[i,j]且j==i代表著下標(biāo)為i的單個(gè)字符,必定是回文串,F(xiàn)(j+1)= min(F(j+1),F(xiàn)(i)+1),以此為中心,i--,j++,如果區(qū)間兩頭的字符相同,說(shuō)明[i-1,j+1]的區(qū)間字符串為回文串,在不超出原字符串s的總區(qū)間[0,len-1]的循環(huán)情況下,重復(fù)上面的操作,直到循環(huán)條件不成立
回文串可能是奇數(shù)個(gè)字符,也可能是偶數(shù)個(gè)字符,上面的情況是奇數(shù)個(gè)字符的情況,換成偶數(shù)個(gè)字符的情況只需要判斷[i,i+1]是否為回文串,如果是,就參考上面的方式,以此為中心向兩頭展開(kāi),求以[i,i+1]為中心最長(zhǎng)的回文串,從而求出每個(gè)狀態(tài)的最小分割數(shù)。
案例說(shuō)明:
import java.util.*;
public class Solution {
public int minCut(String s) {
int len = s.length();//字符串的長(zhǎng)度
if(len == 0) return 0;
int[] count = new int[len + 1];
for(int i = 0; i <= len; i++) count[i] = i - 1;//狀態(tài)初始化
for(int i = 0; i < len; i++) {
func3(s, i, i, count);//奇數(shù)個(gè)字符的回文串
func3(s, i, i + 1, count);//偶數(shù)個(gè)字符的回文串
}
return count[len];//返回結(jié)果
}
private void func3(String s, int i, int j, int[] count) {
//不超過(guò)字符串s的區(qū)間范圍且下標(biāo)i的字符和下標(biāo)j的字符相等的條件下向兩頭擴(kuò)展,得到最長(zhǎng)的回文串,以此來(lái)求出狀態(tài)
while(i >= 0 && j < s.length() && s.charAt(i) == s.charAt(j)) {
count[j + 1] = Math.min(count[j + 1], count[i] + 1); //狀態(tài)轉(zhuǎn)移
--i;//左區(qū)間擴(kuò)展一格
++j;//右區(qū)間擴(kuò)展一格
}
return;
}
}
到此這篇關(guān)于Java花式解決'分割回文串 ii'問(wèn)題詳解的文章就介紹到這了,更多相關(guān)Java解決分割回文串 ii內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Maven插件構(gòu)建Docker鏡像的實(shí)現(xiàn)步驟
這篇文章主要介紹了Maven插件構(gòu)建Docker鏡像的實(shí)現(xiàn)步驟,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-10-10spring boot設(shè)置過(guò)濾器、監(jiān)聽(tīng)器及攔截器的方法
這篇文章主要給大家介紹了關(guān)于spring boot設(shè)置過(guò)濾器、監(jiān)聽(tīng)器及攔截器的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用spring boot具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04Elasticsearch開(kāi)發(fā)AtomicArray使用示例探究
這篇文章主要為大家介紹了Elasticsearch AtomicArray使用示例探究,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08Java紅黑樹(shù)的數(shù)據(jù)結(jié)構(gòu)與算法解析
紅黑樹(shù)問(wèn)題是各大計(jì)算機(jī)考研命題以及面試算法題目中的熱門(mén),接下來(lái)我們?yōu)榇蠹覉D解紅黑樹(shù)的數(shù)據(jù)結(jié)構(gòu)與算法解析,需要的朋友可以參考下2021-08-08