码题集OJ-安全验证 (matiji.net)
题面:
题意:给定一个字符串s,找出最长的子串t,满足前后缀都为t,且中间段还出现了t(中间段:除了第一个与最后一个字符)
第一个样例就是fix出现了在前缀,也出现了在后缀,中间段也能找到fix,且fix为最长满足要求的字串,故答案输出fix
思路1:
1.一个朴素的做法就是,从大到小枚举字符串长度,设当前枚举的字符串长度为len,则前缀字符串为t1(截取区间[1,len]) , 后缀字符串为t2 ( 截取区间[n-len+1,n],n为字符串长度),判断t1是否等于t2,不等于的话减小枚举长度,等于的话,再判断中间段是否出现了t1(t2,此时t1等于t2)
2.判断中间段是否出现了t1,可以暴力判断,但是此时时间复杂为o(n*m),可以用find函数优化(java/c++/pythoon,find查找字符串时都是o(n)的时间复杂度),此时的总时间复杂度会降为o(n^2)
3.n^2的时间复杂度,理论上是不可以过的,不过这里可以试着先提交,提交发现你会发现此时就可以Ac了,先贴上一个代码吧,
AC_Code:C++(时间复杂度n^2)(理论会上会tle,但是可以过码题集上的所有数据)
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
#include <queue>
#include<stack>
#include<cmath>
#include <unordered_set>
#include <unordered_map>
#include<set>
#include <map>
using namespace std;
typedef long long LL;
typedef pair<int,int>PII;
#define x first
#define y second
#define ls u<<1
#define rs u<<1|1
#define all(ss) ss.begin(),ss.end()
int const mod1=998244353;
int const mod2=1e9+7;
int const N=2e5+7;
int const INF=0x3f3f3f3f;
int T;
int n,m;
string s;
//检查前后缀是否相等
bool check(int len){
for(int i=1,j=n-len+1;i<=len;i++,j++)
if(s[i]!=s[j]) return false;
return true;
}
void solve(){
cin>>s;
n=s.size();
s=' '+s; //让下标从1开始
string mid; //除去首尾字符后剩下的中间字符
for(int i=2;i<=n-1;i++) mid+=s[i];
int len=n-2;
string ans; //答案
for(int i=1;i<=len;i++) ans+=s[i];
while(len>0){ //长度小于2不会进入while循环
if(!check(len)){ //前缀不等于后缀
ans.pop_back();
len--;
continue;
}
if((int)mid.find(ans)!=-1){ //查找中间是否有ans字符串
cout<<ans<<endl;
return; //找到了直接输出答案,结束程序
}
ans.pop_back();
len--;
}
puts("No");
}
void init(){
}
int main()
{
//std::ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
T=1;
//cin>>T;
//scanf("%d",&T);
init();
while(T--){
solve();
}
return 0;
}
对find函数感兴趣,看这里c++的find函数介绍
思路2:hash字符串+二分优化
1.换过角度思考,我们可以预处理出,前后缀相等的所有合法的长度,此时可以用hash字符串,o(n)的时间复杂度可以预处理出所有前后缀相等的长度
2.有了所有前后缀相等的合法长度,此时一个暴力的做法就是从大到小枚举长度,判断是否合法
3.但是这个方法理论上又是会tle的
一个重要的性质:再所有合法的长度中,如果存在一个字符串t1,其中间段能找到一个与t1相等的字符串p1,那么一个比t1短的字符串t2,中间段也一定能找到一个与t2相等的字符串p2。那么此时我们就可以考虑二分所有的合法长度了。
此处敲三下黑板:红字部分是重点,没懂,多看几十遍
AC_Code:C++(时间复杂度nlogn)
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
int const N=1e6+7;
int const base=131,mod=1e9+7;
int T;
int n,m;
vector<int>a; //存储前后缀相等的长度
char s[N];
LL hash1[N],p[N]; //hash1为hash数组,p为进制数
//模板
LL get(int l,int r){
return ((hash1[r]-hash1[l-1]*p[r-l+1])%mod+mod)%mod;
}
//检查当前长度是否合法
bool check(int len){
LL target=get(1,len);
for(int i=2;i+len-1<=n-1;i++)
if(get(i,i+len-1)==target)
return true;
return false;
}
void solve(){
scanf("%s",s+1);
n=strlen(s+1); //有效字符[1,n]
//hash字符串
p[0]=1;
for(int i=1;i<=n;i++){
hash1[i]=(hash1[i-1]*base+s[i])%mod;
p[i]=(p[i-1]*base)%mod;
}
//预处理出前缀等于后缀的所有长度
for(int len=1;len<=n-2;len++)
if(get(1,len)==get(n-len+1,n))
a.push_back(len);
//二分最大长度
int max_len=0;
int l=0,r=a.size()-1;
while(l<=r){
int mid=(l+r)>>1;
if(check(a[mid])) max_len=a[mid],l=mid+1;
else r=mid-1;
}
//输出答案
if(max_len) for(int i=1;i<=max_len;i++) putchar(s[i]);
else puts("No");
}
void init(){
}
int main()
{
//std::ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
T=1;
//cin>>T;
//scanf("%d",&T);
init();
while(T--){
solve();
}
return 0;
}
思路3:扩展KMP算法(又称z函数)
z函数:z[i]表示以i开头的后缀与前缀相等的最长长度
这里默认下标从0开始,z[0]默认为0
z(abcabcabc)=[0,0,0,6,0,0,3,0,0]
z(aacaa)=[0,1,0,2,1]
z(aacaadaa)=[0,1,0,2,1,0,2,1]
想彻底学会z函数的请到这里Z 函数(扩展 KMP) - OI Wiki
z函数模板:时间复杂度是o(n)的
vector<int> z_function(string s) {
int n = (int)s.length();
vector<int> z(n);
for (int i = 1, l = 0, r = 0; i < n; ++i) {
if (i <= r && z[i - l] < r - i + 1) {
z[i] = z[i - l];
} else {
z[i] = max(0, r - i + 1);
while (i + z[i] < n && s[z[i]] == s[i + z[i]]) ++z[i];
}
if (i + z[i] - 1 > r) l = i, r = i + z[i] - 1;
}
return z;
}
1.z函数可以直接获得从任意一个字符开始与前缀相同的最长长度值。
2.先预处理出来z函数,找出z函数数组的最大值max_len(其实就是与前缀相同的最大值),那么所有的前后缀相等的情况一定被这个最大值所包含,那么只要你的长度小于max_len,那么中间就一定会出现子串t。
3.还有一种情况就是长度等于max_len的情况也可以,如s="aacaadaa",此时max_len=2,最长前后缀为aa,但是中间也出现aa那么答案就是aa了
4.s="abcabcabc",此时前后缀相等的情况有abc,abcabc两种情况,max_len=6,此时的abcabc要么包含前缀,要么包含后缀都是不合法的,中间没有出现abcabc,故答案只能为abc
5.红色部分的字是关键,看不懂,就多看几遍,多看几遍不懂,记得看z函数的定义,
AC_Code:C++(时间复杂度n)
#include <iostream>
#include<vector>
using namespace std;
int T;
int n,m;
//模板
vector<int> z_function(string s) {
int n = (int)s.length();
vector<int> z(n);
for (int i = 1, l = 0, r = 0; i < n; ++i) {
if (i <= r && z[i - l] < r - i + 1) {
z[i] = z[i - l];
} else {
z[i] = max(0, r - i + 1);
while (i + z[i] < n && s[z[i]] == s[i + z[i]]) ++z[i];
}
if (i + z[i] - 1 > r) l = i, r = i + z[i] - 1;
}
return z;
}
void solve(){
string s; cin>>s;
n=s.size();
vector<int>z=z_function(s);
//可以打印观察z函数
// for(int i=0;i<n;i++) cout<<z[i]<<' ';
// cout<<endl;
//max_len最大等于前缀的长度
//max_cnt最大等于前缀的长度的个数
int max_len=0,max_cnt=0;
int ans=0; //前后缀相等的最大长度
for(int i=0;i<n;i++){
max_len=max(max_len,z[i]);
if(i+z[i]==n) //前缀等于后缀
ans=max(ans,z[i]);
}
for(int i=0;i<n;i++) //统计等于最大前缀出现的次数
max_cnt+=max_len==z[i];
if(ans&&ans==max_len&&max_cnt>=2) { //存在前后缀相等,且中间段也出现过的情况
for(int i=0;i<ans;i++) putchar(s[i]);
}
else{
ans=0;
for(int i=0;i<n;i++)
if(max_len!=z[i]&&i+z[i]==n) //除了max_len,找最大值
ans=max(ans,z[i]);
if(ans) for(int i=0;i<ans;i++) putchar(s[i]);
else puts("No");
}
}
void init(){
}
int main()
{
//std::ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
T=1;
//cin>>T;
//scanf("%d",&T);
init();
while(T--){
solve();
}
return 0;
}