题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=3613
题目大意:一条字符串切成两部分,如果是回文的要计算总价值,不是回文价值就为0
注意的两组数据:
Input
1 1 1......
a
Output
0
Input
-1 1 1......
aaaaaa
Output
-6(虽然AC代码输出0也对)
解题思路一:
利用反串对原串exkmp判断原串所有后缀是否回文
利用正串对反串exkmp判断反串所有后缀是否回文进而判断原串对应位置前缀是否回文。
这里的一个结论是
求出exkmp的extend数组, extend[i] == 后缀长度 后缀回文
证明:我们凭借自己的认知可以判断extend[i] = 后缀长度/2 (eg:7/2=3,8/2=4)就是回文了,一定要相等吗?这样会不会少考虑情况?
其实两种说法是等价的,当已匹配部分超过后缀长度/2,那么一定会完全匹配。
你看:
然后记得答案初始化为负无穷,不要为0,因为可能取不到0。
然后枚举。(先处理出前缀和,后缀和)
代码:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define ll long long
#define for1(i,a,b) for (int i=a;i<=b;i++)
#define for0(i,a,b) for (int i=a;i<b;i++)
#define INF 0x3f3f3f3f
#define idx(x) x-96
const int N = 5e5+5;
int nt[N],ex[N];
bool flag[2][N];
char s[N],ss[N];
int suf[N],pre[N];
int val[27];
void GETNEXT(char *s,int len)
{
int i=0,j,po;
nt[0] = len;
while (i+1<len && s[i]==s[i+1]) i++;
nt[1] = i;
po = 1;
for0(i,2,len){
if (po+nt[po]-i > nt[i-po])
nt[i] = nt[i-po];
else {
j = po+nt[po]-i;
if (j<0) j = 0;
while (i+j<len && s[i+j]==s[j]) j++;
nt[i] = j;
po = i;
}
}
}
void EXKMP(char *s,char *ss,int len,int sign)
{
int i=0,j,po;
GETNEXT(ss,len);
while (i<len && s[i]==ss[i]) i++;
ex[0] = i;
po = 0;
for0(i,1,len){
if (po+ex[po]-i > nt[i-po])
ex[i] = nt[i-po];
else {
j = po+ex[po]-i;
if (j<0) j = 0;
while (i+j<len && s[i+j]==ss[j]) j++;
ex[i] = j;
po = i;
}
}
if (sign==0){///后缀
for0(i,0,len) if (ex[i] == len-1 - i + 1) flag[0][i] = true;
}
else {///前缀
for0(i,0,len) if (ex[i] == len-1 - i + 1) flag[1][len-1-i+1-1] = true;
}
}
int main()
{
int T;
scanf("%d",&T);
while (T--){
memset(flag,false,sizeof flag);
for1(i,1,26) scanf("%d",val+i);
scanf("%s",s);
int len = strlen(s);
///求前缀和
pre[0] = val[idx(s[0])];
for0(i,1,len){
pre[i] = pre[i-1]+val[idx(s[i])];
}
///求反转字符串+求后缀和
suf[len] = 0;
for (int i=len-1;i>=0;i--){
suf[i] = suf[i+1] + val[idx(s[i])];
ss[len-1-i] = s[i];
}
ss[len] = '\0';
//puts(ss);
EXKMP(s,ss,len,0);///判断正串后缀是否回文
EXKMP(ss,s,len,1);///判断反串后缀是否回文,并转化为判断原串对应点的前缀是否回文
/*
for0(i,0,len){
if (flag[0][i]) printf("%d可做后缀 ",i);
if (flag[1][i]) printf("%d可做前缀\n",i);
}
*/
int ans = -INF;
///开始枚举所有切开的情况
for0(i,0,len-2){
if (flag[1][i] && flag[0][i+1]) ans = max(ans,pre[i]+suf[i+1]);
if (flag[1][i] && !flag[0][i+1]) ans = max(ans,pre[i]+0);
if (!flag[1][i] && flag[0][i+1]) ans = max(ans,0+suf[i+1]);
if (!flag[1][i] && !flag[0][i+1]) ans = max(ans,0);
}
printf("%d\n",ans);
}
return 0;
}
解题思路二:
核心思路还是利用manacher判断出前后缀是否回文。
现在看当时的代码写的挺蠢的(可能以后看上面的也会),你懂思路就好,我懒得再改了。
manacher预处理求出Len数组,然后枚举出所有左界为1或者右界为n的回文串(这样)
然后每种情况计算一下对应的价值总和,取最大值
这样我们可以求出所有①回文串+0(另一段不是回文)②回文串+回文串(另一半也是回文)
由第二组数据看出,我们不能保证每个串切成两段都不是回文的情况,因此答案不能初始化为0,先初始化为无穷小,
然后O(N)判断一下是否每个点切开来都是两个回文串,存在一组两个都不是回文的,答案就可以先更新为0
代码:
#include<cstring>
#include<cstdio>
#include<algorithm>
#include<vector>
#define for0(i,a,b) for (int i=a;i<b;i++)
#define for1(i,a,b) for (int i=a;i<=b;i++)
#define pt(x,y) printf("%s = %d\n",#x,y)
using namespace std;
const int N = 5e5+5;
char s[N],tmp[N<<1];
int Len[N<<1];
int INIT(char *st)
{
int i,len=strlen(st);//pt("len",len);
tmp[0]='@';
for(i=1;i<=2*len;i+=2)
{
tmp[i]='#';
tmp[i+1]=st[i/2];
}
tmp[2*len+1]='#';
tmp[2*len+2]='$';
tmp[2*len+3]=0;
return 2*len+1;
}
int MANACHER(char *st,int len)
{
int mx=0,ans=0,po=0;
for(int i=1;i<=len;i++)
{
if(mx>i)
Len[i]=min(mx-i,Len[2*po-i]);
else
Len[i]=1;
while(st[i-Len[i]]==st[i+Len[i]])
Len[i]++;
if(Len[i]+i>mx)
{
mx=Len[i]+i;
po=i;
}
ans=max(ans,Len[i]);
}
return ans-1;
}
struct node
{
int l_or_r;///回文是左边一半还是右边一半,左1,右2
int bank;///左边一半就是回文右边界,右一半就是左边界
};
vector<node>v;
int ans;
int val[30],presum[N],sufsum[N];///价值,前缀和,后缀和
#define idx(i) i-96
int main()
{
#define DEBUG
#ifdef DEBUG
//freopen("C:/Users/DELL/Desktop/input.txt", "r", stdin);
//freopen("C:/Users/DELL/Desktop/output.txt", "w", stdout);
#endif
int T;
scanf("%d",&T);
while (T--){
ans = -50000010;///初始答案设置为无穷小
memset(presum,0,sizeof presum);
memset(sufsum,0,sizeof sufsum);
//memset(Len,0,sizeof Len);
v.clear();
for1(i,1,26) scanf("%d",val+i);
scanf("%s",s);
int len = strlen(s);
if (len==1){///只有一个字符的情况特判
puts("0");
continue;
}
for (int i=1;i<=len;i++) presum[i] = presum[i-1]+val[idx(s[i-1])];
for (int i=len;i>=1;i--) sufsum[i] = sufsum[i+1]+val[idx(s[i-1])];
int llen = INIT(s);
MANACHER(tmp,llen);
///判断是否没有都不是回文的情况
for (int i=1;i<=len-1;i++){///[1,i]为左,[i+1,len]为右,判断是否怎样分都是回文
if (Len[i+1]-1!=i && Len[len+i+1]-1!=len-i) {ans = 0;break;}///存在两段都不是回文的话,ans=0可以做到
}
for1(i,1,len){
int mid_len = Len[2*i]-1;///XXIXX
int lmid_len = Len[2*i+1]-1;///XXIXXX
int rmid_len = Len[2*i-1]-1;///XXXIXX
if (mid_len>0 && mid_len<len){
if (i-mid_len/2==1) v.push_back({1,mid_len});
if (i+mid_len/2==len) v.push_back({2,len+1-mid_len});
}
if (lmid_len>0 && lmid_len<len){
if (i-lmid_len/2+1==1) v.push_back({1,lmid_len});
if (i+lmid_len/2==len) v.push_back({2,len+1-lmid_len});
}
if (rmid_len>0 && rmid_len<len){
if (i-rmid_len/2==1) v.push_back({1,rmid_len});
if (i+rmid_len/2-1==len) v.push_back({2,len+1-rmid_len});
}
}
for (auto now:v){
//for (int i=0;i<v.size();i++){node now = v[i];
int nowans = 0;
if (now.l_or_r==1){///当前回文是左边一块
nowans = presum[now.bank];
int st = now.bank+1,ed = len;
int l = ed-st+1;
//pt("l",l);
if (Len[st+ed]-1==l){
nowans += sufsum[st];
}
}
else {///当前回文是右边一块
nowans = sufsum[now.bank];
int st = 1,ed = now.bank-1;
int l = ed-st+1;
if (Len[st+ed]-1==l){
nowans += presum[ed];
}
}
ans = max(ans,nowans);
}
printf("%d\n",ans);
}
return 0;
}
总结:
①exkmp:在处理回文问题方面,exkmp能判断前后缀是否回文,针对此类问题exkmp比较方便,清晰.
但是exkmp面对不是前缀也不是后缀的情况判断回文就无能为力了。(也许有但我还不会)
②manacher可以判断任意回文,但是要分类,做起来可能没有exkmp判断前后缀那么形象。Len数组用起来真的烦!(也许事实上方便但我还不会)
事实上manacher判断一段区间是否回文判断 Len[ed-st]-1==ed-st+1即可emm,所以两种方法应该是差不多的。