字符串总结2——回文串相关

回文串相关

1.manacher

为了避免长度奇偶性带来的麻烦,给每个字符前加一个特殊字符’#‘,为了避免非法访问可以在0号位置上再加个’@’
马拉车算法主要是计算 r [ i ] r[i] r[i]表示以 i 为回文中心能向左右拓展的最长长度,称为回文半径
我们维护两个值mx和p,分别表示之前得到的回文串最右边界的位置和这个回文串的中心p
我们要计算先给 r [ i ] r[i] r[i]赋一个下界,设 j 为 i 关于 p 的对称点,即 j=2p-i ,我们需要分以下三种情况讨论:

  1. i >= mx ,那么初值就是 1
  2. mx - i > r[j] ,那么初值就是r[j]
  3. mx - i <= r[j],那么初值就是mx-i

这三种情况合并起来可以写成 r [ i ] = m i n ( m x − i , r [ 2 ∗ p − i ] ) r[i]=min(mx-i,r[2*p-i]) r[i]=min(mxi,r[2pi])
然后就是向右拓展合法的位置即可

因为每次向右拓展都会带来mx的更新,所以复杂度均摊$O(n)

P5446 [THUPC2018]绿绿和串串

发现我们要求的是一个串最长可以由哪个前缀操作构成,然后不断进行这个操作不知道怎么能发现,删去末尾最短的回文串(长度大于1) 的后一半即可

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=2e6+5;
int T,cnt,n;
char s[maxn],ss[maxn];
int r[maxn],vis[maxn];
int main()
{
    // freopen("a.in","r",stdin);
    // freopen("a.out","w",stdout);
    scanf("%d",&T);
    while(T--)
    {
        scanf("%s",s+1);
        n=strlen(s+1);
        cnt=0;
        ss[0]='?'; ss[++cnt]='#';
        for(int i=1;i<=n;i++) ss[++cnt]=s[i],ss[++cnt]='#';
        ss[cnt+1]='~';
        for(int i=0;i<=cnt;i++) vis[i]=0,r[i]=0;
        int mx=0,mid=0;
        for(int i=1;i<=cnt;i++)
        {
            if(i<=mx) r[i]=min(mx-i,r[mid*2-i]);
            while(ss[i+r[i]]==ss[i-r[i]]) r[i]++;
            if(i+r[i]>mx) mx=i+r[i]-1,mid=i;
            // printf("%d ",r[i]);
        }
        // printf("\n");
        for(int i=cnt;i>=1;i--)
        {
            if(i+r[i]-1==cnt) vis[i]=1;
            else if(vis[i+r[i]-2] && i==r[i])
                vis[i]=1;
        }
        for(int i=1;i<=cnt;i++)
            if(ss[i]>='a' && ss[i]<='z' && vis[i])
                printf("%d ",i/2);
        printf("\n");
    }
    return 0;
}
ybtoj527.回文子串

看到这种区间性的操作,可以尝试使用线段树维护
给线段树上每个点维护st,ed,res,分别表示前k个字符构成的字符串,后k个字符构成的字符串,res表示区间的答案

修改操作因为是推平成一个字符,很好处理
重点是处理好pushup即可
对于合并两个区间,也就是要计算跨过mid 和 mid+1 两个位置和回文串个数,可以把ed[lson]和st[rson]拼接到一起,做一次manacher,统计跨过mid 和 mid+1的个数即可

时间复杂度 O ( q k l o g ∣ S ∣ ) O(qklog|S|) O(qklogS)


2.回文自动机 PAM

把回文串分为奇数长度和偶数长度两种,分别建立奇根和偶根,每个转移边相当于在两边同时加上字符c,得到的都是回文串
我们模仿后缀自动机的增量构造方式,依然是按顺序一个字符一个字符插入,维护fail边指向最长的回文后缀,同时给每个节点维护回文串长度 l e n [ u ] len[u] len[u]
插入一个字符先跳fail边直到 s i = s i − l e n [ u ] − 1 s_i=s_{i-len[u]-1} si=silen[u]1,也就是找到一个可以通过两边增加字符 s i s_i si得到另一个回文串的位置,如果这个串还没有出现过,就增加这个点,然后维护它的fail,方式依然是跳fail直到能匹配上为止。
注意把偶根的fail指向奇根,而奇根不需要管fail,因为奇根不可能实配。
这样就得到了回文自动机,可以证明节点数为 O ( n ) O(n) O(n),每次插入一个字符最多只会新增一个回文串。

