洛谷 - P6292 区间本质不同子串个数(SAM+LCT+线段树)

题目链接:点击查看

题目大意:给出一个长度为 n 的字符串,再给出 m 次询问,每次询问需要回答区间 [ l , r ] 内有多少个本质不同的字符串

题目分析:首先简化模型,回顾一下如何求解 “区间内有多少个不同的数” :SPOJ - DQUERY

求解上面那道题目的方法可谓是五花八门,我们采用其中一种离线的方式:先将所有的询问储存下来,按照右端点 r 排序后,每次对于重复出现的数字,用线段树维护其最后一次出现的位置,那么答案就是 [ l , r ] 中元素的个数了

对于这个题目而言,将每个本质不同的字符串视为一个连续的区间 [ l , r ],我们只需要维护左端点最后一次出现的位置即可

因为需要知道每个子串最后一次出现的位置,所以我们选择对字符串构造后缀自动机,对于某个右端点 r 来说,对应到 SAM 上,从最长的那个前缀 [ 1 , r ] 对应的节点开始,沿着 parent 树到根节点的这条路径上,都是以 r 为右端点的子串,设 pre[ i ] 为节点 i 上一次出现时的右端点位置,我们只需要暴跳 father,每次将之前出现过的位置,在线段树上区间更新成 -1 (因为对于每个节点来说,代表的都是一段连续的后缀,所以可以用到线段树的区间更新),最后再将 [ 1 , r ] , [ 2 , r ] ... [ r , r ] 这 r 个后缀所代表的子串的左端点,即 [ 1 , r ] 在线段树上 +1 就好了

不过问题是,这样暴跳 father 的时间复杂度是不正确的,直接这样实现在本题中只能得到 80 分,有一个测试点会超时

考虑优化,因为每次选择一条链,自下而上去更新,其本质就是:

  1. 令这条链每个节点相应的位置在线段树上区间更新
  2. 令每个节点都被端点 r 覆盖(也就是将 pre 都标记为 r)

这个过程其实就是 LCT 中的 access 函数的操作,这样每次更新至多需要更新 logn 个 splay,套上线段树就是 log^2n,均摊下来就是 nlog^2n 了

因为在 SAM 自底向上的一条路径上,其中任意连续的一段,所能表示的后缀也是连续的,所以在 LCT 的 splay 上只需要维护一下相应重链上最短的子串长度就好了

因为在本题中用不到 LCT 的 link、cut 等操作,所以将相应的一些基本函数都删掉了,只保留了与 access 函数有关的一些基本函数

总时间复杂度是 O( nlog^2n + mlogn )

代码:
 

//#pragma GCC optimize(2)
//#pragma GCC optimize("Ofast","inline","-ffast-math")
//#pragma GCC target("avx,sse2,sse3,sse4,mmx")
#include<iostream>
#include<cstdio>
#include<string>
#include<ctime>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<stack>
#include<climits>
#include<queue>
#include<map>
#include<set>
#include<sstream>
#include<cassert>
#include<bitset>
using namespace std;
     
typedef long long LL;
     
typedef unsigned long long ull;
     
const int inf=0x3f3f3f3f;

const int N=1e6+100;

char s[N];

vector<pair<int,int>>q[N];

LL ans[N];

namespace Seg
{
	struct Node
	{
		int l,r,len;
		LL sum,lazy;
	}tree[N<<2];
	void pushup(int k)
	{
		tree[k].sum=tree[k<<1].sum+tree[k<<1|1].sum;
	}
	void pushdown(int k)
	{
		if(tree[k].lazy)
		{
			LL lz=tree[k].lazy;
			tree[k].lazy=0;
			tree[k<<1].sum+=tree[k<<1].len*lz;
			tree[k<<1|1].sum+=tree[k<<1|1].len*lz;
			tree[k<<1].lazy+=lz;
			tree[k<<1|1].lazy+=lz;
		}
	}
	void build(int k,int l,int r)
	{
		tree[k].l=l;
		tree[k].r=r;
		tree[k].len=r-l+1;
		tree[k].sum=tree[k].lazy=0;
		if(l==r)
			return;
		int mid=l+r>>1;
		build(k<<1,l,mid);
		build(k<<1|1,mid+1,r);
	}
	void update(int k,int l,int r,LL val)
	{
		if(tree[k].l>r||tree[k].r<l)
			return;
		if(tree[k].r<=r&&tree[k].l>=l)
		{
			tree[k].sum+=tree[k].len*val;
			tree[k].lazy+=val;
			return;
		}
		pushdown(k);
		update(k<<1,l,r,val);
		update(k<<1|1,l,r,val);
		pushup(k);
	}
	LL query(int k,int l,int r)
	{
		if(tree[k].l>r||tree[k].r<l)
			return 0;
		if(tree[k].l>=l&&tree[k].r<=r)
			return tree[k].sum;
		pushdown(k);
		return query(k<<1,l,r)+query(k<<1|1,l,r);
	}
}

namespace SAM
{
	int tot,last,pos[N],pre[N];
	struct Node
	{
	    int ch[26];
	    int fa,len;
	}st[N<<1];
	inline int newnode()
	{
		tot++;
		for(int i=0;i<26;i++)
			st[tot].ch[i]=0;
		st[tot].fa=st[tot].len=0;
		return tot;
	}
	void add(int x)
	{
	    int p=last,np=last=newnode();
	    st[np].len=st[p].len+1;
	    while(p&&!st[p].ch[x])st[p].ch[x]=np,p=st[p].fa;
	    if(!p)st[np].fa=1;
	    else
	    {
	        int q=st[p].ch[x];
	        if(st[p].len+1==st[q].len)st[np].fa=q;
	        else
	        {
	            int nq=newnode();
	            st[nq]=st[q]; st[nq].len=st[p].len+1;
	            st[q].fa=st[np].fa=nq;
	            while(p&&st[p].ch[x]==q)st[p].ch[x]=nq,p=st[p].fa;//向上把所有q都替换成nq
	        }
	    }
	}
	void init()
	{
		last=1;
		tot=0;
		newnode();
	}
	void build()
	{
		init();
		int len=strlen(s+1);
		for(int i=1;i<=len;i++)
		{
			add(s[i]-'a');
			pos[i]=last;
		}
	}
}

