字符串小祭

字符串哈希

板子

板子题
这东西写出来大概是酱子。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1000099,P=998244353,bs=191;
ll hst[N],hss,pbs[N];
char S[N],T[N];
int ls,lt;
inline ll ghs(int l,int r){
	return (hst[r]-hst[l-1]*pbs[r-l+1]%P+P)%P;
}
int main(){
	int _; scanf("%d",&_),pbs[0]=1;
	for(int i=1;i<=1000000;i++)
		pbs[i]=pbs[i-1]*bs%P;
	while(_--){
		scanf("%s%s",S+1,T+1);
		ls=strlen(S+1),lt=strlen(T+1);
		hss=0;
		int cnt=0;
		for(int i=1;i<=ls;i++)
			hss=(hss*bs+S[i])%P;
		for(int i=1;i<=lt;i++)
			hst[i]=(hst[i-1]*bs+T[i])%P;
		for(int i=1;i<=lt-ls+1;i++)
			if(hss==ghs(i,i+ls-1))
				++cnt;
		printf("%d\n",cnt);
	}
	return 0;
}
例题一

来一个 Link
首先注意到只有偶长度串且恰有一半为一的串才有机会。
然后考虑,我们可以构造一个倒序的反串,
比如:

1 1 0 0 1 0 1 1
0 0 1 0 1 1 0 0

这样可以 \(O(1)\) 检验一个串。
那不难想到二分,看看这东西性质,发现不可二分。
真不可二分吗?
枚举端点确实没辙,但枚举中心点就不一样了。
然后有 \(O(n\log n)\) 做法。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=500009,bs=191,P=998244353;
typedef long long ll;
int n;
char S[N],T[N];
ll hs[N],ht[N],pbs[N],ans;
inline ll gss(int l,int r){
    return (hs[r]-hs[l-1]*pbs[r-l+1]%P+P)%P;
}
inline ll gst(int l,int r){
    return (ht[r]-ht[l-1]*pbs[r-l+1]%P+P)%P;
}
bool check(int l,int r){
    return gss(l,r)==gst(n-r+1,n-l+1);
}
int main(){
    scanf("%d%s",&n,S+1),pbs[0]=1;
    for(int i=1;i<=n;i++)
        pbs[i]=pbs[i-1]*bs%P;
    for(int i=1;i<=n;i++)
        T[n-i+1]=(S[i]=='0')?'1':'0';
    for(int i=1;i<=n;i++)
        hs[i]=(hs[i-1]*bs+S[i])%P;
    for(int i=1;i<=n;i++)
        ht[i]=(ht[i-1]*bs+T[i])%P;
    for(int lmd=1,rmd;lmd<n;lmd++){
        rmd=lmd+1;
        int l=1,r=min(lmd,n-rmd+1),mid;
        if(!check(lmd,rmd)) continue;
        while(l<r){
            mid=(l+r+1)>>1;
            if(check(lmd-mid+1,rmd+mid-1)) l=mid;
            else r=mid-1;
        }
        ans+=l;
    }
    printf("%lld\n",ans);
    return 0;
}

简单构造 哈希 二分

例题二

Link
首先他得是一个前缀。
那你考虑我们可以暴力枚举,但是咋判断呢?
这个好像是典典结论,什么右移判定循环节好像是。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1000099,P=998244353,bs=191;
ll hs[N],pbs[N];
char S[N];
int n;
inline ll ghs(int l,int r){
	return (hs[r]-hs[l-1]*pbs[r-l+1]%P+P)%P;
}
int main(){
	scanf("%d%s",&n,S+1),pbs[0]=1;
    for(int i=1;i<=n;i++)
        pbs[i]=pbs[i-1]*bs%P;
    for(int i=1;i<=n;i++)
        hs[i]=(hs[i-1]*bs+S[i])%P;
    for(int i=1;i<=n;i++) if(ghs(1,n-i)==ghs(i+1,n))
        printf("%d",i),exit(0);
	return 0;
}
例题三

阅读题
你注意到这个字符串是循环的,所以操作次数其实可以无所谓。
这个哈希其实很好想,不怕被卡每个数字 rand 一个玩应直接加都可以,这里选择乘起来。
然后跑一个双指针之类的玩应。
非常毒瘤,不建议尝试。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
mt19937 rnd;
const int N=100010,P=1e9+7;
int n,ls,q,len[N],m,R[N],smlen,bs[N],S;
vector<int> a[N];
inline int pos(int k){
    return (k-1)%m+1;
}
inline int iv(int x){
    int ans=1,tmp=P-2;
    while(tmp){
        if(tmp&1) ans=1ll*ans*x%P;
        x=1ll*x*x%P,tmp>>=1;
    }
    return ans;
}
int baoli(int x){
    int sum=1,ans=0;
    for(int i=1,lng=0,ff=1,ffhas=len[R[1]],curid;i<=x;i++){
        curid=R[pos(i)];
        lng+=len[curid],sum=1ll*sum*a[curid][1]%P;
        if(lng<ls) continue;
        while(lng>ls){
            if(lng-ffhas>=ls){
                lng-=ffhas;
                int pff=R[pos(ff)];
                int lenff=len[pff];
                sum=1ll*sum*iv(a[pff][lenff-ffhas+1])%P;
                ++ff,ffhas=len[R[pos(ff)]];
            }else{
                lng-=ffhas;
                int pff=R[pos(ff)];
                int lenff=len[pff];
                sum=1ll*sum*iv(a[pff][lenff-ffhas+1])%P;
                ffhas=ls-lng;
                sum=1ll*sum*a[pff][lenff-ffhas+1]%P;
                lng=ls;
            }
            if(sum==0) exit(0);
        }
        if(sum==S) ++ans;
    }
    return ans;
}

