不能!! 运用动态规划识别最大回文子串

经知乎用户@Yaxe 提醒,最大回文子串问题可能不能用动态规划解决,因为不满足最优子问题条件,如反例bananas, abbabbabba等。

鉴于本博文对于我自己仍有学习动态规划的意义,还是保留原文。

原文如下:

运用动态规划识别最大回文子串

本文记录了一种灵活利用动态规划(无须回溯)来识别最大回文子串的算法,比流行的马拉车算法性能更好,是我练习动态规划时想到的。
本文将先给出一个要求识别最大回文子串的简化例题,再给出流行的马拉车算法解法、简单的动态规划解法、优化后的动态规划解法,最后给出一个关于相比于简化例题更一般化场景的解答

一个简化例题

例题来自 1040 Longest Symmetric String (25分)

题目中文大意:给定一个非空字符串,仅要求输出其最大回文子串长度。
输入样例:

Is PAT&TAP symmetric?

输出样例:

11

马拉车解法(Manacher’s Algorithm)

马拉车算法需要扩充输入的字符串以适应初始串长奇偶不同的情况,其时间复杂度为O(n),空间复杂度为O(n),其中n为初始串长。
马拉车算法详细解释可见https://zhuanlan.zhihu.com/p/70532099

容易想到的动态规划解法

时间O(n) 空间O(n)

原始输入数据:
输入数据
设置动态规划数组dp[]用于保存以line[i]为末尾的回文串的串首下标
加入dp[]数组
观察上表会很容易联想到i与dp[i]的关系中包含了最大回文子串的信息,即最大回文子串的长度必为i-dp[i]+1的最大取值(11-1+1==11)。
那么要如何在不回溯的情况下一边扫描获得dp[]数组呢?dp[i]由远及近依次:

(0、dp[0]根据定义必然是0)
1、可能是dp[i-1]-1(对应于line下标从dp[i-1]到i-1是一个回文串,且line[dp[i-1]]==line[i],即dp[i-1]-1为最远可能的地方);
2、可能是dp[i-1](对应于line[k]==line[k+1]==line[k+2]==line[…]==line[i-2]==line[i-1]==line[i],其中0<=k<i,即一连串相同的字符);
3、可能是i本身(对应于line[i]与之前的字符不构成回文串,类似于dp[0])

其中第2条不能写成

if(情况0{...}
else if(情况1){...}
else if(line[i]==line[i-1]){
	dp[i]=dp[i-1];	//错误
}
else{...}//情况3

比如反例:
情况2
此时dp[4]就不是dp[3]==1,而是3,这是因为虽然line[i]==line[i-1],但不能保证dp[i-1]也是由情况2产生的。
因此需要引入另一个数组seq[],用于在情况2时判断以line[i-1]为末尾的回文串是否由连续的同一字符组成(情况0、3除外)。
那么情况2应该写成

if(情况0{...}
else if(情况1){...}
else if(line[i]==line[i-1]){
	if(seq[i-1])dp[i]=dp[i-1];
	else dp[i]=i-1;
	seq[i]=true;
}
else{...}//情况3

引入seq[]数组
上述讨论落实到编程时还要注意i-1要有意义,dp[i-1]-1要有意义。

当获得了dp[]数组后,便可以一趟扫描确定表达式i-dp[i]+1最大的值,也就是最长回文子串的长度。

而上面所有的seq[]数组、dp[]数组、最大长度均能写入同一个一趟扫描循环之内。

//  时间O(n) 空间O(n)
int main(){
    string line;
    getline(cin,line);
    vector<int>dp(line.size());     //  dp[i]为以line[i]为末尾的回文串的串首下标
    vector<bool>seq(line.size());   //  seq[i]表示以line[i]为末尾的回文串是否由连续的同一字符组成
    vector<bool>sep(line.size());   //  sep[i]表示以line[i]为末尾的回文串是否由连续的同一字符组成
    int maxlen=1;
    for(int i=1;i<line.size();++i){
        int counterind=dp[i-1]-1;   //  最远可能的回文串串首下标
        if(counterind>=0&&line[counterind]==line[i])    //  先检查最远处是否可能
            dp[i]=counterind;
        else{                       //  此时最远下标已不可能
            if(line[i-1]==line[i]){ //  检查是否可能组成更短的由连续相同字符组成的回文串
                if(seq[i-1])        //  前面已经有好几个相同字符了
                    dp[i]=dp[i-1];
                else                //  当前字符才刚刚为第二个相同字符
                    dp[i]=i-1;
                seq[i]=true;
            }
            else                    //  最远处不可能,也不可能是相同字符的回文串,则只能是最简单回文串
                dp[i]=i;            //  即本回文串就只有这一个字符
        }
        if(i-dp[i]+1>maxlen)        //  更新最大回文串长度
            maxlen=i-dp[i]+1;
    }
    cout<<maxlen;
    return 0;
}

只需一趟扫描就能同步确定seq[]、dp[]和最大长度,且无回溯,故时间复杂度为O(n),n为输入的字符串长度。
使用的辅助数组seq[]和dp[]均和输入字符串一样长,故空间复杂度为O(n)

改进的动态规划解法

时间O(n) 空间O(1)

仔细观察上述算法,发现每次确定最大长度时之和当前的dp[i]有关,而确定dp[i]只与dp[i-1]、seq[i-1]有关,故可以压缩辅助数组seq[],只使用单个变量seq表示原来的seq[i-1],同理只用单个变量dp表示原来的dp[i]与dp[i-1]。
比如如果原来是dp[i]=dp[i-1],则dp不用改变,否则如果是dp[i]=某值,则现在为dp=某值)

//  动态规划,空间优化   时间O(n) 空间O(1)
int main(){
    string line;
    getline(cin,line);
    int dp=0;       //  即dp[i-1]或dp[i]
    bool seq=false; //  即seq[i-1]或seq[i]
    int maxlen=1;
    for(int i=1;i<line.size();++i){
        int counterind=dp-1;    //  dp[i-1]
        if(counterind>=0&&line[counterind]==line[i]){
            dp=counterind;  //  dp[i]
            seq=false;      //  new
        }
        else{
            if(line[i-1]==line[i]){
                //if(seq)   dp[i]=dp[i-1];
                if(!seq)
                    dp=i-1; //  dp[i]
                seq=true;   //  changed
            }
            else{
                dp=i;    // dp[i]
                seq=false;  //  new
            }
        }
        if(i-dp+1>maxlen)       // dp[i]
            maxlen=i-dp+1;   // dp[i]
    }
    cout<<maxlen;
    return 0;
}

时间效率方面,本就是一趟遍历,无法更加优化,故还是O(n)
空间效率方面,将原来长度为n的两个辅助数组压缩成两个简单变量,优化成了O(1)

完整的例题

不仅仅要求出最大回文串长度,而且要给出最大回文串在初始串中的位置,换句话说就是要给出该最大回文串本身。这才是识别最大回文串的更一般化的场景。
不难发现,只需增设一个变量记录最大回文串出现的位置,在上面的一趟遍历更新最大回文串长的时候同步更新该变量,即可方便地满足上述需求。且时间复杂度、空间复杂度还是为O(n)、O(1)。

杂谈

这是我的第二篇博文,私以为还蛮有创造性和价值的,希望对你有所帮助。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值