标签:级别 ref 预处理 bbb 模板 insert 需要 转化 img
尽量找了个字符串里面最简单的部分来入门
发现并没想象中的简单...
模板:
int n,p[N]; char s[N]; int manacher(int len) { int mx=0,id=0,res=0; for(int i=1;i<=len;i++) { p[i]=(mx>i?min(p[2*id-i],mx-i):1); while(i-p[i]>=1 && i+p[i]<=len && s[i-p[i]]==s[i+p[i]]) p[i]++; res=max(res,p[i]-1); if(i+p[i]>mx) mx=i+p[i],id=i; } return res; }
Manacher能做的事情是,$O(n)$地求出以每个位置为中心的最长字符串长度;比$O(n^2)$的暴力不知道高到哪里去了
不过,算法的步骤其实并不复杂
首先,我们将一个字符串用占位符隔开
比如:ababbabc,将其用“#”隔开,那么就是 #a#b#a#b#b#a#b#c#
这样把字符串加倍有一个好处,就是可以表示长度为偶数的字符串中心
比如#a#b#b#a#的中心就是从左到右第三个“#”
接着,我们从$1$循环到$2n+1$(插入过占位符的字符串)依次求出$p[i]$
$p[i]$的直接含义用文字表述出来有点麻烦,不如先观察一下上面的例子
对于$i=4$,$s[i]=b,p[i]=4$,可以这样理解:
以这个b为中心的最大回文串为 #a#b#a#,那么将b到两端的子串分别分离出来,即 #a#b 和 b#a#,他们的长度都是$4$,正是$p[i]$的大小
于是,以$i$为中心的最大回文串 范围就是$[i-p[i]+1,i+p[i]-1]$;这个回文串的长度总是奇数(即$2\times p[i]-1$)
为了高效求出$p[i]$,我们需要维护两个值$mx,id$
$mx$表示,对于所有$j<i$中 最大的$j+p[j]$,也可以理解为所有中心小于$i$的回文串中 最大的右端点位置 再$+1$;$id$表示的就是这个回文串中心$j$
这两个值可以共同决定当前$p[i]$的下界,即代码中的关键部分
for(int i=1;i<=n;i++) { p[i]=(mx>i?min(p[2*id-i],mx-i):1); ... }
什么是$mx>i$?根据上面的约定,这表示$i$被以$id$为中心的回文串$P_{id}$所包含
由于$i>id$,那么在$P_{id}$中,一定存在一个与$i$关于$id$对称的位置$2\times id-i$;于是在$P_{id}$的范围中,以$i$为中心的子串就与 以$2\times id-i$为中心的子串相同了,那么自然$p[i]$相同
那么为什么是$min(p[2*id-i],mx-i)$呢?有可能以$2\times id-i$为中心的最长回文子串 超出了$P_{id}$的范围,那么对应的,我们只能保证$i+p[i]\leq mx$的部分是回文串,其余的只能暴力向后判断,故要将$p[2*id-i]$与$mx-i$取min
至于$i=mx$的情况(不可能出现$i>mx$),由于不能参考之前的最长回文子串,于是暴力向后判断即可
这样的算法为什么是$O(n)$的呢?我们需要考虑上面代码给$p[i]$赋的初值
如果$p[i]=p[2*id-i]$,那么说明以$2\times id-i$为中心的最长回文子串不超过$P_{id}$的范围,也就是说这个回文子串不可能再往外扩展了;于是之后的暴力判断会直接退出
如果$p[i]=mx-i$或$p[i]=1$,那么说明$i+p[i]>=mx$,之后的暴力判断就必然会将$mx$的值增大;而$mx$最多只能增大$n$次,故一共只会进行$O(n)$级别的暴力判断
一些题目,太难的还不太会...
Luogu P3805 (【模板】manacher算法)
原串中的最大回文子串长度 等于 最大的$p[i]-1$
因为插入占位符后的新串中,最大回文子串的长度为$2\times p[i]-1$,而这样的回文子串的两端一定是“#”;那么原串中对于的子串长度为$p[i]-1$
#include <cstdio> #include <cstring> #include <algorithm> using namespace std; const int N=25000010; int n,p[N]; char s[N]; int main() { scanf("%s",s+1); n=strlen(s+1); for(int i=2*n+1;i>=1;i--) s[i]=(i%2?‘#‘:s[i/2]); n=2*n+1; int mx=0,id=0,ans=0; for(int i=1;i<=n;i++) { p[i]=(mx>i?min(p[2*id-i],mx-i):1); while(i-p[i]>=1 && i+p[i]<=n && s[i-p[i]]==s[i+p[i]]) p[i]++; ans=max(ans,p[i]-1); if(i+p[i]>mx) mx=i+p[i],id=i; } printf("%d\n",ans); return 0; }
BZOJ 3790 (神奇项链)
既然是多个回文串相拼,那么每个分别是最长回文子串是最优的;否则可以将多个串合并成一个拼上去,使得答案更优
我们将以每个位置为中心的最长回文子串全部拿出来,那么就能得到字符串上的多个区间
题目在这时就变成了最小区间覆盖,可以贪心解决
#include <cstdio> #include <cstring> #include <algorithm> using namespace std; const int N=100005; int n,p[N]; char s[N]; int to[N]; int main() { while(~scanf("%s",s+1)) { n=strlen(s+1); for(int i=2*n+1;i>=1;i--) s[i]=(i%2?‘#‘:s[i/2]); n=2*n+1; int mx=0,id=0; for(int i=1;i<=n;i++) { p[i]=(mx>i?min(p[2*id-i],mx-i):1); while(i-p[i]>=1 && i+p[i]<=n && s[i-p[i]]==s[i+p[i]]) p[i]++; if(i+p[i]>mx) mx=i+p[i],id=i; } memset(to,0,sizeof(to)); for(int i=1;i<=n;i++) { int l=i-p[i]+1,r=i+p[i]-1; to[l]=max(to[l],r); } int ans=0,cur=0,rmost=0; for(int i=1;i<=n;i++) { rmost=max(rmost,to[i]); if(i>cur) ans++,cur=rmost; } printf("%d\n",ans-1); } return 0; }
Nowcoder 14943 (小G的项链)
首先考虑$n$的约数数量应该不是很多,那么可以对每个约数分别判断
对于某约数$k$,新项链长为$\frac{n}{k}$
我们要判断它是否能回文,就必须枚举一下起始位置;不过由于每$k$个项链合并一次,起始位置为$1$和为$k+1$是等价的,那么只需要枚举$k$个起始位置
对于一个固定的起始位置,我们可以通过预处理前缀异或和,$O(\frac{n}{k})$地快速获得新项链中的$\frac{n}{k}$项
对这个新数组复制一次(环上问题的常用套路)后做Manacher,看一看最长回文子串长度是否大于等于$\frac{n}{k}$即可
#include <cstdio> #include <locale> #include <cstring> #include <algorithm> using namespace std; const int N=800005; int n; int a[N],pre[N]; int s[N],p[N]; int manacher(int len) { int mx=0,id=0,res=0; for(int i=1;i<=len;i++) { p[i]=(mx>i?min(p[2*id-i],mx-i):1); while(i-p[i]>=1 && i+p[i]<=len && s[i-p[i]]==s[i+p[i]]) p[i]++; res=max(res,p[i]-1); if(i+p[i]>mx) mx=i+p[i],id=i; } return res; } int main() { scanf("%d",&n); for(int i=1;i<=n;i++) scanf("%d",&a[i]),a[n+i]=a[i]; for(int i=1;i<=2*n;i++) pre[i]=pre[i-1]^a[i]; int ans=0; for(int i=1;i<=n && !ans;i++) { if(n%i) continue; int m=4*n/i+1; for(int j=0;j<i;j++) { for(int k=1;k<=n/i;k++) s[k]=pre[k*i+j]^pre[(k-1)*i+j],s[n/i+k]=s[k]; for(int k=m;k>=1;k--) s[k]=(k%2?0:s[k/2]); if(manacher(m)>=n/i) ans=n/i; } } printf("%d\n",ans); return 0; }
Nowcoder 17062 (回文)
考虑对每一个最长回文子串计算代价;若最中间不是最长回文子串,就会导致多删除元素
那么接着就需要考虑中心回文串 两侧的两个子串应该如何处理
由于它们两个在初始条件下不对称,那么至少需要完全删除一个子串,否则最内侧的两个元素不对称
最优解有没有可能在两个子串中都先删除后添加呢?如果是这样的话,可以在左右两端各少添加一个元素,此时整个字符串仍为回文;故最优解的方案是,将一侧的子串全删完、将另一侧删一部分(可能不删),然后将删完的那一侧对称补全
此时的总代价可以转化为,将一侧删完,将另一侧删一部分、再添加剩余的部分
这就可以通过dp来解决了
令$ldel[i],ladd[i]$分别表示 从左侧开始删到第$i$个元素、先删后添加到第$i$个元素的最小代价
那么有$ldel[i]=ldel[i-1]+del[s[i]-‘a‘],ladd[i]=min(ldel[i],ladd[i-1]+add[s[i]-‘a‘])$;$rdel[i],radd[i]$同理
于是对于原串中的每个极大回文子串$[l,r]$,取$min(ladd[l-1]+rdel[r+1],ldel[l-1]+radd[r+1])$就是此位置的代价
#include <cstdio> #include <cstring> #include <algorithm> using namespace std; typedef long long ll; const int N=200005; int n,p[N]; char s[N]; int manacher(int len) { int mx=0,id=0,res=0; for(int i=1;i<=len;i++) { p[i]=(mx>i?min(p[2*id-i],mx-i):1); while(i-p[i]>=1 && i+p[i]<=len && s[i-p[i]]==s[i+p[i]]) p[i]++; res=max(res,p[i]-1); if(i+p[i]>mx) mx=i+p[i],id=i; } return res; } ll add[30],del[30]; ll ladd[N],ldel[N],radd[N],rdel[N]; int main() { scanf("%s",s+1); n=strlen(s+1); n=2*n+1; for(int i=n;i>=1;i--) s[i]=(i%2?‘#‘:s[i/2]); manacher(n); for(int i=0;i<26;i++) scanf("%lld%lld",&del[i],&add[i]); for(int i=1;i<=n/2;i++) ldel[i]=ldel[i-1]+del[s[i*2]-‘a‘], ladd[i]=min(ladd[i-1]+add[s[i*2]-‘a‘],ldel[i]); for(int i=n/2;i>=1;i--) rdel[i]=rdel[i+1]+del[s[i*2]-‘a‘], radd[i]=min(radd[i+1]+add[s[i*2]-‘a‘],rdel[i]); ll ans=1LL<<60; for(int i=1;i<=n;i++) { int l=(i-p[i]+2)/2,r=(i+p[i]-1)/2; ans=min(ans,ldel[l-1]+radd[r+1]); ans=min(ans,ladd[l-1]+rdel[r+1]); } printf("%lld\n",ans); return 0; }
BZOJ 2342 (双倍回文,$SHOI2011$)
这个题目还是很有意思的,展示了$p[i]$数组的进阶用法
对于一个双倍回文子串,我们可以(在添加了占位符的数组中)用$i$表示中点位置
那么我们要找到最小的$j<i$满足$j+p[j]\geq i$且$j\geq i-p[i]/2$
其中,$j+p[j]\geq i$防止了$i$为中心的回文子串$P_i$中间多出几个元素、导致仅为回文而不是双倍回文的情况
而$j\geq i-p[i]/2$防止了$j$为中心的回文子串$P_j$总有一部分在$P_i$范围之外、导致不为双倍回文的情况
这两个条件能够完全限制住符合条件的$j$(注意$i,j$都应为奇数,否则双倍回文串长度不为$4$的倍数),不过优先满足哪个条件会使得写法有一些不同
如果优先满足$j+p[j]\geq i$,那么可以将所有序号$j$按$j+s[j]$从大到小排序,接着从大到小循环$i$、将满足$j+s[j]\geq i$的$j$全部压入一个set
这时只需要在set中找到一个最小的$j$满足$j\geq i-p[i]/2$,用lower_bound就行了
#include <set> #include <cstdio> #include <cstring> #include <algorithm> using namespace std; typedef pair<int,int> pii; const int N=1000005; int n,p[N]; char s[N]; pii a[N]; set<int> S; int main() { scanf("%d",&n); scanf("%s",s+1); for(int i=2*n+1;i>=1;i--) s[i]=(i%2?‘#‘:s[i/2]); n=2*n+1; int mx=0,id=0; for(int i=1;i<=n;i++) { p[i]=(mx>i?min(p[2*id-i],mx-i):1); while(i-p[i]>=1 && i+p[i]<=n && s[i-p[i]]==s[i+p[i]]) p[i]++; if(i+p[i]>mx) mx=i+p[i],id=i; } for(int i=1;i<=n;i+=2) a[i/2+1]=pii(i+p[i],i); sort(a+1,a+n/2+2); int ans=0,j=n/2+1; for(int i=n;i>=1;i-=2) { while(j>=1 && a[j].first>=i) S.insert(a[j].second),j--; int pos=i-p[i]/2; if(S.lower_bound(pos)!=S.end()) ans=max(ans,(i-*S.lower_bound(pos))*2); } printf("%d\n",ans); return 0; }
如果优先满足$j\geq i-p[i]/2$,那么我们从小到大循环$i$,先找到最小的$j$满足$j\geq i-p[i]$,但是此时的$j$不一定满足$j+p[j]\geq i$
如果$j+p[j]<i$,那么就更加不可能$j+p[j]<i+1$了,故可以直接不再考虑$j$;这可以通过并查集中由$j$向$j+1$连边来实现
这样在并查集中一直跳,直到第一个$j$满足$j+p[j]\geq i$
#include <cstdio> #include <cstring> #include <algorithm> using namespace std; const int N=1000005; int n,p[N],fa[N]; char s[N]; int find(int a) { if(fa[a]==a) return a; return fa[a]=find(fa[a]); } int main() { scanf("%d",&n); scanf("%s",s+1); for(int i=2*n+1;i>=1;i--) s[i]=(i%2?‘#‘:s[i/2]),fa[i]=i; n=2*n+1; int mx=0,id=0; for(int i=1;i<=n;i++) { p[i]=(mx>i?min(p[2*id-i],mx-i):1); while(i-p[i]>=1 && i+p[i]<=n && s[i-p[i]]==s[i+p[i]]) p[i]++; if(i+p[i]>mx) mx=i+p[i],id=i; } int ans=0; for(int i=1;i<=n;i+=2) { int j=max(1,i-p[i]/2); if(j%2==0) j++; while(j+p[j]<i) { fa[j]=find(j+2); j+=2; } ans=max(ans,(i-j)*2); } printf("%d\n",ans); return 0; }
慢慢补一点更难的题
(待续)
标签:级别 ref 预处理 bbb 模板 insert 需要 转化 img
原文地址:https://www.cnblogs.com/LiuRunky/p/Manacher.html