bool bus[N];
int baoli2(int x,int y){
    int sum=1,ans=0;
    for(int i=1,lng=0,ff=1,ffhas=len[R[1]],curid;i<=y;i++){
        curid=R[pos(i)];
        lng+=len[curid],sum=1ll*sum*a[curid][1]%P;
        if(lng<ls) continue;
        while(lng>ls){
            if(lng-ffhas>=ls){
                lng-=ffhas;
                int pff=R[pos(ff)];
                int lenff=len[pff];
                sum=1ll*sum*iv(a[pff][lenff-ffhas+1])%P;
                ++ff,ffhas=len[R[pos(ff)]];
            }else{
                lng-=ffhas;
                int pff=R[pos(ff)];
                int lenff=len[pff];
                sum=1ll*sum*iv(a[pff][lenff-ffhas+1])%P;
                ffhas=ls-lng;
                sum=1ll*sum*a[pff][lenff-ffhas+1]%P;
                lng=ls;
            }
            if(sum==0) exit(0);
        }
        if(sum==S){
        	if(i<=x) ++ans;
        	else bus[i-x]=1;
		}
    }
    return ans;
}
int main(){
    unordered_set<int> st;
    st.insert(0);
    for(int i=0;i<=100000;i++){
        bs[i]=(rnd()%P+P)%P;
        while(st.count(bs[i])) bs[i]=(rnd()%P+P)%P;
        st.insert(bs[i]);
    }
    scanf("%d%d%d",&n,&ls,&q);
    S=1;
    for(int i=1,x;i<=ls;i++)
        scanf("%d",&x),S=1ll*S*bs[x]%P;
    for(int i=1,x;i<=n;i++){
        scanf("%d",&len[i]);
        a[i].push_back(0);
        for(int j=1;j<=len[i];j++)
            scanf("%d",&x),a[i].push_back(bs[x]);
        for(int j=len[i]-1;j>=1;j--)
            a[i][j]=1ll*a[i][j]*a[i][j+1]%P;//,printf("a %d %d=%d\n",i,j,a[i][j]);
    }
    
    scanf("%d",&m);
    for(int i=1;i<=m;i++)
    	scanf("%d",&R[i]),smlen+=len[R[i]];
	
    int rte=(ls+smlen-1)/smlen,ans;
    if((rte+1)*smlen>=q)
    	ans=baoli(q);
    else{
    	ans=baoli2(rte*m,rte*m+m);
    	q-=rte*m;
    	for(int i=1,sl=0;i<=m;i++) if(bus[i])
			ans+=(q-i)/m+1;
	}
    printf("%d\n",ans);
    return 0;
}
例题四

Link
不妨考虑不带修怎么做。
你考虑到每一个节点唯一对应于一组操作序列……

你考虑树的结构不变,每个点如果以根为操作节点,对应的序列近似唯一。
你考虑任意一个操作等价于从根开始的一个操作序列。

那你考虑,如果你搞一个序列哈希,你这东西是不是就等价于求已知哈希表中给定序列的最长前缀。
那你考虑,序列哈希带修左转大母神原型与偶像崇拜,线段树可做。

然而,这个哈希是不好判定前缀的,直接二分复杂度会变成双 log。
于是写一个线段树二分即可。

