KMP,Manacher,Trie习题处理
KMP
Compress Words
题意:给出n个字符串,将他们合并,有相同前后缀得要省略,比如"ababa"和"bac"合并,因为"ababa"有一个后缀是"ba",“bac"有一个前缀"ba”,因为相同所以省略一个,最终就是"abababac"也就是"ababac"
暴力思路
暴力思路很简单就是假如我们现在只考虑合并a,b两个字符串,a的长度为n,b的长度为m,假设n>m(方便讨论),我们就去看a的后缀是否存在b得最长前缀也就是字符串b中长度为m的前缀,如果不存在那我们就看a中是否存在b长度为m-1的前缀,以此内推,如果到最后连长度为1的前缀都没有那就直接合并就行了,那如何确定a是否存在b的前缀呢,那最简单的就是一个一个去暴力匹配,举个例子,字符串a:“cdabcababab”,b:“abaaba”,长度分别为n,m
我们先看a的后缀是否存在b长度为m的前缀(也就是b本身),如何确定就是一个一个去匹配,因为对于a中[1,n-m]都是没有意义的起始,因为匹配不到哪里去就是图中"cdabc"那一截,因为b最长就m你a最多就重合m个,所以下标我是从n-m+1那里开始的
我们匹配到第4位失败发现失败了,那就说明a的后缀不存在b长度为m的前缀,那就去看是否存在长度为m-1的前缀,对应到图其实就是字符串b往右移动一位,然后继续一位一位匹配
我们发现第一位就匹配失败了,然后既然长度为m-1长度前缀失败了,那就去看长度为m-2以此类推,那这样移动是最多移动m次,但每次匹配也是匹配m位,这样就是O(m2),肯定超时
那知道KMP的话就能想到,我能不能匹配失败后不是移动一位,而是根据已知信息多移动几位呢,因为匹配长度为m的前缀的时候,前三位是匹配是成功的,也就说a[1]=b[1]、a[2]=b[2]、a[3]=b[3],又因为b[1]!=b[2],所以a[2]!=b[1],也就说根据已知信息我们可以推断出
移动一位一定是失败的,所以可以多移动几位,这个思路就是KMP的思路也不想再说一遍了,不理解的去看我关于KMP的博客就行
那所以我们这里匹配的时候直接套KMP板子就行
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N =1e6+5;
char ans[N],s[N];
int ne[N];
void getNext(string s){//求Next数组
int n=s.size();
for(int i=2,j=0;i<=n;i++){
while(j&&s[i]!=s[j+1])j=ne[j];
if(s[i]==s[j+1]) j++;
ne[i]=j;
}
return ;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n;
cin>>n;
cin>>ans+1;
for(int i=1;i<n;i++){
cin>>s+1;
getNext(s);
int lena=strlen(ans+1);
int lenb=strlen(s+1);
int k=0;
for(int j=max(lena-lenb,0)+1;j<=lena;j++){//j就是枚举a的指针,k是b的,但是匹配的时候是用a[j]和b[k+1]匹配,k也 //k也代表当前成功匹配的个数
while(k&&ans[j]!=s[k+1])k=ne[k];
if(ans[j]==s[k+1])k++;
}
int cou=lena+1;
for(int j=k+1;j<=lenb;j++){
ans[cou++]=s[j];
}
}
cout<<ans+1;
return 0;
}
Manacher
[THUPC2018]绿绿和串串
题意:我们定义翻转的操作:把一个串以最后一个字符作对称轴进行翻转复制。形式化地描述就是,如果他翻转的串为 R R R,那么他会将前 ∣ R ∣ − 1 \left| R\right|-1 ∣R∣−1 个字符倒序排列后,插入到串的最后。
举例而言,串abcd
进行翻转操作后,将得到abcdcba
;串qw
连续进行
2
2
2 次翻转操作后,将得到qwqwq
;串z
无论进行多少次翻转操作,都不会被改变。然后问你一个字符串的哪些前缀可以通过翻转得到原串,输出对应前缀长度即可
首先,我们可以知道对一个字符串翻转,那么这个字符串肯定是回文的,并且以字符串最后一个字符为回文中心,比如abc翻转就是abcba,以c为回文中心
那这个题的思路是什么呢,就比如长度为i的前缀是否可以通过翻转变成原串该如何判断?我们先去看以i为中心翻转是什么结果
以i为中心翻转对应的结果就是蓝色区间+红色区间,那如果翻转后的子串能得到i其实也就是说翻转后的子串中[1,n]区间要和原串一样即可,因为区间1就是区间3,所以只需要区间2=区间4就行,如果区间2=区间4说明什么呢,因为区间4是翻转得来的,所以区间4一定=区间6,那其实也就是说区间6+区间4这一段是以i为中心的回文串,那再想一下Manacher的回文半径,是不是就是说如果以i为中心的回文半径>=n-i那就可以通过一次翻转得到原串
上面说的一次翻转的情况,对于能通过翻转得到原串的点我们打上标记,那对于需要多次翻转才能形成原串的该如何处理呢,只需要判断一次翻转后的末尾是否被打上标记即可,因为我们对第i位打上标记就是说以i为中心翻转可以形成原串,那既然我翻转一次的结尾被打上标记,是不是就是说我翻转一次后再翻转也能形成原串,思路就说完了
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N =5e6+5;
char str[N],tmp[N<<1];
int Len[N<<1];
int vis[N<<1];
int init(char *s){//模板
int cou=0,len=strlen(s);
tmp[cou++]='$';
for(int i=0;i<len;i++){
tmp[cou++]='#';
tmp[cou++]=s[i];
}
tmp[cou++]='#';
tmp[cou++]='%';
return 2*len+1;
}
int Manacher(char *s){//模板
int len=init(s);
int mx=0,p=0;
for(int i=1;i<=len;i++){
if(i<mx) Len[i]=min(mx-i,Len[2*p-i]);
else Len[i]=1;
while(tmp[i-Len[i]]==tmp[i+Len[i]]) Len[i]++;
if(i+Len[i]>mx){
mx=i+Len[i];
p=i;
}
}
return len;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int t;
cin>>t;
while(t--){
cin>>str;
memset(Len,0,sizeof(Len));
memset(vis,0,sizeof(vis));
int len=Manacher(str);//直接套模板
for(int i=len;i>0;i--){//一定要递减,因为我们的先给能一次翻转成功的打上标记,这样两次三次的才能判断出来,一次翻转能 //成功的子串一定比同性质的两次三次长
if(i+Len[i]-1==len)vis[i]=1;//如果一次翻转可以成功
else if(vis[i+Len[i]-2]&&i==Len[i])vis[i]=1;//如果翻转后的末尾被打上标记并且他是前缀,那么也可以
}
for(int i=1;i<=len;i++){//输出
if(vis[i]&&tmp[i]>='a'&&tmp[i]<='z')cout<<i/2<<' ';//因为构造的字符串存在#这些所以要判断一下
}
cout<<endl;
}
return 0;
}