zoukankan      html  css  js  c++  java
  • 【BZOJ1396】识别子串(后缀自动机+线段树)

    点此看题面

    大致题意: 给定一个字符串,对每个位置别求出包含该位置、仅出现一次的子串的最短长度。

    后缀自动机

    众所周知,一个子串的出现次数就是后缀自动机上对应节点的(Sz),此题中也就是要求(Sz=1)

    根据套路,我们枚举前缀找到对应节点,而子串就是该节点自身及其列祖列宗。

    然后我们发现两个基本事实:

    • 一个节点的(Sz)不可能为(0)。这应该显然吧,毕竟(Sz)表现的是子串出现次数。
    • 祖先的(Sz)肯定大于该节点的(Sz),注意这里的大于是严格大于。因为一个祖先的(Sz)必然会从至少两个儿子处转移(否则这个祖先就应该和儿子合并)。

    结合这两点,我们就会发现,列祖列宗(Sz)必然大于(1),是不用考虑的。

    因此,我们只需要在当前点(Sz=1)时考虑当前点的答案。

    回忆一下知识点:后缀自动机上每个点维护的是若干长度依次增(1)(endpos)集合相同当前点所表示字符串的后缀,其中最长串的长度就是当前点的(L),最短串的长度是(L_{fa} +1)

    换言之,假设当前枚举到位置(i)(显然当前点(L=i)),那么前缀(i)长度为(L_{fa} +1sim i)的后缀都是合法的。

    前缀的后缀可以表示为子串,也可以写作子串([1sim i-L_{fa} ,i])都是合法的。

    那么考虑这些子串对每个位置上答案的贡献,不难发现可以分两类讨论:

    • 对于([i-L_{fa} +1,i]),它们被所有子串包含,其中最短的就是长度为(L_{fa} +1)的后缀。
    • 对于(j∈[1,i-L_{fa} ]),显然包含它们的最短子串就是以其为开头的后缀,也就是长度为(i-j+1)的后缀。

    也就是说,前半部分是对一段区间内的答案贡献一个定值,而后半部分是对一段区间内的答案贡献一个等差数列。

    显然可以开两个线段树分别维护。

    当然,你可以只用一个线段树。但由于定值可以看做公差为(0)的特殊的等差数列,因此看似两个线段树实际上只需要写一个,而且省了不少码量。不过具体如何写还是要看个人喜好了。

    代码

    #include<bits/stdc++.h>
    #define Tp template<typename Ty>
    #define Ts template<typename Ty,typename... Ar>
    #define Reg register
    #define RI Reg int
    #define Con const
    #define CI Con int&
    #define I inline
    #define W while
    #define N 100000
    #define Gmin(x,y) (x>(y)&&(x=(y)))
    using namespace std;
    int n;char s[N+5];
    template<int d> class SegmentTree//线段树,d为公差
    {
    	private:
    		#define PT CI l=1,CI r=n,CI rt=1
    		#define LT l,mid,rt<<1
    		#define RT mid+1,r,rt<<1|1
    		int V[N<<2];
    	public:
    		I void Build(PT)//建树
    		{
    			if(V[rt]=1e9,l==r) return;int mid=l+r>>1;Build(LT),Build(RT);
    		}
    		I void Upt(CI L,CI R,CI x,PT)//修改,使用标记永久化就方便了许多
    		{
    			if(L<=l&&r<=R) return (void)Gmin(V[rt],x);int mid=l+r>>1;
    			L<=mid&&(Upt(L,R,x,LT),0),R>mid&&(Upt(L,R,x-d*(mid-l+1),RT),0);
    		}
    		I int Qry(CI x,PT)//询问,注意每个点上标记的贡献
    		{
    			RI t,res=V[rt]-d*(x-l);if(l==r) return res;int mid=l+r>>1;
    			return t=x<=mid?Qry(x,LT):Qry(x,RT),min(t,res);
    		}
    };SegmentTree<0> S0;SegmentTree<1> S1;
    class SuffixAutomation//后缀自动机
    {
    	private:
    		int Nt,lst,p[N<<1],t[N<<1];struct node {int Sz,L,F,S[30];}O[N<<1];
    	public:
    		I SuffixAutomation() {Nt=lst=1;}
    		I void Ins(CI x)//插入字符,没什么新奇的
    		{
    			RI p=lst,now=lst=++Nt;O[now].L=O[p].L+1,O[now].Sz=1;
    			W(p&&!O[p].S[x]) O[p].S[x]=now,p=O[p].F;if(!p) return (void)(O[now].F=1);
    			RI q=O[p].S[x];if(O[p].L+1==O[q].L) return (void)(O[now].F=q);
    			RI k=++Nt;(O[k]=O[q]).L=O[p].L+1,O[O[now].F=O[q].F=k].Sz=0;
    			W(p&&O[p].S[x]==q) O[p].S[x]=k,p=O[p].F;
    		}
    		I void Work()//按长度排序后统计Sz,同样没什么新奇的
    		{
    			RI i;for(i=1;i<=Nt;++i) ++t[O[i].L];for(i=1;i<=n;++i) t[i]+=t[i-1];
    			for(i=1;i<=Nt;++i) p[t[O[i].L]--]=i;for(i=Nt;i;--i) O[O[p[i]].F].Sz+=O[p[i]].Sz;
    		}
    		I void Solve()//求答案
    		{
    			for(RI i=1,p=1;i<=n;++i) p=O[p].S[s[i]&31],O[p].Sz==1&&//枚举前缀,若Sz=1才考虑贡献
    			(
    				O[O[p].F].L&&(S0.Upt(i-O[O[p].F].L+1,i,O[O[p].F].L+1),0),//定值
    				S1.Upt(1,i-O[O[p].F].L,i),0//等差数列
    			);
    			for(RI i=1,x,y;i<=n;++i) printf("%d
    ",min(S0.Qry(i),S1.Qry(i)));//输出答案
    		}
    }S;
    int main()
    {
    	RI i;for(scanf("%s",s+1),n=strlen(s+1),i=1;i<=n;++i) S.Ins(s[i]&31);
    	return S0.Build(),S1.Build(),S.Work(),S.Solve(),0;
    }
    
  • 相关阅读:
    [转]编程能力与编程年龄
    github for windows 使用
    github 改位置
    Linux下设置和查看环境变量
    Docker基础 :网络配置详解
    docker入门实战笔记
    Jenkins +Maven+Tomcat+SVN +Apache项目持续集成构建
    使用nsenter工具进入Docker容器
    Docker从入门到实战(四)
    Docker从入门到实战(三)
  • 原文地址:https://www.cnblogs.com/chenxiaoran666/p/BZOJ1396.html
Copyright © 2011-2022 走看看