点击查看代码
#include<bits/stdc++.h>
#define ls (i<<1)
#define rs (i<<1|1)
#define ins int mid=(l+r)>>1
using namespace std;
const int N=500099,bs=1000097;
typedef unsigned long long ull;
int n,m,q,f[N],a[N];
ull pbs[N],hsnd[N];
vector<int> k[N];
unordered_map<ull,int> mp;
mt19937_64 rnd;
struct sgt{
	inline void pu(int i,int lr){
		hs[i]=hs[ls]*pbs[lr]+hs[rs];
	}
	void bd(int i,int l,int r){
		if(l==r){hs[i]=a[l]; return;}
		ins; bd(ls,l,mid),bd(rs,mid+1,r),pu(i,r-mid);
	}
	void mf(int i,int l,int r,int X,int K){
		if(l==r){hs[i]=K; return;}
		ins;
		if(X<=mid) mf(ls,l,mid,X,K);
		else mf(rs,mid+1,r,X,K);
		pu(i,r-mid);
	}
	ull _qr(int i,int l,int r,int L,int R){
		if(l==L&&r==R) return hs[i];
		ins;
		if(R<=mid) return _qr(ls,l,mid,L,R);
		else if(L>mid) return _qr(rs,mid+1,r,L,R);
		else return _qr(ls,l,mid,L,mid)*pbs[r-mid]+_qr(rs,mid+1,r,mid+1,R);
	}
	int qr(int i,int l,int r,int L,int R,ull cum){
		//printf("[%d %d] [%d %d] %lld\n",l,r,L,R,cum);
		if(l==r)
			return mp.count(cum*bs+hs[i])?mp[cum*bs+hs[i]]:-1;
		ins;
		if(R<=mid) return qr(ls,l,mid,L,R,cum);
		else if(L>mid) return qr(rs,mid+1,r,L,R,cum);
		else{
			ull gl=_qr(ls,l,mid,L,mid);
			ull dl=gl+cum*pbs[mid-L+1];
			if(mp.count(dl)){//左段可行
				int ans=qr(rs,mid+1,r,mid+1,R,dl);
				return (~ans)?ans:mp[dl];
			}else
				return qr(ls,l,mid,L,mid,cum);
		}
		
	}
	ull hs[N<<2];
}T;

void dfs(int i,int rk){
	hsnd[i]=hsnd[f[i]]*bs+rk;
	sort(k[i].begin(),k[i].end());
	int srk=0;
	for(int j:k[i]) dfs(j,++srk);
}

int main(){
	scanf("%d%d%d",&n,&m,&q),pbs[0]=1;
	for(int i=1;i<=500000;i++)
		pbs[i]=pbs[i-1]*bs;
	for(int i=1;i<=n;i++) scanf("%d",&f[i]),k[f[i]].push_back(i);
	for(int i=1;i<=m;i++) scanf("%d",&a[i]);
	T.bd(1,1,m);
	for(int i=1;i<=n;i++) if(f[i]==0) dfs(i,0);
	for(int i=1;i<=n;i++){
		if(mp.count(hsnd[i]))
			puts("I AK IOI!"),exit(0);
		mp[hsnd[i]]=i;
	}
	for(int i=1,op,aa,bb,cc;i<=q;i++){
		scanf("%d%d%d",&op,&aa,&bb);
		if(op==1){
			scanf("%d",&cc);
			if(k[aa].size()<a[bb])
				printf("%d\n",aa);
			else
				printf("%d\n",T.qr(1,1,m,bb,cc,hsnd[aa]));
		}else
			T.mf(1,1,m,aa,bb),a[aa]=bb;
	}
	return 0;
}

马拉车

板子

Link

  • 每两个字符之间以及首尾插入间隔字符,首尾插入特殊的保护字符,转化成只有奇长度回文串的问题。
    例如 abcde 变成 10a0b0c0d0e02
  • 维护 \(f(i)\) 为每个点向外扩展的最大长度,不妨从 1 开始计数。
  • 维护当前状态下,前面扩展到的最大回文串右端点 \(r\),及其对应的回文中心 \(mid\)
    • 如果当前点 \(i>r\),暴力转移。
    • 否则,我们以 \(\min\set{r-i+1,f(mid+mid-i)}\) 为基准值进行转移。

综上,得到了一个线性求 \(f\) 函数的算法。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=23000000;
char S[N],T[N];
int cnt=-1,f[N],mid=0,r=0,ans;
//注意 cnt 应从 -1 开始
int main(){
    T[++cnt]='1';
    T[++cnt]='0';
    scanf("%s",S);
    int ls=strlen(S);
    for(int i=0;i<ls;i++)
        T[++cnt]=S[i],T[++cnt]='0';
    T[cnt+1]='2';
    for(int i=1;i<=cnt;i++){
        if(i>r) f[i]=1;
        else f[i]=min(r-i+1,f[mid+mid-i]);
        while(T[i+f[i]]==T[i-f[i]]) ++f[i];
        if(i+f[i]-1>r) r=i+f[i]-1,mid=i;
        ans=max(ans,f[i]-1);
    }
    printf("%d\n",ans);
    return 0;
}
例题一

Link
找到最后一个尽量靠左的 \(mid\) 即可。

例题二

Link
很板子了,考虑扫出来所有的序列最大长度,排个序,记个数推一下即可。
或者搞一个差分也可。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=2000009,P=19930726;
int n,f[N],sum[N];
ll k;
char S[N];
inline int qp(int a,int b){
	int ans=1;
	while(b){
		if(b&1) ans=1ll*ans*a%P;
		a=1ll*a*a%P,b>>=1;
	}
	return ans;
}
int main(){
	scanf("%d%lld%s",&n,&k,S+1),S[0]='!',S[n+1]='?';
	for(int i=1,mid=0,r=0;i<=n;i++){
		f[i]=(i>r)?1:min(f[mid+mid-i],r-i+1);
		while(S[i+f[i]]==S[i-f[i]]) ++f[i];
		++sum[1],--sum[f[i]+f[i]+1];
        if(i+f[i]-1>r) r=i+f[i]-1,mid=i;
	}
	for(int i=3;i<=n;i++,i++) sum[i]+=sum[i-2];
	int ans=1;
	for(int i=(n%2==1)?n:n-1;i>=1;i--,i--){
		if(k<=sum[i]){
			ans=1ll*ans*qp(i,(int)k)%P;
			printf("%d\n",ans);
			return 0;
		}
		ans=1ll*ans*qp(i,sum[i])%P,k-=sum[i];
	}
	puts("-1");
	return 0;
}
例题三

