问题描述
Pear有一个字符串,不过他希望把它切成两段
这是一个长度为N(<=105)的字符串
Pear希望选择一个位置,把字符串不重复不遗漏地切成两段,长度分别是t和N-t(这两段都必须非空)
Pear用如下方式评估切割的方案:
定义“正回文子串”为:长度为奇数的回文子串
设切成的两段字符串中,前一段中有A个不相同的正回文子串,后一段中有B个不相同的非正回文子串,则该方案的得分为A╳B。
注意,后一段中的B表示的是:“…非正回文…”,而不是: “…正回文…”。
那么所有的切割方案中,A╳B的最大值是多少呢?
【输入数据】
输入第一行一个正整数N(<=105)
接下来一行一个字符串,长度为N。该字符串仅包含小写英文字母
【输出数据】
一行一个正整数,表示所求的A╳B的最大值
【样例输入】
10
bbaaabcaba
【样例输出】
38
在对本题进行解答前必须先了解一下什么叫做“回文”。回文的定义是:
把相同的词汇或句子,在下文中调换位置或颠倒过来,产生首尾回环的情形,叫做回文,也叫回环
比如说,单个的某字符是一个回文(如a),多个的某些(如aba、aabbaa、abcba)也是
“上海自来水来自海上”是一个回文,“非人磨墨墨磨人”也是
而本题中则出现了一个新的名称“正回文”
这个新的名称其实就是在“回文”的基础上对其多加了一个限制(长度为奇数)
因此在我们解题过程中不可避免地需要写一个判断函数,用以判别传递进的某个字串是否为回文,如下:
bool isPalindrome(string str)
{ //判断传递进的字符串是否是回文
int i=0,j=str.length()-1;
while(i<j)
{
if(str[i]==str[j]) i++,j--;
else return false;
}
return true;
}
当然,你也可以顺带在这里面把是否为正回文(即字符串长度是否为奇)给判断了
接着我们来对题目进行一个解读和分析
题目给出的任务是让我们对一个字符串进行分割(要求不能存在为空的子串),假设分割后前面半截字符串为A,后面半截字符串为B。此时我们的程序需求出子串A中包含的正回文数re,以及子串B中包含的非正回文数notre,然后得到一个值re ╳ notre,显然这个值取决于你在分割原字符串时的分法。
比如说原字符串是ababc,则得到的re╳notre就有以下四个(值的个数=字符串的长度-1):
①假设你分割此字符串的分法为:a | babc,即子串A为”a”,子串B为”babc”。
则A中的正回文数为1(A中的全部子串为:”a”) (红色标注的子串为正回文串,否则不是)
B中的非正回文数为5(B中的全部子串为:”b”,”a”,”c”,”ba”,”ab”,”bc”,”bab”,”abc”,”babc”,注意去掉重复子串)
故得到 re * notre为1*5=5
这里需要对“非正回文”进行一个释义,何谓“非正回文”?
你传入一个字符串,对其进行判断其是否为正回文:如果为真则是正回文;如果为假则是非正回文
②假设你分割此字符串的分法为:ab | abc,即子串A为”ab”,子串B为”abc”。
则A中的正回文数为2(A中的全部子串为:”a”,”b”,”ab”)
B中的非正回文数为3(B中的全部子串为:”a”,”b”,”c”,”ab”,”bc”,”abc”)
故得到 re * notre为2*3=6
③假设你分割此字符串的分法为:aba | bc,即子串A为”aba”,子串B为”bc”。
则A中的正回文数为3(A中的全部子串为:”a”,”b”,”ab”,”ba”,”aba” ,注意去掉重复子串)
B中的非正回文数为1(B中的全部子串为:”b”,”c”,”bc”)
故得到 re * notre为3*1=3
④假设你分割此字符串的分法为:abab | c,即子串A为”abab”,子串B为”c”。
则A中的正回文数为4(A中的全部子串为:”a”,”b”,”ab”,”ba”,”aba”,”bab”,”abab”,注意去掉重复子串)
B中的非正回文数为0(B中的全部子串为:”c”)
故得到 re * notre为4*0=0
题目的要求就是得到这四个值中的最大值(即“所有的切割方案中,A*B的最大值是多少”)。
在上面我给出了一个样例,并以这个样例数据进行了题目的意义解读。
细心的同学可能早已从中发现了本题的一种解题思路——模拟
就像上面的我的分析一样,先用一个循环来枚举每一种切割方案,然后再对其中的每一个方案进行对应的求值,最后输出其中的最大值(即,模拟上面的分析过程来设计算法)
这样的思路很简单,至少在我说出来之后所有人都能马上get到这层意思。但还是那句话:“越容易想到的、越简单的算法,往往是性能越低的”。你仔细看本题的数据范围,N <= 105,乍一看还能接受,这意味着你最外层有105的数量级。但对于这最外层中的每一层,你又需要一个二重循环去遍历得到其中的所有子串。比如说对于某个字符串abcadhf,你如何得到其中的子串?你的算法应该是先利用一个外层循环指定起点,再使用一个内层循环确定在定起点的前提下的终点,并在此将指定子串截取出,如下:
int length=strlen(str); //str为指定的某个字符串
for(int i=0;i<length;i++)
for(int j=i;j<length;j++)
string substr=str.substr(i,j-i+1); //从i开始截取,截取长度为j-i+1的子串
这样看来就是一个三重循环的算法,此时你再来看最外层的105,显然是无法接受的。因为内层的两个循环的数量级与最外层的范围是相关的,且其算数平均数量级=最外层的数据范围/2
简言之,对于题目中给出的最大的那组数据,程序总的时间复杂度大概是O(n15/4),超时无疑
且对于其他(如n取值在103之上的数据),也有超时的风险,因此这个算法必然不可能拿到满分
此时我们就需要另辟捷径
在设计新的算法时,我们需要去总结前面的算法的失败原因,然后从中找到可取的,并舍弃不可取的
前面的算法有一个很大的问题是,做了大量的重复工作
还是拿前面对字符串”ababc”的分析举例,我们在①到④中都需要做两个工作,求子串A的正回文数,求子串B的非正回文数。比如我们在求A的正回文数时,这个过程如下:
①A=”a”时其正回文数为1(A中的全部子串为:”a”)
②A=”ab”时其正回文数为2(A中的全部子串为:”a”,”b”,”ab”)
③A=”aba”时其正回文数为3(A中的全部子串为:”a”,”b”,”ab”,”ba”,”aba”)
④A=”abab”时其正回文数为4(A中的全部子串为:”a”,”b”,”ab”,”ba”,”aba”,”bab”,”abab”)
你会发现,这个求正回文值的过程非常繁琐。对于每一次外层的变动(即字符串A的更新),内层都需要从中再次截取其包含的子串,然后再统计其中的正回文串。同样地,对于字符串B的非正回文串的统计也是如此。这个过程也就是导致前面的那个算法失败的主要原因。
好了,找到了前面的算法的失败原因之后,我们就可以对其进行改进。
首先需要解决的就是,如何把前面由于字符串A(或者字符串B)频繁更新而导致内层重复截取子串的问题解决掉。我们首先想到的应该是,打表(即,我们在程序录入了总的字符串后,就直接将其中的关于分割位置i所得到的对应的正回文数和非正回文数统计出来。这样在之后需要相关的值时就不必再去经历一个复杂且耗时的过程,而是直接从数组中获取即可)
于是可以得到以下两个数组
一个数组存放的是关于切割位置i的正回文数 re[N];
另一个数组存放的是关于切割位置i的非正回文数 not_re[N]
这里再强调一下:正回文数是针对前半截子串,非正回文数是针对后半截子串。因此在求数组 re 和 not_re 时,re 应该从总字符串左边开始向右遍历,not_re 应该从总字符串右边开始向左遍历。
下面给出通过打表得到的对于上面例子中(字符串”ababc”)的两个数组的信息,数组 re 的信息如下:
re[0]=1; //分割为a|babc
re[1]=2; //分割为ab|abc
re[2]=3; //分割为aba|bc
re[3]=4; //分割为abab|c
数组 not_re 中的信息如下:
not_re[1]=5; //分割为a|babc
not_re[2]=3; //分割为ab|abc
not_re[3]=1; //分割为aba|bc
not_re[4]=0; //分割为abab|c
此时我们需要做的就是通过一层循环,求出数组 re 和 not_re 在相应分割方案下,其乘积的最大值
注意:前面在求数组 re 和 not_re 时,由于出发方向的不同(一个从从左到右,一个自右向左),导致他们的起点也就不同(从左到右的数组从索引0开始,而自右向左的数组从索引 n-1 开始),同时也导致终点不同(从左到右的数组到索引 n-2 结束,而自右向左的数组到索引1结束),但是两者的长度都是一致的(均为 n-1)。因此两者在相对应时(即同属于一个分割方案时),其满足的要求是: re[n-1] & not_re[n]
拿上面的例子来说就是,re[0] 和 re[1] 表示同一个分割方案下分别得到的子串A和子串B;同样地,re[1] 和 not_re[2] 也是……所以得出 re[n-1] 和 not_re[n] 为同一个分割方案下分别得到的子串A和子串B。
对于上面的结果,我们来观察各个乘积的结果:
re[0] ╳ not_re[1]=1 ╳ 5=5
re[1] ╳ not_re[2]=2 ╳ 3=6
re[2] ╳ not_re[3]=3 ╳ 1=3
re[3] ╳ not_re[4]=4 ╳ 0=0
显然,这里面的最大值时6,这也和上面的分析结果一致
下面给出本题的完整代码
#include
#include
#include
using namespace std;
const int MAX=100010;
int re[MAX],not_re[MAX]; //分别存放在某个点切断时的(非)正回文数量
set<string> u1,u2; //集合去重以及统计子串数量
long long max(long long a,long long b)
{ return a>b?a:b; }
bool isPalindrome(string str)
{//判断传递进的字符串是否是回文
int i=0,j=str.length()-1;
while(i<j)
{
if(str[i]==str[j]) i++,j--;
else return false;
}
return true;
}
int main()
{
int N;
string str;
cin>>N>>str;
//求正回文的数量
for(int i=0;i<N;i++)
{
for(int j=i;j>=0;j-=2)
{
int len=i-j+1;
string temp=str.substr(j,len);
if(isPalindrome(temp)) u1.insert(temp);
}
re[i]=u1.size();
}
//求非正回文的数量
for(int i=N-1;i>0;i--)
{
for(int j=i;j<N;j++)
{
int len=j-i+1;
string temp=str.substr(i,len);
if(temp.length()%2 && isPalindrome(temp)) continue; //是正回文则跳过
u2.insert(temp);
}
not_re[i]=u2.size();
}
long long ans=0;
for(int i=1;i<N;i++)
ans=max(ans,re[i-1]*not_re[i]);
cout<<ans<<endl;
return 0;
}