P4094 [HEOI2016/TJOI2016]字符串(二分+SAM上线段树合并)

这篇博客介绍了如何使用后缀自动机解决字符串处理问题,具体为寻找两个子串的最大公共前缀。文章详细阐述了通过二分答案、建立后缀自动机以及利用倍增技巧来优化查询的过程,最后实现了一个高效的算法来解决这个问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

LINK

题意

给定串 s s s q q q个询问

每次问 s [ a . . . b ] s[a...b] s[a...b]的所有子串和 s [ c . . . . d ] s[c....d] s[c....d]的最长公共前缀最大值


考虑二分答案 m i d mid mid

问题转化为, s [ c . . . c + m i d − 1 ] s[c...c+mid-1] s[c...c+mid1]是否在 s [ a . . . b ] s[a...b] s[a...b]的某个子串中出现过

考虑到 s [ c . . . c + m i d − 1 ] s[c...c+mid-1] s[c...c+mid1]是前缀,而后缀自动机更擅长处理后缀,我们对s反转成 s ′ s' s S A M SAM SAM

那么现在是 s ′ [ d − m i d + 1 , d ] s'[d-mid+1,d] s[dmid+1,d]是否在 s ′ [ a . . . b ] s'[a...b] s[a...b]的某个子串中出现过

首先我们需要找到 s ′ [ d − m i d + 1 , d ] s'[d-mid+1,d] s[dmid+1,d] S A M SAM SAM上对应的节点,如何快速查找??

我们可以记录一下 s ′ [ 1... d ] s'[1...d] s[1...d]在哪个节点,然后往倍增跳到 l o n g e s t > = m i d longest>=mid longest>=mid的节点 v v v

节点 v v v就包含了状态 s ′ [ d − m i d + 1 , d ] s'[d-mid+1,d] s[dmid+1,d]

那么现在就是判断 v v v e n d p o s endpos endpos中是否出现过 [ a + m i d − 1 , b ] [a+mid-1,b] [a+mid1,b]即可,出现过,说明 s ′ [ a . . . b ] s'[a...b] s[a...b]存在这个串

我们就找到了答案。

维护每个点的 e n d p o s endpos endpos就是对每个节点开一棵权值线段树,就从叶子节点一直往上线段树合并即可

稍微注意一下, a , b , c , d a,b,c,d a,b,c,d读进来要修改一下,因为现在是反串上操作

#include <bits/stdc++.h>
using namespace std;
const int maxn = 2e5+10;
const int N = maxn*41;
int n,m;
int ls[N],rs[N],rt[N],sum[N],num;
void push_up(int root)
{
	sum[root] = sum[ls[root]]+sum[rs[root]];
}
void add(int &root,int l,int r,int index)
{
	if( l>index || r<index )	return;
	if( !root )	root = ++num;
	if( l==r&&l==index ){ sum[root] = 1; return; }
	int mid = l+r>>1;
	add( ls[root],l,mid,index); add( rs[root],mid+1,r,index);
	push_up(root);
}
int ask(int &root,int l,int r,int L,int R)
{
	if( l>R||r<L )	return 0;
	if( !root )	return 0;
	if( l>=L&&r<=R )	return sum[root];
	int mid = l+r>>1;
	return ask(ls[root],l,mid,L,R)+ask( rs[root],mid+1,r,L,R);
}
int merge(int x,int y,int l,int r)
{
	if( !x || !y )	return x|y;
	int p = ++num, mid = l+r>>1;
	if( l==r ){sum[p] = sum[x]|sum[y]; return p; }
	ls[p] = merge( ls[x],ls[y],l,mid);
	rs[p] = merge( rs[x],rs[y],mid+1,r);
	push_up(p);
	return p;
}
int ed[maxn],zi[maxn][28],fa[maxn],len[maxn],las = 1, id = 1;
char s[maxn];
void insert(int c)
{
	int p = las, np = ++id; las = id;
	len[np] = len[p]+1; 
	add( rt[np],1,n,len[np] );//插入endpos 
	for( ; p && zi[p][c]==0 ; p = fa[p] )	zi[p][c] = np;
	if( p==0 )	fa[np] = 1;
	else
	{
		int q = zi[p][c];
		if( len[q]==len[p]+1 )	fa[np] = q;
		else
		{
			int nq = ++id;
			memcpy( zi[nq],zi[q],sizeof zi[nq] );
			len[nq] = len[p]+1; fa[nq] = fa[q];
			fa[np] = fa[q] = nq;
			for( ; p && zi[p][c]==q ;p = fa[p] )	zi[p][c] = nq;
		}
	}
}
vector<int>vec[maxn];
int f[maxn][22];//树上倍增 
void dfs(int u,int father)
{
	f[u][0] = father;
	for(int i=1;i<=20;i++)	f[u][i] = f[f[u][i-1]][i-1];
	for( auto v:vec[u] )
	{
		dfs(v,u);
		rt[u] = merge( rt[u],rt[v],1,n );
	}
}
bool isok(int mid,int a,int b,int c,int d)
{
	int p = ed[d];//[1...d]所在的节点
	for(int i=20;i>=0;i--)
		if( len[f[p][i]]>=mid )	p = f[p][i];//倍增,跳到[d-mid+1,d]所在的节点
	if( ask( rt[p],1,n,a+mid-1,b) )	return true;//有endpos 
	return false; 
}
int main()
{
	cin >> n >> m >> ( s+1 );
	reverse( s+1, s+1+n );
	for(int i=1;i<=n;i++)	insert( s[i]-'a' ),ed[i] = las;
	for(int i=2;i<=id;i++)	vec[fa[i]].push_back( i );
	dfs(1,0);//线段树合并 
	for(int i=1;i<=m;i++)
	{
		int a,b,c,d; scanf("%d%d%d%d",&b,&a,&d,&c);
		a = n-a+1, b = n-b+1, c = n-c+1, d = n-d+1;
		int l = 1, r = d-c+1, ans = 0;
		while( r>=l )
		{
			int mid = l+r>>1;
			if( isok(mid,a,b,c,d) )	l = mid+1, ans = mid;
			else	r = mid-1;
		}
		printf("%d\n",ans);
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值