Link
不妨维护每个点为左右端点对应的最大(小)中心点。
建线段树,区间取 \(min/max\),单点查询,标记永久化不难。

点击查看代码
#include<bits/stdc++.h>
#define ls (i<<1)
#define rs (i<<1|1)
#define ins int mid=(l+r)>>1
using namespace std;
const int N=200999;

char S[N],T[N];
int cnt=-1,f[N];
struct sgmt_min{
    void bd(int i,int l,int r){
        tg[i]=1e9;
        if(l==r) return;
        ins;
        bd(ls,l,mid),bd(rs,mid+1,r);
    }
    void mf(int i,int l,int r,int L,int R,int X){
        if(l==L&&r==R){
            tg[i]=min(tg[i],X);
            return;
        }
        ins;
        if(R<=mid) mf(ls,l,mid,L,R,X);
        else if(L>mid) mf(rs,mid+1,r,L,R,X);
        else mf(ls,l,mid,L,mid,X),mf(rs,mid+1,r,mid+1,R,X);
    }
    void qr(int i,int l,int r,int lim){
        lim=min(lim,tg[i]);
        if(l==r){
            q[l]=lim;
            return;
        }
        ins;
        qr(ls,l,mid,lim),qr(rs,mid+1,r,lim);
    }
    int tg[N<<2],q[N];
}T1;

struct sgmt_max{
    void bd(int i,int l,int r){
        tg[i]=-1e9;
        if(l==r) return;
        ins;
        bd(ls,l,mid),bd(rs,mid+1,r);
    }
    void mf(int i,int l,int r,int L,int R,int X){
        if(l==L&&r==R){
            tg[i]=max(tg[i],X);
            return;
        }
        ins;
        if(R<=mid) mf(ls,l,mid,L,R,X);
        else if(L>mid) mf(rs,mid+1,r,L,R,X);
        else mf(ls,l,mid,L,mid,X),mf(rs,mid+1,r,mid+1,R,X);
    }
    void qr(int i,int l,int r,int lim){
        lim=max(lim,tg[i]);
        if(l==r){
            q[l]=lim;
            return;
        }
        ins;
        qr(ls,l,mid,lim),qr(rs,mid+1,r,lim);
    }
    int tg[N<<2],q[N];
}T2;

int main(){
    scanf("%s",S+1);
    int lens=strlen(S+1);
    T[++cnt]='1',T[++cnt]='0';
    for(int i=1;i<=lens;i++)
        T[++cnt]=S[i],T[++cnt]='0';
    T[cnt+1]='2';
    T1.bd(1,1,cnt),T2.bd(1,1,cnt);
    //puts(T);
    for(int i=1,r=0,mid=0;i<=cnt;i++){
        f[i]=(i>r)?1:min(r-i+1,f[mid+mid-i]);
        while(T[i+f[i]]==T[i-f[i]]) ++f[i];
        if(r<i+f[i]-1) r=i+f[i]-1,mid=i;
        T1.mf(1,1,cnt,i,i+f[i]-1,i);
        //printf("%d-%d min %d\n",i,i+f[i]-1,i);
        T2.mf(1,1,cnt,i-f[i]+1,i,i);
        //printf("%d-%d max %d\n",i-f[i]+1,i,i);
    }
    T1.qr(1,1,cnt,1e9),T2.qr(1,1,cnt,-1e9);
    int ans=0;
    for(int i=1;i<lens;i++)
        if(T1.q[i+i]<=i+i&&T2.q[i+i+2]>=i+i+2){
            int rr=(T2.q[i+i+2]+T2.q[i+i+2]-i-i-2)/2;
            int ll=(T1.q[i+i]+T1.q[i+i]-i-i)/2;
            ans=max(ans,rr-ll+1);
            //printf("%d [%d %d][%d %d]\n",ans,ll,i,i+1,rr);
        }
    printf("%d\n",ans);
    return 0;
}

翻了翻 TJ 发现我纯……
这东西维护每个开头最长回文串长度,下一个是可以递推的!

KMP 与 Z 函数

KMP 板子

Link

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=1000009;
char S[N],T[N];
int ls,lt,f[N];
vector<int> ans;
int main(){
    scanf("%s%s",T+1,S+1);
    ls=strlen(S+1);
    lt=strlen(T+1);
    f[0]=-1;//重要细节
    for(int i=1,lst=-1;i<=ls;i++){
        while(~lst&&S[lst+1]!=S[i]) lst=f[lst];
        f[i]=++lst;
    }
    for(int i=1,j=0;i<=lt;i++){
        while(~j&&T[i]!=S[j+1]) j=f[j];
        if(++j==ls) j=f[j],ans.push_back(i-ls+1);
    }
    for(int x:ans) printf("%d\n",x);
    for(int i=1;i<=ls;i++) printf("%d ",f[i]); 
    return 0;
}
Z 函数板子