namespace LCT
{
	int fa[N],ch[N][2],pre[N],min_len[N],len[N],Stack[N],lazy[N],top;
	bool son(int x)//判断点x是父节点的左儿子还是右儿子 
	{
		return x==ch[fa[x]][1];
	}
	bool isroot(int x)//判断一个点是不是根节点(当前splay的根节点)
	{
		return ch[fa[x]][0]!=x&&ch[fa[x]][1]!=x;
	}
	void pushup(int x)//上传(维护信息)
	{
		min_len[x]=min(len[x],min(min_len[ch[x][0]],min_len[ch[x][1]]));
	}
	void update_lazy(int x,int lz)
	{
		pre[x]=lazy[x]=lz;
	}
	void pushdown(int x)//下传(更新反转标记用)
	{
		if(lazy[x])
		{
			update_lazy(ch[x][0],lazy[x]);
			update_lazy(ch[x][1],lazy[x]);
			lazy[x]=0;
		}
	}
	void rotate(int x)//splay的旋转 
	{
		int y=fa[x],z=fa[y],c=son(x);
		ch[y][c]=ch[x][c^1];if(ch[y][c]) fa[ch[y][c]]=y;
		fa[x]=z;if(!isroot(y))ch[z][son(y)]=x;
		ch[x][c^1]=y;fa[y]=x;pushup(y);
	}
	void splay(int x)//将x转到根节点 
	{
		Stack[top=1]=x;
		for (int i=x;!isroot(i);i=fa[i])
			Stack[++top]=fa[i];
		while (top) pushdown(Stack[top--]);
		for (int y=fa[x];!isroot(x);rotate(x),y=fa[x])
			if (!isroot(y)) son(x)^son(y)?rotate(x):rotate(y);
		pushup(x);
	}
	void access(int x,int id)//把x节点到x所在树(连通块)的根节点之间的路径全部变成重路径
	{
		int y;
		for(y=0;x;y=x,x=fa[x])
		{
			splay(x),ch[x][1]=y,pushup(x);
			if(pre[x])
				Seg::update(1,pre[x]-SAM::st[x].len+1,pre[x]-min_len[x]+1,-1);
		}
		splay(y);
		update_lazy(y,id);
		Seg::update(1,1,id,1);
	}
	void build()
	{
		min_len[0]=inf;
		for(int i=1;i<=SAM::tot;i++)
		{
			fa[i]=SAM::st[i].fa;
			len[i]=min_len[i]=SAM::st[SAM::st[i].fa].len+1;
			lazy[i]=ch[i][0]=ch[i][1]=pre[i]=0;
		}
	}
};

int main()
{
#ifndef ONLINE_JUDGE
//  freopen("data.in.txt","r",stdin);
//  freopen("data.out.txt","w",stdout);
#endif
//  ios::sync_with_stdio(false);
	scanf("%s",s+1);
	int n=strlen(s+1);
	SAM::build();
	LCT::build();
	Seg::build(1,1,n);
	int m;
	scanf("%d",&m);
	for(int i=1;i<=m;i++)
	{
		int l,r;
		scanf("%d%d",&l,&r);
		q[r].emplace_back(i,l);
	}
	for(int i=1;i<=n;i++)
	{
		LCT::access(SAM::pos[i],i);
		for(auto it:q[i])
			ans[it.first]=Seg::query(1,it.second,i);
	}
	for(int i=1;i<=m;i++)
		printf("%lld\n",ans[i]);













    return 0;
}

 

在编程语言中,并没有直接被称为"C+="的语言。可能您指的是C++。C++是一种通用编程语言,它支持面向对象、泛型和过程式编程风格。在C++中处理字符串和计算子串出现次数可以通过各种方法实现,但请注意C++标准库中没有直接的操作符或函数叫做"C+="。 如果您是想要在C++中计算一个随机生成的数字字符串(假设是一个数字组成的字符串)中某个连续子串出现的次数,可以使用以下步骤: 1. 生成随机数字字符串(如果是通过程序生成的话)。 2. 定义一个函数来计算特定子串在字符串中出现的次数。 3. 调用该函数并传入相应的参数,输出结果。 下面是一个简单的C++示例代码,用于计算随机生成的数字字符串中某个子串出现的次数: ```cpp #include <iostream> #include <string> #include <cstdlib> #include <ctime> int countSubString(const std::string &str, const std::string &sub) { int count = 0; size_t pos = 0; while ((pos = str.find(sub, pos)) != std::string::npos) { ++count; pos += sub.length(); } return count; } int main() { srand(time(0)); // 初始化随机数种子 std::string randomStr; std::string subStr; // 假设我们要生成一个长度为10的随机数字字符串 for (int i = 0; i < 10; ++i) { randomStr += char('0' + rand() % 10); } // 子串长度为2 subStr = randomStr.substr(rand() % randomStr.length(), 2); // 计算子串出现次数 int count = countSubString(randomStr, subStr); // 输出结果 std::cout << "Random string: " << randomStr << std::endl; std::cout << "Sub-string: " << subStr << std::endl; std::cout << "Count: " << count << std::endl; return 0; } ``` 这段代码首先生成了一个随机的数字字符串,然后随机选择一个子串,并计算该子串在原字符串中出现的次数。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Frozen_Guardian

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值