小明思考

          Just a software engineer
          posts - 124, comments - 36, trackbacks - 0, articles - 0
            BlogJava :: 首頁 :: 新隨筆 :: 聯系 :: 聚合  :: 管理

          回文字符串的切割問題

          Posted on 2013-04-11 11:24 小明 閱讀(4150) 評論(3)  編輯  收藏 所屬分類: 數據結構和算法

          問題給定一個字符串s,分割s使得每個子串都是回文的(即倒過來和原字符串是一樣的,如aba)
          求最少的分割次數。

          比如對于“aaba”返回1.(分割方法:["a","aba"])

          public class Solution {
              public int minCut(String s) {
                 //write your code here
              }
          }

          解法一:【遞歸】
          對于一個字符串,先找到第一段回文子字符串,然后再求余下的子串的最少分割數,然后再加1,就可以得到一種分割結果,遍歷所以這樣的解法,就可以求出最小的分割次數。
          public class MinCut {
              private boolean isPalindrome(String s){
                  int len = s.length();
                  if(len>1){
                      for(int i=0;i<len/2;++i){
                          if(s.charAt(i)!=s.charAt(len-i-1)){
                              return false;
                          }
                      }
                  }
                  return true;
              }
              public int minCut(String s){
                  if(isPalindrome(s)){
                      return 0;
                  }
                  int min = 99999;

                  int len =s.length();
                  for(int i=1;i<len;++i){
                      String ss = s.substring(0,i);
                      if(isPalindrome(ss)){
                          int result = minCut(s.substring(i))+1;
                          if(result<min){
                              min = result;
                          }
                          if(min<=1){
                              break;
                          }
                      }
                  }
                  return min;
              }
          public static void main(String[] args) throws IOException {
                  long tick = System.currentTimeMillis();
                  MinCut m = new MinCut();
                  int result = m.minCut("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
                  System.out.println("result:"+result+",costs:"+(System.currentTimeMillis()-tick)+" ms");
              }
          }

          這樣算法雖然可行,但是效率很低,對于小字符串還可以工作,對于上面的長度為1400+的字符串就慢的驚人。

          因為有大量的重復計算。改進一下,把已經計算好的子串的最少分割次數的結果記錄下來,應該能快很多。

          Map<String,Integer> memos = new HashMap<String,Integer>();
              
              public int cut(String s){
                  if(isPalindrome(s)){
                      return 0;
                  }
                  if(memos.containsKey(s)){
                      return memos.get(s).intValue();
                  }
                  int min = 99999;

                  int len =s.length();
                  for(int i=1;i<len;++i){
                      String ss = s.substring(0,i);
                      if(isPalindrome(ss)){
                          int result = cut(s.substring(i))+1;
                          if(result<min){
                              min = result;
                          }
                          if(min<=1){
                              break;
                          }
                      }
                  }
                  memos.put(s, min);
                  return min;
              }
              
              public int minCut(String s) {
                  memos.clear();
                  return cut(s);
              }

          果然,優化后的結果要快很多,在我的機器上大概1200ms就可以求出結果了。

          解法二:【動態規劃】
          其實這個問題是一個典型的動態規劃問題。問題的最優解可以歸結為兩個子問題的最優解,再加上1就可以了,使用動態規劃記錄所有的中間狀態就可以降低重復計算。
          狀態轉移公式如下:

          minCut(s,i,j) = 0 if(s[i..j] 是回文的)
          minCut(s,i,j) = min(minCut(s,i,k)+minCut(s,k+1,j))(i<k<j)

          為了減少回文的判斷,使用了兩個數組,M用來記錄最優分割次數,P用來保存子串的回文判斷結果。
          代碼如下:
          public int minCut(String s){
                  int totalLength = s.length();
                  char[] ch=s.toCharArray();
                  int [][] M = new int[totalLength][totalLength];
                  boolean [][] P = new boolean[totalLength][totalLength];
                  for(int len=1;len<=totalLength;++len){
                      for(int i=0;i<totalLength-len+1;++i){
                          int j = i+len-1;
                          if(len==1){
                              P[i][j]=true;
                          }
                          else if(len==2){
                              P[i][j]=(ch[i]==ch[j]);
                          }
                          else{
                              P[i][j]=(ch[i]==ch[j]) && P[i+1][j-1];
                          }
                          if(P[i][j]){
                              M[i][j] = 0;
                          }
                          else{
                              int min = 99999;
                              for(int k=i;k<j;++k){
                                  int t = M[i][k]+M[k+1][j]+1;
                                  if(min>t){
                                      min = t;
                                  }
                              }
                              M[i][j] = min;
                          }
                      }
                  }
                  return M[0][totalLength-1];
              }

          這個算法的復雜度是O(n3)的復雜度,使用了三層循環。對于上面的字符串大概需要花4000ms左右的時間。

          有沒有辦法把復雜度降為O(n2)呢?對于長度為L的字符串的最少切割次數等于L-1的切割次數+1或者如果最后一個字符能夠跟前面的子字符串構成回文的話,就等于去除該子串的剩余部分的切割次數+1。
          有些拗口,還是看狀態轉移公式吧:

          minCut(j)= min(
          minCut(j-i-1)+1: if s(i,j) is palindrom
          where 0<=i<j ,
          minCut(j-1)+1)

          同時優化一下回文的判斷,減少函數調用。

          private boolean isPalindrome(char[] ch, int i,int j){
                  while(i<j){
                      if(ch[i]!=ch[j]){
                          return false;
                      }
                      ++i;
                      --j;
                  }
                  return true;
              }
          public int minCut(String s){
                  int totalLength = s.length();
                  int[] M = new int[totalLength];
                  char[] ch = s.toCharArray();
                  M[0]=0;
                  for(int i=1;i<totalLength;++i){
                      int min = 99999;
                      for(int j=0;j<=i;++j){
                          int cut = 99999;
                          if(isPalindrome(ch,j,i)){
                              if(j>0){
                                  cut = M[j-1]+1;
                              }
                              else{
                                  cut = 0;
                              }
                              if(min>cut){
                                  min = cut;
                              }
                          }
                      }
                      
                      M[i]=min;
                  }
                  return M[totalLength-1];
              }

          優化后的結果,大概只需要200ms左右了。

          評論

          # re: 回文字符串的切割問題[未登錄]  回復  更多評論   

          2013-04-17 12:39 by Harry
          object Palindrome {
          def isPelindrome(l:String):Boolean = {
          val fhalf = l.take(l.length/2)
          val shalf = l.takeRight(l.length/2).reverse
          if (fhalf==shalf) true else false
          }
          def minCut(s:String,partition:Int):Int = {
          val ss = s.take(partition)
          val ts = s.takeRight(s.length - partition)
          if (isPelindrome(ss)&&isPelindrome(ts)) 1 else 0
          }
          def main(args: Array[String]): Unit = {
          val stime = System.currentTimeMillis()
          val t = {for (i <- 1 to 14000) yield "a"}.foldLeft(""){case (c,s)=> s+c}
          val r = for (i <- 1 to t.size) yield minCut(t,i)
          println(r.sum)
          println(System.currentTimeMillis() - stime)
          }

          }

          result: 14,000 "a", takes 2669 ms

          # re: 回文字符串的切割問題  回復  更多評論   

          2013-10-13 22:07 by selldogs
          第三個的復雜度也是 O(N^3) , 你每次判斷是否是回文 不是也有一個O(N)的循環么

          # re: 回文字符串的切割問題  回復  更多評論   

          2014-05-22 14:12 by 2dog
          @selldogs
          同意
          這算法本身就是O(n^3)的
          主站蜘蛛池模板: 县级市| 胶州市| 临桂县| 许昌市| 翁牛特旗| 凤庆县| 辽阳县| 嫩江县| 民勤县| 潼南县| 荣成市| 松桃| 鄂伦春自治旗| 从江县| 北川| 营口市| 金溪县| 正定县| 永康市| 沭阳县| 资溪县| 招远市| 佛冈县| 万源市| 安塞县| 富川| 阳朔县| 肥东县| 崇文区| 全椒县| 万盛区| 阿拉尔市| 丽江市| 浦东新区| 塘沽区| 临邑县| 玉龙| 板桥市| 义乌市| 吴川市| 镇原县|