Link

\(Z(i)\) 定义为原字符串与从第 \(i\) 个字符开始的后缀的最长公共前缀(LCP)长度。
定义一个匹配段为 \([x,x+Z(x)-1]\),由定义,他等于 \([1,Z(x)]\)
类似马拉车,维护一个右端点最大的匹配段,设为 \([l,r]\)
现在想要求 \(Z(i)\)

  • 如果 \(i>r\),暴力即可。
  • 否则,有 \([i,r]=[i-l+1,r-l+1]\)
    非常像马拉车,不妨继承 \(\min\set{Z(i-l+1),r-l+1}\) 并继续尝试扩展。
z[1]=b;
for(int i=2,l=0,r=0;i<=b;i++){
    if(i<=r) z[i]=min(z[i-l+1],r-i+1);
    while(B[z[i]+1]==B[i+z[i]]) ++z[i];
}

然后你考虑第二问。
不妨暴力匹配求出 \(P(1)\)
同样的,维护 A 上面的匹配段 \([l,r]\)
如果 \(i\le r\) 那么有 \(A[i,r]=B[i-l+1,r-l+1]\)
于是继承掉 \(\min\set{r-i+1,Z(i-l+1)}\) 进行扩展。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=20000009;
char A[N],B[N];
int a,b,z[N],p[N];

int main(){
    scanf("%s%s",A+1,B+1);
    a=strlen(A+1),b=strlen(B+1);
    A[a+1]='?',B[b+1]='!';

    z[1]=b;
    ll ans1=b+1;
    for(int i=2,l=0,r=0;i<=b;i++){
        if(i<=r) z[i]=min(z[i-l+1],r-i+1);
        while(B[z[i]+1]==B[i+z[i]]) ++z[i];
        ans1^=1ll*i*(z[i]+1);
        if(i+z[i]-1>r) r=i+z[i]-1,l=i;
    }

    while(A[p[1]+1]==B[p[1]+1]) ++p[1];
    ll ans2=p[1]+1;
    for(int i=2,l=0,r=0;i<=a;i++){
        if(i<=r) p[i]=min(z[i-l+1],r-i+1);
        while(B[p[i]+1]==A[i+p[i]]) ++p[i];
        ans2^=1ll*i*(p[i]+1);
        if(i+p[i]-1>r) r=i+p[i]-1,l=i;
        //printf("%d\n",p[i]);
    }
    
    printf("%lld\n%lld",ans1,ans2);
    return 0;
}
例题一

Link
印章必须是前缀,也必须是后缀,也就是 border。
即,印章必然为跳 border 所得 border 序列之一。

不妨暴力,从短到长枚举 border 序列,KMP 数组已经求好,直接匹配检查即可。

然后你考虑所选 border 的后面一些短 border 又是当前 border 的 border,
如果当前这东西合法,那么你必然要能够在大的数组中挑出若干个位置,
使得他们的 border 能转化成当前的印章。

如果你对所有前缀的 border 关系建图,你会得到一个树形结构,以零为根。
显然,只有序列结尾所在的那颗子树是有意义的,所以可能的答案也就是一条链。
然后我们发现你只能选择一个子树,这个子数内排序后相邻两个节点位置之差不能长过印章。
这个东西……搞一个权值线段树呢?

点击查看代码
#include<bits/stdc++.h>
#define ls (i<<1)
#define rs (i<<1|1)
#define ins int mid=(l+r)>>1
using namespace std;
const int N=500009;
char S[N];
int n,f[N];
vector<int> k[N];

struct sgmt{
	inline void pu(int i){
		if(lim[ls]==0&&lim[rs]==0)
			lim[i]=rim[i]=sep[i]=0;
		else if(lim[ls]==0){
			lim[i]=lim[rs];
			rim[i]=rim[rs];
			sep[i]=sep[rs];
		}else if(lim[rs]==0){
			lim[i]=lim[ls];
			rim[i]=rim[ls];
			sep[i]=sep[ls];
		}else{
			lim[i]=lim[ls];
			rim[i]=rim[rs];
			sep[i]=max(max(sep[ls],sep[rs]),lim[rs]-rim[ls]);
		}
	}
	
	void mf(int i,int l,int r,int X){
		if(l==r){
			lim[i]=rim[i]=X;
			sep[i]=0;
			return;
		}
		ins;
		if(X<=mid)
			mf(ls,l,mid,X);
		else
			mf(rs,mid+1,r,X);
		pu(i);
	}
	
	int lim[N<<2],rim[N<<2],sep[N<<2];
}T;

void dfs(int i){
	T.mf(1,1,n,i);
	for(int j:k[i]) dfs(j);
}

