[LeetCode 132] - 回文分割II(Palindrome Partitioning II)


前言

在軟件開發行業中實際工程做得久了,大多數人會發現很少有機會接觸到各種算法。正如Reddit上有人評論到,當初進公司的時候通過了n輪算法面試,實際工作卻很可能是不斷的解“null pointer exception”的bug。但是算法作為軟件開發的基礎的重要性確是不容置疑的,由此我最近突然想要練習練習算法題,補充一下工作中接觸不到的知識。在探索過程中發現了LeetCode這個網站,其中Online Judge部分有不少不錯的練習題,遂打算在博客中分享解題經驗。我google了一下目前分享LeetCode的博客,大多全篇代碼或比較簡單的解題報告。對於經過ACM訓練的人可能看到要點就能領悟了,但是對於我這種非ACM人士來說經常看得一頭霧水。所以我希望以一個程序員而不是ACM選手的角度分享解題過程,而不是ACM高手那樣的精煉提示:“用DP就能搞定”,“是NP問題”。最后,所有的問題的解決方案將會用C++代碼實現。

問題

給定一個字符串s,切割s使該切割結果中每一個子串都是一個回文。

返回需要切割次數最少的回文集。

例如,如果有s = "aab",

返回1,因為分割集["aa","b"]可以由s切割一次產生

初始思路

不管用什么方法,一個判斷字符串是不是回文的函數肯定是少不了的,這個比較容易實現。我們可以同時從字符串的頭部和尾部向中間移動,只要字符有不等的情況發生,那么改字符串肯定不是回文。代碼如下:

bool IsPalindrome(const std::string& s, size_t start, size_t end)
{
    bool result = true;
    
    while(start < end)
    {
        if(s[start] != s[end])
        {
            result = false;
            break;
        }
        
        ++start;
        --end;
    }
    
    return result;
}

要找出切割次數最少的回文集,那么我們可以找出所有可能的回文集,然后找出切割次數最少的那個。要怎么找出所有的回文集呢?讓我們用較短的aab作為例子人力暴力拆解看看:

a,a,b

aa,b

我們從aab的起點開始,先選長度為1的子串,發現它是回文,這樣問題分解為求a和[ab的所有回文集合]的組合。然后選取長度為2的子串,也是回文,問題分解為aa和[b的所有回文集合]的組合。最后選取長度問3的子串,發現它不是回文。

用代碼模擬一下,大概是這樣:

void FindMinPartition(const std::string& s, size_t start)
{
    size_t pos = start;
            
    while(pos < s.size())
    {
        if(IsPalindrome(s, start, pos))
        {
            FindMinPartition(s, pos + 1);
        }
        ++pos;
    }
}

可以看到函數里面出現了遞歸,既然是遞歸就要有遞歸結束條件。我們可以看看前面模擬的aa和[b的所有回文集合]的情況,判斷b的所有回文集時發現b也是回文,但是我們不需要再往后找了,因為再往后就超出了字符串的范圍,由此我們可以得到遞歸的結束條件應該是start >= s.size()(別忘了下標是從0開始的,下標為s.size()的時候已經越界了)。

現在怎么遞歸清楚了,但是切割次數還沒統計呢。不難看出每判斷出一個回文,就是一次切割。方便起見,我們可以用一個獨立於改函數的全局或成員變量來保存,每次判斷出回文后對其加1。且慢,如果光是加1這個切割次數就變成了整個過程的切割次數總和,這個數字肯定不對。讓我們在用aab的例子來看看,當我們解決完a和[ab的所有回文集合]的組合的問題后取長度為2的字符串aa時,切割次數應該是1,因為切完a后我們又從頭開始切aa,相當於對a的那次切割已經被取消了。從而可以得出每次遞歸解決問題后切割次數應該減1。

好了,那么現在最后的問題就是算最小切割次數了。每次遞歸結束時,我們都會得到一個回文集及切割的次數,將其與一個專門存放最小次數的變量比較,如果當前切割次數小於該變量就更新之即可。這里要注意的是我們對最后的一個子串也“切了一刀”,所以和最小次數比較及賦值時要減去1。

