1 条题解

  • 0
    @ 2025-8-24 22:20:18

    自动搬运

    查看原文

    来自洛谷,原作者为

    avatar EternalAlexander
    魔力的碎片都不再拥有的少年

    搬运于2025-08-24 22:20:18,当前版本为作者最后更新于2020-04-15 20:14:18,作者可能在搬运后再次修改,您可在原文处查看最新版

    自动搬运只会搬运当前题目点赞数最高的题解,您可前往洛谷题解查看更多

    以下是正文


    终于有一个不是 Ynoi 的月赛 F 题了,是不是很感动


    若无特别说明,iijjkk 表示小 M 的位置,小 B 的位置和山顶的位置。操作串表示进行操作三时的 SijS_{i \cdots j}

    过于显然的结论不说明了,需要详细题解请报讲评

    结论 -1

    在所有合法的 jj 中,选取最大的作为小 B 的初始位置一定不劣。

    结论 0

    任何时候,假如满足 i=ji=j,在接下来的过程中保持 i=ji=j 一定不劣。

    结论 1

    假如存在一种方案,使得在进行第一次操作三时,操作串不是给定的 SijS_{i \cdots j} 的子串,且优于一切不是这样的方案,那么这个方案一定不优于先使 i=ji=j,然后再进行操作。

    证明 1

    注意到,在这种方案中,你一定在第一次操作三之前使用了 i1i-1 或者 j+1j+1

    假如你使用了上述操作,并且在第一次操作三时的 i,ji,j 与给定的 i,ji,j 有交,那么你把上述操作移动到这次操作三之后同样合法,可以变为不是这样的方案。

    因为这样进行操作三时的操作串是原来的操作串的子串,因此这个操作三也合法,然后位置的差别可以通过在之后进行同样的操作来修正。

    假如操作串与输入的 SijS_{i \cdots j} 没有交,但多于一个字符,撤销掉若干个上述操作一定可以让它变成一个字符,然后把撤销掉的这些操作移动到最后一定也合法。

    这个结论可以归纳地推广到任意一次操作三。


    至此,我们可以先处理两种情况:不使用操作三,以及先通过某些操作使得 i=ji=j 然后保持这样。

    不使用操作三时,答案为 ik|i-k|

    考虑单字符的情况:记 disi,jdis_{i,j} 表示从 jj 到任意一个 xx 使得 Sx=iS_x = i 的最小代价。考虑如何计算,枚举字符 ii ,然后将所有字符为 ii 的位置加入队列,进行多源 bfs 即可。暴力实现会建出 O(n2)O(n^2) 条边,可以使用一个优化建图技巧:对每个字符建一个虚点,每个位置向对应字符连 11 边,字符向位置连 00 边,跑 01-bfs。也有等价的不需要显式地优化建图的方法。

    然后,枚举第一次操作三时的字符 cc。第一种情况是,ccSijS_{i \cdots j} 中出现过,可以用 ji+disc,k+1j-i+dis_{c,k}+1 更新答案。

    第二种情况是 cc 没有出现过,此时用 ji+min(disc,i,disc,j)+disc,k+1j-i+ \min(dis_{c,i},dis_{c,j})+dis_{c,k}+1 更新答案。


    在后面,我们只考虑,进行操作三时的操作串是 SijS_{i \cdots j} 的子串的情况。其他情况已经处理过了。

    结论 2

    一定存在一种最优解的方案是,先进行若干次操作一和操作二,然后进行一次操作三,然后再进行若干次操作一。

    简要说明一下:考虑假如你在两次操作三之间进行了一次其他操作,将这个操作移到所有操作三的前面也是合法的。

    这样的话,所有操作三是连续的一段,显然可以只进行一次。

    因此:这种情况是,先收缩成一段,然后进行一次操作三,然后归位。


    我们考虑操作三之后 iikk 的位置关系。

    第一种情况是 i<ki<k。发现这样一定不优于让 i=ki=k

    因为,这样操作之后需要进行若干次操作使 i+1i+1,而如果将这些操作放在操作三之前完成,也是合法的。

    第二种情况是 iki \geq k

    把答案写出来,然后去掉常数,发现我们要做的其实是,对于所有在 kk 之后出现过的 [L,R][L,R] ,满足 SLRS_{L \cdots R}SijS_{i \cdots j} 中出现过,最小化 2LR2L-R

    暴力1

    固定 LL 时,显然 RR 应该尽量大。考虑枚举 LL,就是求最大的 RR 使得 SLRS_{L \cdots R}SijS_{i \cdots j} 中出现过。这个有很多种 SAM,SA,后缀树的处理方式,下面给出一种。

    二分 RR,即判断 SLRS_{L \cdots R} 是否存在于 SijS_{i \cdots j} 中。对 SS 建后缀树,倍增定位出 SLRS_{L \cdots R},然后求对应节点的子树中是否存在在 [i+RL,j][i+R-L,j] 中的后缀即可。这一步可以可持久化线段树合并。

    算上枚举 LL,总复杂度 O(nqlog2n)O(nq\log^2n)。然而标算使用的是略为复杂的 O(nqlogn)O(nq\log n) 的离线 LCT 做法。

    暴力2

    固定长度,则 LL 应尽量小。

    首先考虑 kk 固定的情况,发现求出的 jj 是两个后缀的 LCP,也就是说,SijS_{i \cdots j} 是后缀树上一个节点,是 SAM 上的一个等价类。

    对每个点维护子树中的所有在 kk 之后最左的后缀。我们在后缀树上 dp,设 fif_i 为节点 ii 代表的子串的所有子串的 2LR2L-R 的最小值,gig_i 表示节点 ii 的子树中的 kk 之后的最左的后缀的位置,lenilen_i 表示节点 ii 代表的串的长度。

    fi=min(ffai,flinki,gileni1)f_i = \min(f_{fa_i},f_{link_i},g_i - len_i -1) ,其中 faifa_i 表示节点 ii 的父节点,linkilink_i 表示节点 ii 的后缀链接指向的节点。在后缀树上倍增定位出 SijS_{i \cdots j} 查询 dp 值即可。

    注意到这样只能找到一个等价类内最长串的答案,易知这不劣。

    对每个 kk 做一次的复杂度为 O(n)O(n)。直接做复杂度为 复杂度 O(nq)O(nq)

    不暴力了

    相信各位经验丰富的选手看到这里时,正解已经呼之欲出了。

    考虑用上述两个暴力来摊平查询的复杂度。考虑按 kk 从后往前扫,每隔 BB 个位置重新 dp 一次,然后对于到上一次重构中间的小块部分用第一个暴力处理。

    根据第一个暴力的复杂度,取适当的 BB 可以得到 O(nnlogn)O(nnlogn)O(n\sqrt{n \log n}) \sim O(n\sqrt n \log n) 的做法。可以通过所有测试点。

    std 写的是 O(nnlogn+nΣ)O(n\sqrt{n \log n} + n \Sigma) 的做法。


    下面是 std,需要可以自取

    #include <bits/stdc++.h>
    #define maxn 30005
    const int B=40;
    const int inf=1e8;
    int n,q,ch[maxn<<1][2],anc[maxn<<1][17]={0},
    depth[maxn<<1]={0},rank[maxn],tail=0,rk[maxn<<1],dp[maxn<<1],min[maxn<<1],tl,
    ans[maxn],d[maxn<<1],vis[30],dis[27][maxn],pre[maxn<<1],fa[maxn<<1],max[maxn<<1],tag[maxn<<1],
    cnt[26][maxn];
    std::vector<int>rc[27];
    char s[maxn];
    struct Q{int i,j,p,id;}qr[maxn];
    int cmp1(Q a,Q b){return a.p>b.p;}
    int cmp(int a,int b){
    	if (depth[a]!=depth[b])return depth[a]<depth[b];
    	return d[a]<d[b];
    }struct Q2{
    	int l,r,p,id,k;
    }q1[maxn*B];
    int cmp2(Q2 a,Q2 b){return a.l>b.l;}
    void cover(int x,int y){tag[x]=std::min(tag[x],y);max[x]=std::min(max[x],y);}
    void pushdown(int x){
    	if(ch[x][0])cover(ch[x][0],tag[x]);
    	if(ch[x][1])cover(ch[x][1],tag[x]);
    	tag[x]=inf;
    }
    int not_root(int x) {return ch[fa[x]][0]==x||ch[fa[x]][1]==x;}
    void rotate(int x) {
        int f=fa[x],g=fa[f],l=ch[f][1]==x,c=ch[x][l^1];
        if (not_root(f)) ch[g][ch[g][1]==f]=x;
        ch[x][l^1]=f;ch[f][l]=c;
        if (c) fa[c]=f;
        fa[f]=x;fa[x]=g;
    }
    void spaly(int x) {
    	std::stack<int>stk;
    	int u=x;stk.push(u);while(not_root(u)){stk.push(fa[u]);u=fa[u];}
    	while(!stk.empty()){pushdown(stk.top());stk.pop();}
        while(not_root(x)){
        	rotate(x); 
    		if (not_root(fa[x])&&not_root(x))
    			rotate((ch[fa[x]][1]==x)!=(ch[fa[fa[x]]][1]==fa[x])?fa[x]:x);
        }
    }
    struct suffixTree {
    	int link[maxn<<1],len[maxn<<1],start[maxn<<1],s[maxn<<1],tail,n,rem,now,ch[maxn<<1][27];
    	suffixTree ():tail(1),n(0),rem(0),now(1) {len[0]=inf;}
    	int newnode(int st,int le){
    		link[++tail]=1;start[tail]=st;len[tail]=le;return tail;
    	}
    	void extend(int x){
    		s[++n]=x;rem++;
    		for (int last=1;rem;){
    			while (rem>len[ch[now][s[n-rem+1]]]) rem-=len[now=ch[now][s[n-rem+1]]];
    			int &v=ch[now][s[n-rem+1]];int c=s[start[v]+rem-1];
    			if (!v||x==c){
    				link[last]=now;last=now;
    				if (!v) v=newnode(n-rem+1,inf);
    				else break;
    			}else{
    				int u=newnode(start[v],rem-1);
    				ch[u][c]=v;ch[u][x]=newnode(n,inf);
    				start[v]+=rem-1;len[v]-=rem-1;
    				link[last]=v=u;last=u;
    			} if (now==1) rem--;else now=link[now];
    		}
    	}
    }sft;
    
    void dfs(int u,int f){
    	anc[u][0]=f;
    	for (int i=1;i<=15;++i)anc[u][i]=anc[anc[u][i-1]][i-1];
    	int len=std::min(sft.len[u],n-sft.start[u]+1),isleaf=1;
    	pre[u]=fa[u]=f;max[u]=tag[u]=inf;
    	depth[u]=depth[f]+len;d[u]=d[f]+1;
    	for (int i=0;i<=26;++i)
    		if (sft.ch[u][i])dfs(sft.ch[u][i],u),isleaf=0;
    	if (isleaf)
    		rank[n-depth[u]+1]=u;
    	rk[++tl]=u;
    }
    
    void access(int x,int w) {
    	for (int y=0;x;x=fa[y=x])spaly(x),ch[x][1]=y,cover(x,w);
    }
    
    int query(int x,int r){
    	if(!x)return 0;
    	pushdown(x);
    	if(max[x]<r-depth[pre[x]]+1){
    		int d=query(ch[x][1],r);
    		if(d)return d;else return std::min(depth[x],r-max[x]+1);
    	}return query(ch[x][0],r);
    }
    
    int find(int x,int r){
    	for(int y=0;x!=1&&x;x=fa[y=x]){
    		spaly(x);ch[x][1]=0;
    		int u=x;while(ch[u][0]){pushdown(u);u=ch[u][0];}
    		if(max[u]<r-depth[pre[u]]+1)
    			return query(x,r);
    		ch[x][1]=y;
    	}return 0;
    }
    
    void rebuild(int l){
    	std::memset(dp,63,sizeof(dp));
    	for(int i=l;i<=n;++i)min[rank[i]]=i;
    	for(int i=tl;i>=1;i--)min[anc[rk[i]][0]]=std::min(min[rk[i]],min[anc[rk[i]][0]]);
    	for(int j=2;j<=tl;++j){
    		int i=rk[j];
    		dp[i]=min[i]-depth[i]+1;
    		dp[i]=std::min(dp[i],std::min(dp[anc[i][0]],sft.link[i]!=1?dp[sft.link[i]]:inf));
    	}
    } 
    int locate(int l,int r){
    	int u=rank[l];
    	for (int i=15;i>=0;i--)
    		if (depth[anc[u][i]]>=(r-l+1))u=anc[u][i];
    	return u;
    }int lca(int u,int v){
    	if (d[u]<d[v])std::swap(u,v);
    	for(int i=15;i>=0;i--)if(d[anc[u][i]]>=d[v])u=anc[u][i];
    	if (u==v)return u;
    	for(int i=15;i>=0;i--)if(anc[u][i]!=anc[v][i])u=anc[u][i],v=anc[v][i];
    	return anc[u][0];
    }	
    void bfs(int c){
    	std::queue<int>q;
    	std::memset(vis,0,sizeof(vis));
    	std::memset(dis[c],63,sizeof(dis[c]));
    	for(int v:rc[c]){q.push(v);dis[c][v]=0;}
    	vis[c]=1;
    	while(!q.empty()){
    		int x=q.front();int d=s[x]-'a';q.pop();
    		if(!vis[d]){
    			vis[d]=1;
    			for(int v:rc[d])
    				if(dis[c][v]>n){dis[c][v]=dis[c][x]+1;q.push(v);}
    		}if (x>1&&dis[c][x-1]>n){dis[c][x-1]=dis[c][x]+1;q.push(x-1);}
    		if(x<n&&dis[c][x+1]>n){dis[c][x+1]=dis[c][x]+1;q.push(x+1);}
    	}
    }
    void prework(){
    	std::memset(min,63,sizeof(min));
    	sft.link[1]=1;dfs(1,0);std::sort(rk+1,rk+tl+1,cmp);
    	for(int i=1;i<n;++i)sft.link[rank[i]]=rank[i+1];
    	for(int i=1;i<=n;++i)rc[s[i]-'a'].push_back(i);
    	for(int i=0;i<26;++i)bfs(i);
    }
    int corner_cases(int i,int j,int p){
    	int ans=abs(i-p);
    	for(int c=0;c<26;++c){
    		ans=std::min(ans,j-i+dis[c][i]+dis[c][p]+1);
    		ans=std::min(ans,j-i+dis[c][j]+dis[c][p]+1);
    		if(cnt[c][j]-cnt[c][i-1])ans=std::min(ans,j-i+dis[c][p]+1);
    	}return ans;
    }
    int main(){
    	scanf("%d%d",&n,&q);
    	scanf("%s",s+1);
    	for (int i=1;i<=n;++i){
    		sft.extend(s[i]-'a');
    		for(int c=0;c<26;++c)cnt[c][i]=cnt[c][i-1];
    		cnt[s[i]-'a'][i]++;
    	}
    	sft.extend(26);prework();
    	for(int i=1;i<=q;++i){
    		scanf("%d%d%d",&qr[i].i,&qr[i].j,&qr[i].p);qr[i].id=i;
    		int len=depth[lca(rank[qr[i].i],rank[qr[i].j])];
    		qr[i].j=qr[i].i+len-1;
    	}std::sort(qr+1,qr+q+1,cmp1);
    	int cnt=0,last=n,p=0;
    	rebuild(n);
    	for(int i=n;i>=1;--i){
    		cnt++;
    		if (cnt>B){rebuild(i);last=i;cnt=0;}
    		while(p<q&&qr[p+1].p==i){
    			++p;
    			ans[qr[p].id]=
    			std::min(qr[p].j-qr[p].i+1+dp[locate(qr[p].i,qr[p].j)]-qr[p].p,
    			corner_cases(qr[p].i,qr[p].j,qr[p].p));
    			for(int j=i;j<last;++j){
    				q1[++tail].l=qr[p].i;q1[tail].r=qr[p].j;q1[tail].p=j;
    				q1[tail].id=qr[p].id;
    				q1[tail].k=j-i+qr[p].j-qr[p].i+2;
    			}
    		}
    	}std::sort(q1+1,q1+tail+1,cmp2);
    	p=0;
    	for(int i=n;i>=1;--i){
    		access(rank[i],i);
    		while(p<tail&&q1[p+1].l==i){
                ++p;int d=find(rank[q1[p].p],q1[p].r);
    			if(d)ans[q1[p].id]=std::min(ans[q1[p].id],q1[p].k-d);
            }
    	}
    	for(int i=1;i<=q;++i)ans[qr[i].id]+=n-qr[i].j; 
    	for(int i=1;i<=q;++i)printf("%d\n",ans[i]);
    	return 0;
    }
    
    • 1