int main(){
	scanf("%s",S+1),n=strlen(S+1);
	
	f[0]=-1;
	for(int i=1,lst=f[0];i<=n;i++){
		while(~lst&&S[i]!=S[lst+1]) lst=f[lst];
		f[i]=++lst;
		k[f[i]].push_back(i);
	}
	
	int ans=n;
	T.mf(1,1,n,n);
	for(int i=f[n],lst=n;i!=0;i=f[i]){
		T.mf(1,1,n,i);
		for(int j:k[i]) if(j!=lst) dfs(j);
		if(T.sep[1]<=i) ans=i;
		lst=i;
	}
	printf("%d\n",ans);
	return 0;
}

瞄了一圈题解,这思路是真猎奇
其实对于 border 关系建树这种东西还挺常见的,
一会去写失配树……

学习一下逆天 dp,
\(f(i)\) 表示前缀 \(i\) 的答案,
然后 \(f(i)\) 转移有两种。

  • 保底:\(f(i)=i\)
  • 必要探路:
    如果可以,\(f(i)=f(nxt(i))\)
    什么时候可以呢?
    由于这个印章能够覆盖最后一段 border,
    我们维护一个 \(lst(i)\) 函数表示长度为 \(i\) 的印章所覆盖的最右位置,
    如果 \(lst(f(nxt(i)))\ge i-nxt(i)\) 那么就可以转移。

补充证明:
\(f(nxt(i))\) 合法,则他是最优解。
\(f(nxt(i))\) 非法,则 \(i\) 是最优解。

考虑 border 序列。

对于第一点,假设存在一个更短的合法印章 \(L\)
任何短于 \(f(nxt(i))\) 的串都无法表达 \(nxt(i)\)\(L\) 也是。
但是 \(L\) 必然是 \(nxt(i)\) 的一个非严格 border(可能相等),
这样的话,
如果 \(L\) 不能表达 \(nxt(i)\),他也不能表达原串,证毕。

对于第二点,
如果 \(f(nxt(i))\) 非法,
假设存在一个更长的合法印章 \(L\),满足 \(L\neq i\)
由上面的证明,\(L\) 必须是 \(nxt(i)\) 的一个非严格 border。
可以证明 \(f(nxt(i))\)\(L\) 的一个严格 border。
由于 \(f(nxt(i))\) 能表达 \(nxt(i)\)\(f(nxt(i))\)\(L\) 的一个严格 border,
\(f(nxt(i))\) 也能表达 \(L\)
由于 \(f(nxt(i))\) 非法,\(L\) 非法,证毕。

核心逻辑:

  • 如果 \(A\) 能表达 \(B\)\(A\) 无法表达 \(C\),那么 \(B\) 无法表达 \(C\)
  • 如果 \(A\) 能表达 \(B\)\(A\) 必然是 \(B\) 的 border,必然是 \(B\) 最长 border 的非严格 border。
  • 如果 \(A\)\(B\) 的 border 却无法表达 \(B\),那么他也就无法表达以 \(B\) 为非严格 border 的任何串。

然后,\(lst\) 的转移也就非常简单了,不多说。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=500999;
int f[N],dp[N],lst[N],n;
char S[N];
int main(){
    scanf("%s",S+1);
    n=strlen(S+1);
    f[0]=-1;
    for(int i=1,lst=f[0];i<=n;i++){
        while(~lst&&S[i]!=S[lst+1]) lst=f[lst];
        f[i]=++lst;
    }
    dp[1]=lst[1]=1;
    for(int i=2;i<=n;i++){
        if(lst[dp[f[i]]]>=i-f[i])
            dp[i]=dp[f[i]];
        else
            dp[i]=i;
        lst[dp[i]]=i;
    }
    printf("%d\n",dp[n]);
    return 0;
}
例题二

Link
跑一次板子,暴力去跳 border,对于一堆 a 的数据会卡到平方复杂度。
不妨构建 border 树,然后 dfs,查找祖先中小于等于一半的个数,可实现 \(O(n\log n)\)
树状数组即可。

点击查看代码
#include<bits/stdc++.h>
#define lb(x) ((x)&(-(x)))

using namespace std;
const int N=1000009,P=1e9+7;
char S[N];
int f[N],n,ans,t[N];
vector<int> k[N];

inline int qr(int x){
	int ans=0;
	while(x)
		ans+=t[x],x-=lb(x);
	return ans;
}

inline void mf(int x,int d){
	while(x<=n)
		t[x]+=d,x+=lb(x);
}

void dfs(int i){
    if(i)
        ans=1ll*ans*(qr(i/2)+1)%P,mf(i,1);
    for(int j:k[i]) dfs(j);
    if(i)
    	mf(i,-1);
}

int main(){
    int T; scanf("%d",&T);
    while(T--){
        scanf("%s",S+1),n=strlen(S+1),ans=1;
        f[0]=-1,k[0].clear();
        for(int i=1,lst=-1;i<=n;i++){
            k[i].clear();
            while(~lst&&S[lst+1]!=S[i])
                lst=f[lst];
            f[i]=++lst;
            k[f[i]].push_back(i);
        }
        dfs(0),printf("%d\n",ans);
    }
    return 0;
}