最后得出方案:

class Solution32_v1 {
public:
    int minCut(const std::string& s) {
        
        minCut_ = -1;
        currentCut_ = 0;
        
        FindMinPartition(s, 0);
        
        return minCut_;
    }
    
private: 
    void FindMinPartition(const std::string& s, size_t start)
    {
        if(start < s.size())
        {
            size_t pos = start;
            
            while(pos < s.size())
            {
                if(IsPalindrome(s, start, pos))
                {
                    ++currentCut_;    
                    FindMinPartition(s, pos + 1);
                    --currentCut_;
                }
                ++pos;
            }
        }
        else
        {
            if(currentCut_ - 1 < minCut_ || minCut_ == -1)
            {
                minCut_ = currentCut_ - 1;
            }
        }
    }
    
    int minCut_;
    int currentCut_;
};

 運行“Judge small”,順利通過測試!很好,下面讓我們運行“Judge Large”。什么,竟然提交失敗了,超時!

優化

在本機運行超時的用例:

fifgbeajcacehiicccfecbfhhgfiiecdcjjffbghdidbhbdbfbfjccgbbdcjheccfbhafehieabbdfeigbiaggchaeghaijfbjhi

需要6秒才能返回結果。看來LeetCode不允許這么長的執行時間。要怎么優化呢?

經過觀察,可以發現我們在遞歸的過程中有很多重復運算。以上面的字符串為例,我們會計算[f],[i],[f]與[gbeajcacehiicccfecbfhhgfiiecdcjjffbghdidbhbdbfbfjccgbbdcjheccfbhafehieabbdfeigbiaggchaeghaijfbjhi的所有回文集合]。然后又會計算[fif]與[gbeajcacehiicccfecbfhhgfiiecdcjjffbghdidbhbdbfbfjccgbbdcjheccfbhafehieabbdfeigbiaggchaeghaijfbjhi的所有回文集合]。在有很多遞歸調用的情況下,這種重復會浪費很多時間。所以我們需要想一個辦法重復利用某些計算結果。

首先,要保存已近計算過的分割次數,很自然的想到map。於是我們定義一個std::map<std::string, int>類型的類成員變量來保存某個子串的最小分割次數,該字符串本身作為key。

其次,由於要保存各子串的分割次數,使用一個成員變量不能滿足要求,我們需要通過返回值告訴自己的上層函數,並由上層函數計算出最小分割次數。

int partMin = -1;
int partCut = 0;

循環start

partCut = FindMinPartition(s, pos + 1);

if(partCut < partMin || partMin == -1)
{
     partMin = partCut;
}

循環end

return partMin;

最后,我們需要得到的是某個子串的分割次數而不是當前的分割次數。需要通過partCut - currentCut_計算出來。因為currentCut_為進一步分割子串前的分割次數,而partCut為分割子串后的分割次數,兩者的差即為該子串貢獻的分割次數。

這樣在每次嘗試遞歸求解之前,我們都可以判斷一下該子串以前有沒有被計算過,如果有,可以直接使用計算結果而避免遞歸。把所有功能結合起來之后:

class Solution132_v2
{
public:
    Solution132_v2() : currentCut_(0)
    {     
    }
    int FindMinPartition(const std::string& s, size_t start)
    {
        if(start < s.size())
        {        
            int pos = start;
            int partMin = -1;
            int partCut = 0;
            
            while(pos < s.size())
            {
                if(IsPalindrome(s, start, pos))
                {                 
                    std::string rest = s.substr(pos + 1, s.size() - pos - 1);
                    
                    ++currentCut_;
                    
                    if(rest != "")
                    {
                        std::map<std::string, int>::const_iterator iter = infoMap_.find(rest);
                        
                        if(iter != infoMap_.end())
                        {
                            if(currentCut_ + iter->second < partMin || partMin == -1)
                            {
                                partMin = currentCut_ + iter->second;
                            }
                            ++pos;
--currentCut_;
continue; } } partCut = FindMinPartition(s, pos + 1); if(partCut < partMin || partMin == -1) { partMin = partCut; } if(rest != "") { infoMap_[rest] = partCut - currentCut_; } --currentCut_; } ++pos; } return partMin; } else { return currentCut_ - 1; } } private: int currentCut_; std::map<std::string, int> infoMap_; };