例题 P3649 [APIO2014]回文串

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=3e5+5;
char s[maxn];
struct PAM
{
	int ch[26],len,cnt,fail;
}tr[maxn];
int last,tot;
ll ans;
int newnode(int x)
{
	tr[++tot].len=x;
	return tot;
}
int getfail(int x,int n)
{
	while(s[n-tr[x].len-1]!=s[n]) x=tr[x].fail;
	return x;
}
int main()
{
//	freopen("a.in","r",stdin);
//	freopen("a.out","w",stdout);
	scanf("%s",s+1); s[0]=-1;
	tr[0].fail=1; tr[0].len=0;
	tr[1].len=-1; tot=1;
	for(int i=1;s[i];i++)
	{
		s[i]-='a';
		int p=getfail(last,i);
		if(!tr[p].ch[s[i]])
		{
			int q=newnode(tr[p].len+2);
			tr[q].fail=tr[getfail(tr[p].fail,i)].ch[s[i]];
			tr[p].ch[s[i]]=q;
		}
		last=tr[p].ch[s[i]];
		tr[last].cnt++;
	}
	for(int i=tot;i;i--)
		tr[tr[i].fail].cnt+=tr[i].cnt,ans=max(ans,1LL*tr[i].cnt*tr[i].len);
	printf("%lld\n",ans);
	return 0;	
} 
P4287 [SHOI2011]双倍回文

每个节点维护一个 f [ i ] f[i] f[i]表示小于等于一半长度的最长回文后缀长度
维护方式:如果这个点的fail本身长度小于等于一半更新即可
否则跳fail直到字符合法且长度合法为止
代码

P4555 [国家集训队]最长双回文串

正反建立两个回文自动机
分别求出 L [ i ] L[i] L[i] R [ i ] R[i] R[i]表示以 i 为左/右端点的最长回文串长度
那么 a n s = m a x i < n ( L [ i ] + R [ i + 1 ] ) ans=max_{i<n}(L[i]+R[i+1]) ans=maxi<n(L[i]+R[i+1])
代码

P1659 [国家集训队]拉拉队排练

待完成!


3.border理论

对于字符串S,如果 p r e ( S , i ) = s u f ( S , i ) pre(S,i)=suf(S,i) pre(S,i)=suf(S,i),也就是长度为 i 的前后缀相等,就称 p r e ( S , i ) pre(S,i) pre(S,i)为S的一个border

定理:将字符串S的所有回文后缀按照长度排序后,可以划分成 l o g ∣ S ∣ log|S| logS段等差数列

引理1:对于回文串S,T是S的后缀,T是S的border的充要条件为T是回文串

引理2:对于S的borderT,如果 ∣ S ∣ ≤ 2 ∣ T ∣ |S| \le 2|T| S2T ,那么S是回文串的充要条件为T是回文串

引理3:对于回文串S, ∣ S ∣ − ∣ T ∣ |S|-|T| ST是S的最小周期的充要条件是T是S的最长回文真后缀

引理4:在这里插入图片描述

例题 最小回文划分问题

给定一个字符串S,求最少把S划分为几段,能使得S的每一段均为回文串

很容易得到dp,设 f i f_i fi表示前 i 位的答案,那么 f i = m i n { f j + 1 } , s [ j + 1... i ] 为 回 文 串 f_i=min \{f_j+1\},s[j+1...i]为回文串 fi=min{fj+1},s[j+1...i]
我们可以建立PAM,用 O ( n 2 ) O(n^2) O(n2)的时间复杂度暴力转移,也就是在当前节点暴力跳fail,每个人都尝试转移一次

在这里插入图片描述在这里插入图片描述

#include <bits/stdc++.h>

#define N 500005
using namespace std;
struct Node
{
    int len, fa, ch[26];
} nd[N << 1];
int ct = 1, last, diff[N], slink[N], dp[N], g[N];
char op[N];

inline int newNode(int x)
{
    int p = ++ct;
    nd[p].len = x;
    return p;
}

inline void add(int c, int pos)
{
    int u = last;
    while (op[pos - nd[u].len - 1] != op[pos]) u = nd[u].fa;
    if (nd[u].ch[c] == 0) {
        int s = newNode(nd[u].len + 2), f = nd[u].fa;
        while (op[pos - nd[f].len - 1] != op[pos]) f = nd[f].fa;
        nd[s].fa = nd[f].ch[c], nd[u].ch[c] = s;
    }
    last = nd[u].ch[c];
    diff[last] = nd[last].len - nd[nd[last].fa].len;
    if (diff[last] == diff[nd[last].fa]) slink[last] = slink[nd[last].fa];
    else slink[last] = nd[last].fa;
}

int main()
{
    nd[0].len = 0, nd[0].fa = 1, nd[1].len = -1, nd[1].fa = 0;
    scanf("%s", op + 1);
    for (int i = 1; op[i]; i++) {
        add(op[i] - 'a', i), dp[i] = 0x7fffffff;
        for (int j = last; j > 1; j = slink[j]) {
            g[j] = dp[i - nd[slink[j]].len - diff[j]];
            if (diff[j] == diff[nd[j].fa]) g[j] = min(g[j], g[nd[j].fa]);
            dp[i] = min(dp[i], g[j] + 1);
        }
    }
    printf("%d", dp[strlen(op + 1)]);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值