还有倍增跳父亲的方法,感觉也不如这个简单qaq
还有一种玄妙的树上双指针,可以线性,
主要是发现 border 树每一条链都是单调的,性质优秀
不太好讲,贴个代码吧……

点击查看代码
#include<bits/stdc++.h>

using namespace std;
const int N=1000009,P=1e9+7;
char S[N];
int f[N],n,ans;
vector<int> k[N];
int q[N],tt,cur;

void dfs(int i){
    int ori_cur=cur;
    if(i){
        q[++tt]=i;
        while(q[cur+1]<=i/2) ++cur;
        ans=1ll*ans*(cur+1)%P;
    }
    for(int j:k[i]) dfs(j);
    if(i)
        --tt,cur=ori_cur;
}

int main(){
    int T; scanf("%d",&T);
    while(T--){
        scanf("%s",S+1),n=strlen(S+1),ans=1;
        f[0]=-1,k[0].clear();
        for(int i=1,lst=-1;i<=n;i++){
            k[i].clear();
            while(~lst&&S[lst+1]!=S[i])
                lst=f[lst];
            f[i]=++lst;
            k[f[i]].push_back(i);
        }
        dfs(0),printf("%d\n",ans);
    }
    return 0;
}
例题三

Link
矩乘标准数据范围……
对于一个 KMP 自动鸡,反正就是枚举每一个位置,看看再加一个数字会到哪里啦……

点击查看代码
#include<bits/stdc++.h>
#define f(i,l,r) for(int i=(l);i<=(r);i++)
using namespace std;
const int N=30;
int n,m,p,f[N],mp[N][N],ans[N][N],tmp[N][N];
char S[N];
int main(){
    scanf("%d%d%d%s",&n,&m,&p,S+1);
    f[0]=-1;
    for(int i=1,lst=-1;i<=m;i++){
        while(~lst&&S[lst+1]!=S[i]) lst=f[lst];
        f[i]=++lst;
    }
    f(i,0,m-1){
        f(x,0,9){
            int j=i;
            while((~j)&&(S[j+1]!=x+'0')) j=f[j];
            ++mp[i][++j];
        }
    }
    f(i,0,m) ans[i][i]=1;
    while(n){
        if(n&1){
            f(i,0,m) f(j,0,m) f(k,0,m)
                tmp[i][k]+=ans[i][j]*mp[j][k],tmp[i][k]%=p;
            f(i,0,m) f(j,0,m)
                ans[i][j]=tmp[i][j],tmp[i][j]=0;
        }
        n>>=1;
        f(i,0,m) f(j,0,m) f(k,0,m)
            tmp[i][k]+=mp[i][j]*mp[j][k],tmp[i][k]%=p;
        f(i,0,m) f(j,0,m)
            mp[i][j]=tmp[i][j],tmp[i][j]=0;
    }
    int Ans=0;
    f(i,0,m-1) Ans+=ans[0][i],Ans%=p;
    printf("%d\n",Ans);
    return 0;
}
例题四

Link
不妨开一个栈往里硬差,维护匹配到了哪里,然后硬做就完事了。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=1000009;
char S[N],T[N];
int s,t,f[N];
stack<int> st;
vector<char> ans;
int main(){
    scanf("%s%s",S+1,T+1),s=strlen(S+1),t=strlen(T+1);
    f[0]=-1;
    for(int i=1,lst=-1;i<=t;i++){
        while(~lst&&T[lst+1]!=T[i]) lst=f[lst];
        f[i]=++lst;
    }
    st.push(0);
    for(int i=1;i<=s;i++){
        int j=st.top();
        while(~j&&T[j+1]!=S[i]) j=f[j];
        st.push(++j),ans.push_back(S[i]);
        if(j==t) while(j--) st.pop(),ans.pop_back();
    }
    for(char c:ans) putchar(c);
    return 0;
}
例题五

Link
这道题可以二分哈希来做,
你考虑如果用一个前缀来当周期,这东西最大长度是可二分的。
然后问题就转化成区间取最大值,以及求和。
可以司机,也可以求和转单点查询,再跑标记永久化的线段树。
然后思考怎么 KMP 做。

不用思考了,依旧结论,\(n-f(i)\) 即为答案。
不妨理解一下,对于一个循环节,容易证明可以构造补长度的 border。

例题六

Link
在此祝题面作者及其家人身体健康……
差分去做 KMP 匹配,没了……

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=200900;
int n,m,a[N],b[N],f[N];
int main(){
    scanf("%d%d",&n,&m),--n,--m;
    if(m==0) printf("%d\n",n+1),exit(0);
    for(int i=0;i<=n;i++) scanf("%d",a+i);
    for(int i=0;i<=m;i++) scanf("%d",b+i);
    for(int i=n;i>=1;i--) a[i]-=a[i-1];
    for(int i=m;i>=1;i--) b[i]-=b[i-1];
    f[0]=-1;
    for(int i=1,lst=-1;i<=m;i++){
        while(~lst&&b[lst+1]!=b[i]) lst=f[lst];
        f[i]=++lst;
    }
    int ans=0;
    for(int i=1,j=0;i<=n;i++){
        while(~j&&b[j+1]!=a[i]) j=f[j];
        if(++j==m) j=f[j],++ans;
        
    }
    printf("%d\n",ans);
    return 0;
}
例题七