現在在本機處理fifgbeajcacehiicccfecbfhhgfiiecdcjjffbghdidbhbdbfbfjccgbbdcjheccfbhafehieabbdfeigbiaggchaeghaijfbjhi只需要1ms了。提交,運行!這回大多數字符串通過了,但是又掛在了

aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

aabbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

這樣一個字符串上。

再次優化

在本機再次運行處理這個超長字符串,用時3.6秒。還能怎么優化呢?由於我們要找的是最小分割次數,是不是能終止一些已經沒可能產生最小分割的循環?嘗試在判斷回文成功后加入下列代碼:

int estimate = 0;
if(pos == s.size() - 1)
{
    estimate = currentCut_;
}
else
{
    estimate = currentCut_ + 1;
}
                    
if(estimate >= partMin && partMin != -1)
{
    break;
}

這里注意當當前回文是字符串后半部分時,分割次數是不會增加的(我們在后面返回時會減掉)。而如果不是最后,至少會增加1。如果估計的分割數將會大於等於當前的最小分割數,那么已經沒有必要再找下去了。

再次嘗試本機運行,用時基本沒有變化。這是怎么回事?重新審視我們的分割方法,每次取子串都是從最短的取起,如abba會首先得到[a],[b],[b],[a]的3次分割子集。這就導致了濾條件基本沒用,因為首先放進去的都是分割次數最大的。由於子串越長,分割次數肯定更少,我們應該采用先從最長子串取起的方法。還是拿最簡單的aab做例子。我們先取長度為3的aab,發現不是回文。再取長度為2的aa,發現是回文,進而求解b的回文集。最后去長度為1的a,進而求解ab的回文集。這樣從最長取起的方法就可以有效的利用我們的終止循環條件了。調整后的代碼如下:

class Solution132
{
public:
    Solution132() : currentCut_(0)
    {
    }
    int FindMinPartition(const std::string& s, size_t start)
    {
        if(start < s.size())
        {            
            int pos = s.size() - 1;
            int partMin = -1;
            int partCut = 0;

            while(pos >= (int)start)
            {
                if(IsPalindrome(s, start, pos))
                {
                    int estimate = 0;
                    if(pos == s.size() - 1)
                    {
                        estimate = currentCut_;
                    }
                    else
                    {
                        estimate = currentCut_ + 1;
                    }
                    
                    if(estimate >= partMin && partMin != -1)
                    {
                        break;
                    }                    
                    
                    std::string rest = s.substr(pos + 1, s.size() - pos- 1);
                    
                    ++currentCut_;
                    
                    if(rest != "")
                    {
                        std::map<std::string, int>::const_iterator iter = infoMap_.find(rest);
                        
                        if(iter != infoMap_.end())
                        {
                            //std::cout << "String: " << rest << "-Count: " << iter->second << std::endl;
                            if(currentCut_ + iter->second < partMin || partMin == -1)
                            {
                                partMin = currentCut_ + iter->second;
                            }
                            --pos;
                            --currentCut_;
                            continue;
                        }
                    }
                    
                    partCut = FindMinPartition(s, pos + 1);
                    if(partCut < partMin || partMin == -1)
                    {
                        partMin = partCut;
                    }
                    
                    if(rest != "")
                    {
                        infoMap_[rest] = partCut - currentCut_;
                    }
                    
                    --currentCut_;
                }
                --pos;
            }
            
            return partMin;
        }
        else
        {
            return currentCut_ - 1;
        }
    }
    
private:
    int currentCut_;
    std::map<std::string, int> infoMap_;
};

這回在本機處理那個超長字符串只要7ms了,應該沒問題了。提交並運行:

 終於成功通過 Judge Large!在這次解題過程中我們可以看到通過一步步的優化,實現同樣目的的程序性能可以有數百倍的提高。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM