题目链接:E - Who Says a Pun?
法1:dp,O(n^2)
设 为第一个串从i位置开始,第二个串从j位置开始,最大匹配长度。
dp转移是:
#include <bits/stdc++.h>
using namespace std;
#define FOR(i,a,b) for(int i=(a), (i##i)=(b); i<=(i##i); ++i)
#define ROF(i,a,b) for(int i=(a), (i##i)=(b); i>=(i##i); --i)
template<class T>inline bool cmax(T&a,const T&b){return a<b?a=b,1:0;}
template<class T>inline bool cmin(T&a,const T&b){return a>b?a=b,1:0;}
#define int long long
const int N = 5e3+5;
int n,f[N][N]; //f[x][y]表示第一个串从x开始,第二个串从y开始,两后缀最大相等前缀长度
char s[N];
int ans=0;
int dfs(int x,int y){ //计算f[x][y]
if(x>n || y>n) return 0; //特判
if(f[x][y]!=-1) return f[x][y];
if(s[x]!=s[y]) f[x][y]=0;
else f[x][y] = min(y-x, dfs(x+1,y+1)+1);
cmax(ans, f[x][y]);
return f[x][y];
}
signed main(){
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin>>n>>(s+1);
memset(f,-1,sizeof(f));
FOR(i,1,n) FOR(j,i+1,n) dfs(i,j);
cout<<ans;
}
法2:SA,O(n^2)
求出height数组之后很容易知道任意对的LCP,显然答案就是,其中i和j都是1~n的整数。
#include <bits/stdc++.h>
using namespace std;
#define FOR(i,a,b) for(int i=(a), (i##i)=(b); i<=(i##i); ++i)
#define ROF(i,a,b) for(int i=(a), (i##i)=(b); i>=(i##i); --i)
template<class T>inline bool cmax(T&a,const T&b){return a<b?a=b,1:0;}
template<class T>inline bool cmin(T&a,const T&b){return a>b?a=b,1:0;}
const int N=1e6+5;
char s[N];
int n,m,x[N],y[N],c[N],sa[N];
int height[N],rk[N]; //height[i]=LCP(i,i-1), rk[i]是s[i~n]在所有后缀中的字典序排名
void SA(){
/**
* 首先定义一些变量
* suffix[i]: 表示s[i~n]这个后缀
* rk[i]: suffix[i]在所有后缀中的排名
* sa[i]: 满足suffix[sa[1]] < suffix[sa[2]] < ... < suffix[sa[n]],也就是说,排名为i的后缀的编号是sa[i]
* 有一个很重要的性质:sa[rk[i]] = rk[sa[i]] = i
*/
//对{s[i],i}二元组进行基数排序
for(int i=1;i<=n;i++){x[i]=s[i];++c[x[i]];} //c数组是桶,x[i]是第i个元素的第一关键字
for(int i=2;i<=m;i++)c[i]+=c[i-1];
for(int i=n;i>=1;i--)sa[c[x[i]]--]=i; //注意,这里为了防止sa值重复,c[x[i]]需要--,第一关键字相同时,位置越靠前的后缀排名越靠前
for(int k=1;k<=n;k=k<<1) //2.倍增进行基数排序
{
int num=0; //只是一个数组指针
for (int i=n-k+1;i<=n;++i) y[++num]=i; //定义y[num]表示第二关键字中,排名为num的数的位置,[n-k+1,n]这个区间后面没有字母了,所以是最小的,放在最前面
for(int i=1;i<=n;i++) //在第一关键字中排名越靠前,表示应该排在前面,所以从1~n遍历sa[i],也就是按照第一关键字从小到大遍历
if(sa[i]>k)y[++num]=sa[i]-k; //sa[i]表示排名为i的第一关键字pos,注意pos>k的才会作为当前的第二关键字
//按{第一关键字,第二关键字}进行基数排序
for(int i=1;i<=m;i++)c[i]=0;
for(int i=1;i<=n;i++)c[x[i]]++;
for(int i=2;i<=m;i++)c[i]+=c[i-1];
for(int i=n;i>=1;i--){sa[c[x[y[i]]]--]=y[i];y[i]=0;}
swap(x,y); //下面即将要生成新的x数组(新的第一关键字),它要用到旧的第一关键字,所以把旧的暂存在y这里
num=1;x[sa[1]]=1; //当前第一关键字是1,排名第一个的那个(sa[1])的第一关键字也当然是1
for(int i=2;i<=n;i++) //再计算排名是2~n的二元组的新的第一个关键字
{
if(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k])x[sa[i]]=num; //第一关键字和第二关键字都和排名上一个的一样,那么新的第一个关键字就一样
else x[sa[i]]=++num; //否则就是排名前一个的新的第一关键字+1
}
if(num==n)break; //如果排名数量num已经是n了(已经排好了),就没必要继续排了,剪枝,结束
m=num; //新的关键字数,是旧的排名数
}
// for(int i=1; i<=n; i++) cout<<sa[i]<<' '; cout<<'\n';
}
void LCP(){
/**
* 首先定义一些变量
* height[i]: 表示suffix[sa[i]]和suffix[sa[i-1]]的LCP长度,也就是排名相邻的两个后缀的最长公共前缀长度
* h[i]: 表示height[rk[i]],也就是suffix[i]和suffix[i-1]的LCP长度
* 用到一个很重要的性质:h[i] >= h[i-1]+1,它使得LCP函数的均摊复杂度变为O(N)
*
* 跑完这个函数之后,具体有什么用呢?用法如下:
* LCP(i,j) = min(height[k]) (i<k<=j)
*/
int k=0; //就是当前的h[i]
for(int i=1; i<=n; i++) rk[sa[i]]=i; //根据sa数组反推出rk数组
for(int i=1; i<=n; i++){
if(rk[i]==1) continue; //height[1]=0
if(k) k--; //用到h[i] >= h[i-1]+1的性质
int j=sa[rk[i]-1]; //排名在i前一位的后缀编号
while(i+k<=n && j+k<=n && s[i+k]==s[j+k]) k++; //逐位匹配
height[rk[i]] = k;
}
// for(int i=1; i<=n; i++) cout<<height[i]<<' '; cout<<'\n';
}
signed main(){
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin>>n;
cin>>(s+1);
m=122; //'z'的ascii码是122,故一开始的关键字数是122
SA();
LCP();
int ans=0;
FOR(i,1,n){
int mnh=0x3f3f3f3f;
FOR(j,i+1,n){
cmin(mnh, height[j]);
cmax(ans, min(abs(sa[i]-sa[j]), mnh));
}
}
cout<<ans;
}
法3:SAM,O(n)
该做法参考自:ABC141 E - Who Says a Pun?(SAM)_live4m的博客-CSDN博客
本题要用sam做的话,需要熟知sam两个重要的性质:
- sam中每个结点都是一个endpos等效类的集合,比如某个节点可能存着{ab,cab,dcab}这三个后缀,那么此时ab,cab,dcab的endpos集合都是完全一样的,这样他们才能存到一起!
- sam中的后缀树:假如u是v的父节点,也就是u = fa[v],那么u中存着的所有后缀一定都是v的后缀,举个例子,u代表{ba, aba},v代表{baba, ababa, aababa}。
用一个简单的例子来解释一下上面两个性质,假如S="aababa"。它的后缀树如下(根结点是空字符串,已被省略):
(图片来自【学习笔记】后缀自动机SAM_OneInDark的博客-CSDN博客_后缀自动机 ,侵删)
对于性质1:用同一个红框框起来的所有后缀就是一个endpos等效类的集合,他们在sam中被缩成一个点。(可理解为:同一条单链的,能缩成一个点)
对于性质2:看这棵树的结构,父亲一定是儿子的后缀。
根据性质二可以知道,儿子对应的后缀出现了,那么父亲对应的后缀一定也出现了。那么根据这个性质,我们可以通过类似树形dp的方式,维护每个后缀最左边出现位置L和最右边出现位置R。
对于节点x:
1.如果R-L>=maxlen[x],那么该节点长度为[minlen[x],maxlen[x]]的串出现了至少两次,
那么可以用maxlen[x]更新答案。
2.如果R-L<maxlen[x],那么该节点只有长度为[minlen[x],R-L]的串出现了至少两次,
那么可以用R-L更新答案。对上述情况取max就是最后的答案。
#include <bits/stdc++.h>
#define FOR(i,a,b) for(int i=(a), (i##i)=(b); i<=(i##i); ++i)
#define ROF(i,a,b) for(int i=(a), (i##i)=(b); i>=(i##i); --i)
template<class T>inline bool cmax(T&a,const T&b){return a<b?a=b,1:0;}
template<class T>inline bool cmin(T&a,const T&b){return a>b?a=b,1:0;}
// #define int long long
const int N = 1e4+5;
using namespace std;
struct SAM{ //(其中N是字符串最大长度,代码注释掉表示不是很常用,但也可能用到)
int tot=1, last=1; //最大点编号,上一个点编号
int len[N<<1], fa[N<<1], sz[N<<1];
vector<int> g[N<<1]; //需要建立后缀树时要用到(有时候要用)
//int f[N<<1]; //记录子树的sz之和(不常用)
int to[N<<1][26];
int l[N<<1],r[N<<1];
// 为当前的SAM增加一个字符c
void extend(int c,int pos){
int p, cur = ++tot; //新建一个结点(因为S[1..i+1]必定是一个新状态)
len[cur] = len[last]+1; //S[1...i+1].len = S[1...i] + 1
sz[cur] = 1; //初始sz值都是1
//情况1,或者情况2的前半部分
for(p=last; p && !to[p][c]; p=fa[p]){
to[p][c] = cur; //如果前面的状态没有连c,那就现在连上
}
if(!p) fa[cur] = 1; //如果一直到原点,都是没有连过c,那就是满足情况1,link接到起始点1
else{ //否则就是中间有的点连过c,满足情况2
int q = to[p][c]; //p点有连过c,连了c的state在q这里
if(len[q]==len[p]+1) fa[cur]=q; //情况2-A类。不用拆分,直接连q
else{ //情况2-B类,需要拆分q点,拆分出一个cl结点
int cl = ++tot; //拆分出的新结点
len[cl] = len[p]+1; //把能连link的部分拆分到cl这里(就是满足len[p]+1的部分)
// to[cl] = to[q]; //复制所有原结点的trans连接
memcpy(to[cl], to[q], sizeof(to[q]));
fa[cl] = fa[q]; //后缀重连
fa[cur] = fa[q] = cl; //后缀重连
while(p && to[p][c]==q){ //把之前trans到q的点全部改成trans到cl(因为trans指向state中最短的字符串,而这个串在cl那里)
to[p][c] = cl;
p = fa[p];
}
}
}
last = cur; //最后一步,记录末尾结点
l[cur]=r[cur]=pos; //
}
//建立后缀树(有时候要用)
void build(){
FOR(i,2,tot) g[fa[i]].push_back(i);
}
//dfs计算size
void dfs(int u){
for(int v:g[u]){
dfs(v);
//为什么不能像下面两行(62,63行)那样直接取min或max呢?
//因为拆分出的点(cl点)没有l和r的初始值,他们只继承儿子的信息,没有自己的信息
if(!l[u] || l[u]>l[v]) l[u]=l[v];
if(!r[u] || r[u]<r[v]) r[u]=r[v];
// cmin(l[u], l[v]);
// cmax(r[u], r[v]);
}
}
int cal(){
int ans=0;
// FOR(i,1,tot) cout<<len[i]<<' '<<l[i]<<' '<<r[i]<<'\n';
FOR(i,1,tot) cmax(ans, min(len[i], r[i]-l[i]));
return ans;
}
} sam;
int n; char s[N];
signed main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin>>n>>(s+1);
FOR(i,1,n) sam.extend(s[i]-'a', i);
sam.build();
sam.dfs(1);
cout<<sam.cal();
}