Link
思路是明确的,弄一个 \(f(i)\) 表示 \(i\) 截断的方案,KMP 预处理位置。
但是你考虑到子串可以不连续,所以你求的实际上是:

\[\sum_{i=1}^n \sum_{j=1}^i dp(i) \]

但这也不难做。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=100009,P=1e9+7;
char S[N],T[N];
int n,m,f[N],dp[N],sum1[N],sum2[N];
set<int> mp;
int main(){
    scanf("%s%s",S+1,T+1);
    n=strlen(S+1),m=strlen(T+1);
    f[0]=-1;
    for(int i=1,lst=-1;i<=m;i++){
        while(~lst&&T[lst+1]!=T[i]) lst=f[lst];
        f[i]=++lst;
    }
    for(int i=1,j=0;i<=n;i++){
        while(~j&&S[i]!=T[j+1]) j=f[j];
        if(++j==m) j=f[j],mp.insert(i);
    }
	if(mp.empty()) puts("0"),exit(0);
    int sep=*mp.begin();
    for(int i=0;i<sep;i++){
    	dp[i]=(int)(i==0);
    	sum1[i]=1;
    	sum2[i]=i+1;
    }
    for(int i=sep;i<=n;i++){
        auto tmp=mp.upper_bound(i);
        --tmp;
        int lim=*tmp-m;
        dp[i]=sum2[lim];
        sum1[i]=(sum1[i-1]+dp[i])%P;
        sum2[i]=(sum2[i-1]+sum1[i])%P;
        //printf("%d dp=%d lim=%d sum1=%d sum2=%d\n",i,lim,dp[i],sum1[i],sum2[i]);
    }
    printf("%d\n",(sum1[n]+P-1)%P);
    return 0;
}
例题八

Link
是 border 树耶!我们有救了!
这题不知道怎么说,树上维护子树大小耶!

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=100009;
char S[N]; int n,f[N],sz[N];
vector<int> k[N],a,b;
void dfs(int u){
    sz[u]=1;
    for(int v:k[u]) dfs(v),sz[u]+=sz[v];
}
int main(){
    scanf("%s",S+1),n=strlen(S+1);
    f[0]=-1;
    for(int i=1,lst=-1;i<=n;i++){
        while(~lst&&S[lst+1]!=S[i]) lst=f[lst];
        f[i]=++lst,k[f[i]].push_back(i);
    }
    dfs(0);
    for(int i=n;i!=0;i=f[i])
        a.push_back(i),b.push_back(sz[i]);
    int tmp=a.size();
    printf("%d\n",tmp);
    for(int i=tmp-1;i>=0;i--) printf("%d %d\n",a[i],b[i]);
    return 0;
}
例题九

Link
考虑 zx 状态设计,设 \(f(i)\) 表示匹配到 \(i\) 所能够获得的最大完成个数。
于是 \(O(nm|w|)\) 做完了。

然而并没有,交了一发然后 T 了。
我们发现我们应该把这个 KMP 自动机类似物正经的建出来,而不是每次都暴力的去跳那个 border。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=100009;
int f[N],n,m,dp[N],tmp[N],go[N][26];
char S[N],T[N];
inline void solve(int i,int j){
	int _j=j,cnt=0;
	j=go[j][S[i]-'a'];
	if(j==m) j=f[j],++cnt;
	tmp[j]=max(tmp[j],dp[_j]+cnt);
}
int main(){
    scanf("%s%s",S+1,T+1);
    n=strlen(S+1),m=strlen(T+1);
    f[0]=-1;
    for(int i=1,lst=-1;i<=m;i++){
    	while(~lst&&T[lst+1]!=T[i]) lst=f[lst];
    	f[i]=++lst;
    }
    
    for(int i=0;i<26;i++) go[0][i]=0;
    go[0][T[1]-'a']=1;
    for(int i=1;i<m;i++) for(int j=0;j<26;j++)
    	go[i][j]=(j==T[i+1]-'a')?i+1:go[f[i]][j];
    
    for(int j=1;j<m;j++) dp[j]=-1e9;
    for(int i=1;i<=n;i++){
    	for(int j=0;j<m;j++) tmp[j]=-1e9;
    	for(int j=0;j<m;j++) if(dp[j]>=0){
    		if(S[i]=='?'){
    			for(S[i]='a';S[i]<='z';++S[i])
    				solve(i,j);
    			S[i]='?';
    		}else solve(i,j);
    	}
    	for(int j=0;j<m;j++) dp[j]=tmp[j];
    }
    int ans=0;
    for(int i=0;i<m;i++)
    	ans=max(ans,dp[i]);
    printf("%d\n",ans);
    return 0;
}
posted @ 2026-08-10 16:14  2025ing  阅读(24)  评论(0)    收藏  举报