ABC141 E - Who Says a Pun? (三种解法,dp/SA/SAM)

题目链接:E - Who Says a Pun?

法1:dp,O(n^2)

f[i][j] 为第一个串从i位置开始,第二个串从j位置开始,最大匹配长度。

dp转移是:f[i][j] = \left\{\begin{matrix} 0,\, \: (s[i]!=s[j])\\ f[i+1][j+1]+1,\, \: (s[i]==s[j])\: \end{matrix}\right.

#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数组之后很容易知道任意(i,j)对的LCP,显然答案就是max(abs(i-j), LCP(i,j)),其中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两个重要的性质:

  1. sam中每个结点都是一个endpos等效类的集合,比如某个节点可能存着{ab,cab,dcab}这三个后缀,那么此时ab,cab,dcab的endpos集合都是完全一样的,这样他们才能存到一起!
  2. 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();
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值