回文树学习
https://oi-wiki.org/string/pam/ 回文树讲解
https://vjudge.net/contest/113534#problem/L 题目链接
len[i]:节点i的回文串的长度
next[i][c]:节点i的回文串在两边添加字符c以后变成的回文串的编号(和字典树的next指针类似)
fail[i]:类似于AC自动机的fail指针,指向失配后需要跳转到的节点(即为i的最长回文后缀且不为i)
cnt[i]:节点i表示的回文串在S中出现的次数(建树时求出的不是完全的,count()加上子节点以后才是正确的)
num[i]:表示以节点i表示的最长回文串的最右端点为回文串结尾的回文串个数(也就是fail指针路径的深度)
last:指向以字符串中上一个位置结尾的回文串
cur: 指向由next构成的树中当前回文串的父亲节点(即当前回文串是cur左右两边各拓展一个字符得来)
S[i]:第i次添加的字符
p:添加的节点个数
n:添加的字符个数
举个例子,比如当前串为cbbabb,那么最长的回文子串为bbabb,我们要加入字符a,那么a先与bbabb左边的字符(c)比较,发现不一样,于是跳转到bbabb的最长的后缀回文子串,也就是bb,继续匹配。发现bb左边的字符为a,和我们要加入的字符一样,所以新加入的len就是len(bb)+2=4,也就是子串abba。
分析
假设现在我们有串S=’abbaabba’。
先建两个树根,偶数长度的根为0,奇数长度的根为1。注意,我们将len[0]设为0,但将len[1]设为-1。将p、n、last均初始化为0。将S[0]设为-1,这是放一个字符集中没有的字符,减少特判。然后,我们将fail[0]指向1。
举个例子,若有串S=’abbaabba’。
首先我们添加第一个字符’a’,S[++ n] = ‘a’,然后将cur赋为get_fail(last)。其中的get_fail函数就是让找到第一个使得S[n-len[last]-1]==S[n]的last。注意,此处的n不为get_fail中的参数,依然为添加的字符个数。这样做的话,我们就可以通过fail构成的失配链找到last的所有回文后缀(包括它自己),然后从长到短依次判断此后缀的前一位是否等于S[n],等于则表明可以构成一个回文串。
判断此时next[cur][‘a’]是否已经有后继,如果next[cur][‘a’]没有后继,我们就进行如下的步骤:
新建节点(节点数p++,且之后p=3),并让now等于新节点的编号(now=2),则len[now]=len[cur]+2(每一个回文串的长度总是在其最长子回文串的基础上在两边加上两个相同的字符构成的,所以是+2,同时体现出我们让len[1]=-1的优势,一个字符自成一个奇回文串时回文串的长度为(-1)+2=1)。
然后我们让fail[now]=next[get_fail ( fail[cur] )][‘a’],即得到fail[now](此时为fail[2] = 0)。计算get_fail(fail[cur])是为了求出在cur的所有回文后缀中(不包括它自己,因为和AC自动机一样,fail[now]不能指向now),满足前一位等于S[n]的后缀,我们即可用它来往两边拓展一格,即为now的最长回文后缀(不包括它自己)。然后next[cur][‘a’] = now。
当上面步骤完成后我们让last = next[cur][‘a’](不管next[cur][‘a’]是否有后继),然后cnt[last] ++。
如上述方法插完所有字符后,我们将节点x在fail指针树中将自己的cnt累加给父亲,从叶子开始倒着加,最后就能得到串S中出现的每一个本质不同(两个串长度不同或者长度相同且至少有一个字符不同便是本质不同可以使用burnside引理)回文串的个数。
构造回文树需要的空间复杂度为O(N∗字符集大小),时间复杂度为O(N∗log(字符集大小),这个时间复杂度比较神奇。如果空间需求太大,可以改成邻接表的形式存储,不过相应的要牺牲一些时间。
功能
求串S前缀0~i内本质不同回文串的个数
求串S内每一个本质不同回文串出现的次数
求串S内回文串的个数(其实就是1和2结合起来)
求以下标i结尾的回文串的个数
例题
【JZOJ3654】【APIO2014】回文串(palindrome)
Problem
考虑一个只包含小写拉丁字母的符串s。我们定义s的一个子串t的“出现值”为 t在s中的出现次数乘以t的长度。 请你求出s的所有回文子串中的最大出现值。
Hint
#define N 300010//绝对要多开一点,因为回文树会多开两个节点,我就被这坑过一次 #define ll long long int i,nn; ll ans; char s[N]; struct Palindrome_Automaton//回文自动机 { int i,len[N],next[N][26],fail[N],cnt[N],last,cur,S[N],p,n,num[N]; int newnode(int l)//新建节点 { for(int i = 0; i <= 25; i++)next[p][i]=0;//新建的节点为p,先消除它的子节点 cnt[p]=0; num[p]=0; len[p]=l; return p++;//勿打成++p,因为此节点为p,我们应返回p } inline void init()//初始化 { p=n=last=0; newnode(0); newnode(-1); S[0]=-1; fail[0]=1; } int get_fail(int x) { while(S[n-len[x]-1]!=S[n])x=fail[x]; return x; } inline void add(int c,int pos)//插字符 { c-='a'; S[++n]=c; int cur=get_fail(last); if(!next[cur][c]) { int now=newnode(len[cur]+2);
//可以在这记录该节点回文串最后一个字母的位置if(len[now]>1)pos[now]=pos; fail[now]=next[get_fail(fail[cur])][c]; next[cur][c]=now; num[now]=num[fail[now]]+1; } last=next[cur][c]; cnt[last]++; } void cont()//统计本质相同的回文串的出现次数 { //逆序累加,保证每个点都会比它的父亲节点先算完,于是父亲节点能加到所有子孙 for(int i = p - 1; i >= 0; i--) cnt[fail[i]]+=cnt[i]; } }run; int main() { scanf("%s",s); run.init(); nn=strlen(s)-1;//千万要先把这个记录下来,因为求长度的时间复杂度是O(n)——直接扫一遍,碰到结束符才停止,我一开始把它直接塞进下方循环的nn里,就T了一遍 for(int i = 0; i <= nn; i++)run.add(s[i],i); run.cont(); for(int i = 2; i <= run.p - 1; i++) ans=max(ans,(ll)run.len[i]*run.cnt[i]); printf("%lld",ans); }
https://nanti.jisuanke.com/t/41389
求字符串里面所有的回文串中不同字母个数的总和。
#define N 300010//绝对要多开一点,因为回文树会多开两个节点,我就被这坑过一次 #define ll long long int i,nn; ll ans; char s[N]; int vis[30]; struct Palindrome_Automaton//回文自动机 { int i,len[N],next[N][26],fail[N],cnt[N],last,cur,S[N],p,n,num[N]; int newnode(int l)//新建节点 { for(int i = 0; i <= 25; i++)next[p][i]=0;//新建的节点为p,先消除它的子节点 cnt[p]=0; num[p]=0; len[p]=l; return p++;//勿打成++p,因为此节点为p,我们应返回p } inline void init()//初始化 { p=n=last=0; newnode(0); newnode(-1); S[0]=-1;//必须是字符集中没有的字符。 fail[0]=1; } int get_fail(int x) { while(S[n-len[x]-1]!=S[n])x=fail[x]; return x; } inline void add(int c,int pos)//插字符 { c-='a'; S[++n]=c; int cur=get_fail(last); if(!next[cur][c]) { int now=newnode(len[cur]+2); fail[now]=next[get_fail(fail[cur])][c]; next[cur][c]=now; num[now]=num[fail[now]]+1; } last=next[cur][c]; cnt[last]++; } void cont()//统计本质相同的回文串的出现次数 { //逆序累加,保证每个点都会比它的父亲节点先算完,于是父亲节点能加到所有子孙 for(int i = p - 1; i >= 0; i--) cnt[fail[i]]+=cnt[i]; } }run; void dfs(int x, int nu) { for(int i = 0; i < 26; i++) { int pm=nu; if(run.next[x][i]) { vis[i]++; if(vis[i] == 1) pm = nu + 1; ans += (ll)pm * run.cnt[run.next[x][i]]; dfs(run.next[x][i], pm); vis[i]--; } } } int main() { scanf("%s",s); run.init(); nn=strlen(s)-1;//千万要先把这个记录下来,因为求长度的时间复杂度是O(n)——直接扫一遍,碰到结束符才停止,我一开始把它直接塞进下方循环的nn里,就T了一遍 for(int i = 0; i <= nn; i++)run.add(s[i],i); run.cont(); ans=0; memset(vis, 0, sizeof(vis)); dfs(0,0); memset(vis, 0, sizeof(vis)); dfs(1,0); printf("%lld",ans); }
回文自动机中的节点个数就表示本质不同的回文串的个数。只要在减去2就行(根节点有两个,一个是表示偶数回文串的,一个是表示奇数回文串的)。
如果是求该字符串所有的回文串的个数(即位置不同也算不同)。那么就是最后所有的cnt数组相加可得。
https://vjudge.net/contest/113534#overview
HDU 5157求出不相交的回文串有多少对
首先,我们反着添加一下,让add返回当前位置的回文串个数num,顺便求一个后缀和,这样就相当于处理出了每个位置右边有多少个回文串,然后我们正着添加一下,这样左边的和右边的乘一下就是答案,求和就ok、
#define LL long long #define ULL unsigned long long #define INF 0x3f3f3f3f3f3f3f3f #define mm(a,b) memset(a,b,sizeof(a)) #define PP puts("*********************"); template<class T> T f_abs(T a){ return a > 0 ? a : -a; } template<class T> T gcd(T a, T b){ return b ? gcd(b, a%b) : a; } template<class T> T lcm(T a,T b){return a/gcd(a,b)*b;} // 0x3f3f3f3f3f3f3f3f const int MAXN = 100005, SIZE = 26; struct Palindromic_Tree { int next[MAXN][SIZE];//next指针 int fail[MAXN];//fail指针 int cnt[MAXN];//表示节点i表示的回文串的个数(建树时求出的不是完全的,最后Count()函数跑一遍以后才是正确的) int num[MAXN];//表示以节点i表示的最长回文串的最右端点为回文串结尾的回文串个数 int len[MAXN];//len[i]表示节点i表示的回文串的长度(一个节点表示一个回文串) int S[MAXN];//存放添加的字符 int last;//指向上一个字符所在的节点,方便下一次add int n;//字符数组指针 int p;//节点指针 int NewNode(int L) {//新建节点 for(int i = 0; i < SIZE; ++i) next[p][i] = 0; cnt[p] = num[p] = 0; len[p] = L; return p++; } void Init() {//初始化 p = n = 0; NewNode(0); NewNode(-1); last = 0; S[n] = -1;//开头放一个字符集中没有的字符,减少特判 fail[0] = 1; } int GetFail(int x) {//和KMP一样,失配后找一个尽量最长的 while(S[n - len[x] - 1] != S[n]) x = fail[x]; return x; } int Add(int c) { S[++n] = c; int cur = GetFail(last);//通过上一个回文串找这个回文串的匹配位置 if(!next[cur][c]) {//如果这个回文串没有出现过,说明出现了一个新的本质不同的回文串 int now = NewNode(len[cur] + 2);//新建节点 fail[now] = next[GetFail(fail[cur])][c];//和AC自动机一样建立fail指针,以便失配后跳转 next[cur][c] = now; num[now] = num[fail[now]] + 1; } last = next[cur][c]; cnt[last]++; return num[last]; } void Count() { for(int i = p - 1; i >= 0; --i) cnt[fail[i]] += cnt[i]; //父亲累加儿子的cnt,因为如果fail[v]=u,则u一定是v的子回文串! } }tree; char str[MAXN]; LL sum[MAXN]; int main(){ while(~scanf("%s",str+1)){ int len=strlen(str+1); tree.Init(); sum[0]=0; for(int i=1;i<=len;i++) sum[i]=tree.Add(str[i]-'a'); for(int i=1;i<=len;i++) sum[i]+=sum[i-1]; tree.Init(); LL ans=0; for(int i=len;i>=1;i--) ans+=(LL)tree.Add(str[i]-'a')*sum[i-1]; printf("%lld\n",ans); } return 0; }
cf 17E
跟上面一起基本一样,不过是反过来求相交 的。不过这个模板是用vector的。
/* @Author: Top_Spirit @Language: C++ */ #include <bits/stdc++.h> using namespace std ; typedef unsigned long long ull ; typedef long long ll ; typedef pair < ll, ll > P ; const int Maxn = 2e6 + 10 ; const int INF = 0x3f3f3f3f ; const double PI = acos(-1.0) ; const ull seed = 133 ; const int Mod = 51123987 ; struct pilindromic_tree{ // int Next[Maxn][26] ; vector < P > Next[Maxn] ; int len[Maxn] ; int cnt[Maxn] ; int num[Maxn] ; int fail[Maxn] ; int s[Maxn] ; int last ; int n, p ; int newNode (int k){ // for (int i = 0; i < 26; i++) Next[p][i] = 0 ; Next[p].clear() ; cnt[p] = 0 ; num[p] = 0 ; len[p] = k ; return p++ ; } void init(){ p = 0; last = 0 ; n = 0 ; s[n] = -1 ; fail[0] = 1 ; newNode(0) ; newNode(-1) ; } int get_fail(int x){ while (s[n - len[x] - 1] != s[n]) x = fail[x] ; return x ; } int findNext(int a, int b){ for (int i = 0; i < Next[a].size(); i++){ if (Next[a][i].first == b) return Next[a][i].second ; } return 0 ; } void add(int c){ c -= 'a' ; s[++n] = c ; int cur = get_fail(last) ; if (!findNext(cur,c)){ int Now = newNode(len[cur] + 2) ; // fail[Now] = Next[get_fail(fail[cur])][c] ; fail[Now] = findNext(get_fail(fail[cur]), c) ; // Next[cur][c] = Now ; Next[cur].push_back(make_pair(c, Now)) ; num[Now] = num[fail[Now]] + 1; } // last = Next[cur][c] ; last = findNext(cur, c) ; cnt[last]++ ; } void Count(){ for (int i = p - 1; i >= 0; i--){ cnt[fail[i]] += cnt[i] ; } } }Tree; int n ; char str[Maxn] ; ll pre[Maxn] ; int main (){ cin >> n >> str + 1 ; Tree.init() ; pre[0] = 0 ; for (int i = 1; i <= n; i++) { Tree.add(str[i]) ; pre[i] = (Tree.num[Tree.last] + pre[i - 1]) % Mod ; } ll ans = (pre[n] * (pre[n] - 1) / 2) % Mod ; Tree.init() ; ll ans1 = 0 ; for (int i = n; i >= 1; i--){ Tree.add(str[i]) ; ans1 = (ans1 + Tree.num[Tree.last] * pre[i - 1]) % Mod ; } ans = (ans - ans1 + Mod) % Mod ; cout << ans << endl ; return 0 ; }
HDU - 5394 Trie in Tina Town
题目:按要求建一棵树,每个节点有一个字符,求树上的价值=sum(len*cnt),len为回文串长度,cnt为该回文在树上出现了多少次。一共只有a,b,c,d这几种字符,回文串要保证是从上到下的。
思路:把last改成数组就可以了,保存每一层的last
然后这一题的串是在树上,所以字符串是按深度一个一个来。
#pragma comment(linker, "/STACK:1024000000,1024000000")#define LL long long #define ULL unsigned long long #define INF 0x3f3f3f3f3f3f3f3f #define mm(a,b) memset(a,b,sizeof(a)) #define PP puts("*********************"); template<class T> T f_abs(T a){ return a > 0 ? a : -a; } template<class T> T gcd(T a, T b){ return b ? gcd(b, a%b) : a; } template<class T> T lcm(T a,T b){return a/gcd(a,b)*b;} // 0x3f3f3f3f3f3f3f3f const int MAXN = 2e6+50, SIZE = 4; struct Palindromic_Tree { int next[MAXN][SIZE];//next指针 int fail[MAXN];//fail指针 LL cnt[MAXN]; int len[MAXN];//len[i]表示节点i表示的回文串的长度(一个节点表示一个回文串) int S[MAXN];//存放添加的字符 int last[MAXN];//指向上一个字符所在的节点,方便下一次add int n;//字符数组指针 int p;//节点指针 int NewNode(int L) {//新建节点 for(int i = 0; i < SIZE; ++i) next[p][i] = 0; cnt[p] = 0; len[p] = L; return p++; } void Init() {//初始化 p = n = 0; NewNode(0); NewNode(-1); last[0]=0; S[n] = -1;//开头放一个字符集中没有的字符,减少特判 fail[0] = 1; } int GetFail(int x) {//和KMP一样,失配后找一个尽量最长的 while(S[n - len[x] - 1] != S[n]) x = fail[x]; return x; } LL Add(int dep,int c) { S[n=dep] = c; int cur = GetFail(last[n-1]);//通过上一个回文串找这个回文串的匹配位置 if(!next[cur][c]) {//如果这个回文串没有出现过,说明出现了一个新的本质不同的回文串 int now = NewNode(len[cur] + 2);//新建节点 fail[now] = next[GetFail(fail[cur])][c];//和AC自动机一样建立fail指针,以便失配后跳转 next[cur][c] = now; cnt[now]=cnt[fail[now]]+len[now]; } last[n]= next[cur][c]; return cnt[last[n]]; } void Count() { for(int i = p - 1; i >= 0; --i) cnt[fail[i]] += cnt[i]; //父亲累加儿子的cnt,因为如果fail[v]=u,则u一定是v的子回文串! } }tree; char str[MAXN]; int head[MAXN],v[MAXN],_next[MAXN],tot; LL ans; void dfs(int dep,int u){ for(int i=head[u];i!=-1;i=_next[i]){ ans+=tree.Add(dep,str[v[i]]-'a'); dfs(dep+1,v[i]); } } int main(){ int T,n,x; scanf("%d",&T); while(T--){ scanf("%d",&n); tree.Init(); mm(head,-1);tot=0; for(int i=1;i<=n;i++){ scanf("%s%d",str+i,&x); v[tot]=i; _next[tot]=head[x]; head[x]=tot++; } ans=0; dfs(1,0); printf("%lld\n",ans); } return 0; }
双向回文串的模板
HDU 5421 Victor and String
维护一个可以两边都可以添加的回文自动机,那么我们首先要解决这么几个问题:
last指针的位置:我们开两个变量存,代表两边构造到了什么位置
fail的寻找,由于我们分成了左右,相当于是从中间向左向右找,这个时候就需要一个辅助变量tot来记录构造到的位置,方便我们进行寻找。
左右两边的影响:在一边添加实际上会影响到另一边的指针,我们利用上面的tot变量判断左右两边是不死连在一起。
#define pr pair<int,int> #define fi first #define se second #define mp make_pair #define ll long long using namespace std; const int MAXN = 1e5+10 ; const int N = 26 ; struct Palindromic_Tree { int next[MAXN*2][N] ;//next指针,next指针和字典树类似,指向的串为当前串两端加上同一个字符构成 int fail[MAXN*2] ;//fail指针,失配后跳转到fail指针指向的节点 int len[MAXN*2] ;//len[i]表示节点i表示的回文串的长度 int S[MAXN*2] ;//存放添加的字符 int tot[2]; int num[MAXN*2]; int last[2] ;//指向上一个字符所在的节点,方便下一次add int n ;//字符数组指针 int p ;//节点指针 int newnode ( int l) {//新建节点 for ( int i = 0 ; i < N ; ++ i ) next[p][i] = 0 ; len[p] = l ; num[p] = 0; return p ++ ; } void init () {//初始化 p = 0 ; newnode ( 0 ) ; newnode ( -1 ) ; last[0] = last[1] = 0 ; tot[0] = MAXN - 5; tot[1] = MAXN - 6; n = 0 ; //S[n] = -1 ;//开头放一个字符集中没有的字符,减少特判 fail[0] = 1 ; } int get_fail ( int x ,int k) {//和KMP一样,失配后找一个尽量最长的 S[tot[0]-1]=-1; S[tot[1]+1]=-1; while(S[tot[k]]!=S[tot[k]+(k?-1:1)*(len[x]+1)]) x=fail[x]; return x; } int add ( int x ,int k) { x-='a'; S[tot[k]+=(k?1:-1)]=x; int cur=get_fail(last[k],k); if(!(last[k]=next[cur][x])) { int now=newnode(len[cur]+2); fail[now]=next[get_fail(fail[cur],k)][x]; next[cur][x]=now; num[now]=num[fail[now]]+1; last[k]=now; if(len[last[k]]==tot[1]-tot[0]+1) last[k^1]=last[k]; } return num[last[k]]; } }pam ; char buf[MAXN]; char s[10]; int main() { int q; while(scanf("%d",&q) != EOF) { pam.init(); int op; ll ans = 0; for(int i = 0;i<q;i++) { scanf("%d",&op); if(op == 1) { scanf("%s",s); ans += pam.add(s[0],0); } else if(op == 2) { scanf("%s",s); ans += pam.add(s[0],1); } else if(op == 3) { printf("%d\n",pam.p - 2); } else { printf("%lld\n",ans); } } } return 0; }
Palindromic Substring
题目:a-z都表示一个0~25的整数,一个长度为len的回文串的价值由前(len+1)/2个字符组成的26进制整数表示,
给出一个字符串,询问第k小的回文子串的价值(对于每个人都要求一下)
思路:保存一下节点的价值
#define LL long long #define ULL unsigned long long #define INF 0x3f3f3f3f3f3f3f3f #define mm(a,b) memset(a,b,sizeof(a)) const LL MOD=777777777; const int MAXN = 100005, SIZE = 26; LL score[20][MAXN],pow26[MAXN],num[20][26]; LL ask[20]; char str[MAXN]; int n,m; struct Palindromic_Tree { int next[MAXN][SIZE];//next指针 int fail[MAXN];//fail指针 LL cnt[MAXN]; int len[MAXN];//len[i]表示节点i表示的回文串的长度 int S[MAXN];//存放添加的字符 S[MAXN << 1] int last;//指向上一个字符所在的节点,方便下一次add int n;//字符数组指针 //lbd, rbd int p;//节点指针 int NewNode(int l) {//新建节点 for(int i = 0; i < SIZE; ++i) next[p][i] = 0; cnt[p] = 0; len[p] = l; return p++; } void Init(/*int x*/) {//初始化 p = n = 0; NewNode(0); NewNode(-1); last = 0; S[n] = -1; fail[0] = 1; mm(score[0],0); mm(score[1],0); } int GetFail(int x/*, int d*/) {//和KMP一样,失配后找一个尽量最长的 while(S[n - len[x] - 1] != S[n]) x = fail[x]; return x; } void Add(int c/*, int d*/) {//d=0表示在首部添加字符,d=1表示在尾部添加字符 S[++n] = c; int cur = GetFail(last);//通过上一个回文串找这个回文串的匹配位置 if(!next[cur][c]) {//如果这个回文串没有出现过,说明出现了一个新的本质不同的回文串 int now = NewNode(len[cur] + 2);//新建节点 fail[now] = next[GetFail(fail[cur]/*, d*/)][c];//和AC自动机一样建立fail指针,以便失配后跳转 next[cur][c] = now; for(int i=0;i<m;i++) score[i][now]=(score[i][cur]+pow26[(len[now]-1)/2]*num[i][c]%MOD)%MOD; } last = next[cur][c]; cnt[last]++; } void Count() { for(int i = p - 1; i >= 0; --i) cnt[fail[i]] += cnt[i]; } }tree; int ans[MAXN],now; bool cmp(int x,int y){ return score[now][x]<score[now][y]; } void solve(){ int sz=tree.p; for(int i=0;i<m;i++){ now=i; for(int j=2;j<sz;j++) ans[j]=j; sort(ans+2,ans+sz,cmp); for(int j=2;j<sz;j++){ if(ask[i]>tree.cnt[ans[j]]) ask[i]-=tree.cnt[ans[j]]; else{ printf("%lld\n",score[i][ans[j]]); break; } } } } int main(){ int T; pow26[0]=1; for(int i=1;i<MAXN;i++) pow26[i]=pow26[i-1]*26%MOD; scanf("%d",&T); while(T--){ tree.Init(); scanf("%d%d",&n,&m); scanf("%s",str); for(int i=0;i<m;i++){ scanf("%lld",&ask[i]); for(int j=0;j<26;j++) scanf("%lld",&num[i][j]); } for(int i=0;i<n;i++) tree.Add(str[i]-'a'); tree.Count(); solve(); printf("\n"); } return 0; }
Gym 100543G Virus synthesis
题意
有AGTC四种字符,一开始有一个空串,每次操作,可以在首或尾加任意个字符,或者将已有字符镜面复制(左右两种复制方法),要求最少的操作步数使得得到给出的字符串
题解
首先,最后的串一定是从他的回文子串向两侧添加得到的
所以,只需要考虑构成回文串的最小操作数
设回文树中一节点 u, 其答案为 dp[u],长度为 len[u],其父亲为 v
那么,什么样的串才能得到回文串呢?
从它的回文子串得到
从它的非回文子串得到,只能通过向两侧补齐得到,最笨的方法
首先说从回文子串得到
若回文子串和本身同对称轴,那么对应的最优解就是PAM结点的父节点 v,那么,在 v 镜面对称之前,先在一侧加上一个字符,再对称,则可以得到 u
否则,若其不是 u 的一个长度小于 len[u]/2 的后缀回文串,只能通过暴力向两侧加,否则,将其补到一半,对称一下即可
可以发现,除了从 v 和 u 的后缀回文串得到,其他的得到方式都是暴力,而且,这两种方式最多会退化成暴力,所以,只考虑回文串之间的转移就行!!!
那么只要在回文树上 dp 就好了
若长度为奇数, dp[u]=dp[fail[u]]+len[u]-len[fail[u]]
若长度为偶数, dp[u]=min(dp[v]+1,dp[half[u]]+len[u]/2−len[half[u]]+1)
其中, half[u] 为结点 uu 的长度小于其一半的最长后缀回文串
#include<bits/stdc++.h> #define N 100010 #define INF 0x3f3f3f3f #define eps 1e-6 #define pi 3.141592653589793 #define mod 777777777 #define P 1000000007 #define LL long long #define pb push_back #define fi first #define se second #define cl clear #define si size #define lb lower_bound #define ub upper_bound #define bug(x) cerr<<#x<<" : "<<x<<endl #define mem(x,y) memset(x,0,sizeof(int)*(y+3)) #define sc(x) scanf("%d",&x) #define scc(x,y) scanf("%d%d",&x,&y) #define sccc(x,y,z) scanf("%d%d%d",&x,&y,&z) using namespace std; typedef pair<int,int> pp; char s[N]; int ans; int tot; struct PAM { int next[N][4] ;//next指针,next指针和字典树类似,指向的串为当前串两端加上同一个字符构成 int fail[N] ;//fail指针,失配后跳转到fail指针指向的节点 int cnt[N]; //表示节点i表示的回文串的个数(建树时求出的不是完全的,最后count()函数跑一遍以后才是正确的) int num[N] ; //表示以节点i表示的最长回文串的最右端点为回文串结尾的回文串个数(包括本身)。 int len[N] ;//len[i]表示节点i表示的回文串的长度 int s[N] ;//存放添加的字符 int half[N]; int dp[N]; int last ;//指向上一个字符所在的节点,方便下一次add int n ;//字符数组指针 int p ;//节点指针 //r为结尾的回文串的长度一定可以分成logn段等差数列 inline int newnode ( int l ) {//新建节点 for ( int i = 0 ; i < 4 ; ++ i ) next[p][i] = 0 ; cnt[p]= 0 ; num[p] = 0 ; len[p] = l ; return p ++ ; } inline void init () {//初始化 p = 0 ; newnode ( 0 ) ; newnode ( -1 ) ; last = 0 ; n = 0 ; s[n] = -1 ;//开头放一个字符集中没有的字符,减少特判 fail[0] = 1 ; half[0]=half[1]=1; } inline int get_fail ( int x ) {//和KMP一样,失配后找一个尽量最长的 while ( s[n - len[x] - 1] != s[n] ) x = fail[x] ; return x ; } inline void add ( int c ) { // c -= 'a' ; s[++ n] = c ; int cur = get_fail ( last ) ;//通过上一个回文串找这个回文串的匹配位置 if ( !next[cur][c] ) {//如果这个回文串没有出现过,说明出现了一个新的本质不同的回文串 int now = newnode ( len[cur] + 2 ) ;//新建节点 fail[now] = next[get_fail ( fail[cur] )][c] ; next[cur][c] = now ; if (len[now]==1) half[now]=0;else{ int pos=half[cur]; while ( s[n - len[pos] - 1] != s[n]||len[pos]+2>len[now]/2 ) pos = fail[pos] ; half[now]=next[pos][c]; } if (len[now]&1) dp[now]=dp[fail[now]]+len[now]-len[fail[now]]; else if (len[now]<=2) dp[now]=len[now]; else dp[now]=min(dp[cur]+1,dp[half[now]]+len[now]/2-len[half[now]]+1); } last = next[cur][c] ; ans=min(ans,dp[last]+tot-len[last]); } }A; int main(int argc, char const *argv[]) { int T; sc(T); while(T--){ scanf("%s",s+1); int n=strlen(s+1); tot=n; A.init(); ans=1e9; for(int i=1;i<=n;i++) { int x; if (s[i]=='A') x=0;else if (s[i]=='G') x=1;else if (s[i]=='C') x=2;else x=3; A.add(x); } printf("%d\n",ans); } return 0; }