[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!在这次解题过程中我们可以看到通过一步步的优化,实现同样目的的程序性能可以有数百倍的提高。

posted @ 2013-05-19 23:25  Shawnone  阅读(5201)  评论(2编辑  收藏  举报