【十二省联考 2019】字符串问题
以前写完题后鸽了博客,现在补一下。
今天下午机房的人在大屏幕上一直挂着 IOI 的榜,关注阿塞拜疆那边的比赛情况,我本来是衷心祝愿中国队能有人阿克 Day1 的(zzq?),然而六点后回来发现只有本杰明阿克了(实际上他还是三小时就阿克了),中国队最高的是 zzq ,和俄罗斯的 300iq 好像是并列第三?可惜了没能遂 AK 之愿,不过还是恭喜啊
但是 wxh 好像就比较萎了,在中国队垫底……
听说俄罗斯队目前团体 rk1,🐮🍺
题意
给定字符串 \(s\),然后给定 \(n_a\) 个 \(A\) 类区间和 \(n_b\) 个 \(B\) 类区间,再给定 \(m\) 条从第一类区间连向第二类区间的边,一个第二类区间要连向一个第一类区间 当且仅当前者是后者的前缀。每个第一类区间的权值是区间长度,求这张图上的最长路(无限长则输出 \(-1\))。
\(n_a,n_b,|s|,m\le 2\times 10^5\)
题解
如果图直接给你,那这就是个普及组的拓扑求最长路了,若有环则输出 \(-1\),否则拓扑排序递推出最长路。
然而这道题不直接给你图,但建出来图后就变成了上面的普及组问题了。下面考虑怎么建图。
第一类区间连向第二类区间的边已经给你了,在 SAM 上给对应的两点连边就醒了。
第二类区间连向第一类区间怎么处理?不难发现 SAM 的 fail 树就是一个天然的压缩版后缀 trie(就是把原串的所有后缀插入一个 trie 并进行路经压缩),一个点的祖先对应的子串是这个点的子串的后缀。那么反串的 fail 树就是正串的前缀 trie 了,一个点的祖先对应的子串是这个点的子串的前缀,我们姑且叫它前缀树。
你有可能会问:什么?正串的 SAM 本身的边不就是一个串的前缀连向这个串么? 建议你去看看 SAM 本身是不是一棵树再说 这里就可以看
于是我们 \(O(1)\) 找到 SAM 上左端点与第二类区间左端点相同的后缀 对应的节点,然后从这个点倍增跳 fail 边,找到包含这个第二类区间的节点(跳到这个点的 $len\ge $ 区间长度,父亲的 $len\lt $ 区间长度为止)。
在前缀树上,这个点所代表的子串 是以这个点为根的子树中的所有点代表的子串 的前缀,所以这个点需要连向子树内的所有点。这就是第二类区间连向第一类区间的情况。
然后问题来了:怎么向子树内的所有点连边?
线段树优化建图前缀树本身不就是一个性质优良的优化建图工具嘛!
所以在前缀树上补上给定的 \(m\) 条第一类区间连向第二类区间的边后,直接拓扑求最长路就行了。
经过一两个小时的 rush,你测一发样例发现最后一个数误判成了 \(-1\)
做法哪里出问题了吗?
观察题目和数据,发现数据里面有一列 \(|A_i|\ge |b_j|\) 并且前 \(80\%\) 的数据都是“保证”
这个是干嘛用的?
仔细思考一下,发现第二类区间连向第一类区间的操作 是有问题的。我们在前缀树上定位一个第二类区间,定位到了某个点,但这个点包含了一些后缀相同、长度连续的子串,而这个第二位区间只是其中一个。将它向子树内所有点连边没毛病,但对于这个点本身,它有可能向这个点连进来一个长度为 \(len\) 的第二类区间,然后从这个点连出去一个长度 \(\lt len\) 的第一类区间。显然前者是不能走到后者的,因为第二类区间作为第一类区间的前缀 长度肯定不能大于第一类区间。但 \(|A_i|\ge |b_j|\) 的限制保证了不存在上述问题。
那么对于 \(|A_i|\lt |b_j|\) 的情况,这个点得以这个第二类区间的长度 \(len\) 为界 拆成两个点,如果从长度大的点进来一个第二类区间,那么它不能走到长度小的点找第一类区间。
由于定位到一个点的区间可能很多,所以一个点可能拆成很多点,疯狂写写就可以了。这里用的是这样一种方法:
na=read(), siz=tot;
rep(i,1,na) ins(1), a[i]=siz;
nb=read();
rep(i,1,nb) ins(0), b[i]=siz;
void ins(bool x){
int l,r;
l=read(), r=read();
r=r-l+1, l=id[l];
dwn(i,19,0) if(st[l][i] && len[st[l][i]]>=r) l=st[l][i];
isA[++siz]=x, len[siz]=r, g[l].push_back(siz);
}
rep(i,1,tot) sort(g[i].begin(),g[i].end(),cmp);
rep(i,1,tot){
int tmp_lst=i;
dwn(j,g[i].size()-1,0){
int now=g[i][j];
add(tmp_lst,now);
if(!isA[now]) tmp_lst=now;
}
Lst[i]=tmp_lst;
}
大致就是,对于每个 定位到该点且长度等于该点长度区间左端点的区间,在该点记录其长度 \(len_i\) 以及是否来自第二类区间。
然后把每个点的所有 \(len_i\) 以 \(len_i\) 从小到大为第一关键字,第二类区间优先为第二关键字排序。
把这个点拆成定位到该点的若干个第二类区间对应的点,拆出来的一个点的长度 就是其对应的第二类区间的长度(注意这里不同于 SAM,拆出来的点的长度就是一个值,不是一个区间,因为拆出来的一个点对应一个长度唯一的第二类区间)。从短到长把它们串起来。然后每个点上长一些毛,连向长度位于 \([该点长度, 下一个点长度)\) 之间的第一类区间。
这样一来,只有经过一堆长度小的第二类点后,才能到达恰好一个长度大的第一类点。
然后前缀树上一个点拆出来的长度最长的点(即最下面一个点)连向其在前缀树上的每个儿子拆出来的长度最短的点(即最上面一个点)。
关注建出的图,实际上就是把前缀树上的每个点拆成了一堆从上到下串起来的点,然后上下端连前缀树的边,这就是前缀树优化建图的关键。
考虑复杂度。因为 \(n_a,n_b,|s|,m\) 最大值同级,下面假设它们都是 \(n\)。
我们在前缀树上做了倍增(好像树剖常数更小?),所以复杂度有了一个 \(O(n\log n)\)。
然后对每个点的 \(len\) 数组都进行了排序。因为一个第一/第二类区间只会给某个点增加一个 \(len_i\),所以所有点的 \(len\) 数组大小之和不超过 \(2n\),对这些数组排序的总复杂度是 \(O(\sum_i len.size()\times \log len.size()) \le O(n\log n)\)(证明:假设一个元素在大小为 \(s\) 的数组中,这个点会贡献 \(O(\log s)\) 的复杂度,那么 \(2n\) 个点一共会贡献不超过 \(O(n\log n)\) 的复杂度。
前缀树本身最多有 \(2n\) 个点(就是 SAM 的点数);两类区间的数量最多均为 \(n\) 个,而每一个区间会在前缀树上的某点拆出一个新点,所以前缀树不会超过 \(4n\) 个点。对 \(4n\) 个点跑拓扑求最长路的复杂度是 \(O(n)\) 的。
三部分复杂度相加,总复杂度 \(O(n\log n)\)。
总结一下,这题就这么几个步骤:在前缀树上倍增定位,拆点,拓扑求最长路。
剩下的看代码理解吧
#pragma GCC optimize(2)
#include<bits/stdc++.h>
#define rep(i,x,y) for(int i=(x);i<=(y);++i)
#define dwn(i,x,y) for(int i=(x);i>=(y);--i)
#define rep_e(i,u) for(int i=hd[u];i;i=e[i].nxt)
#define ll long long
#define N 1200002
using namespace std;
inline int read(){
int x=0; bool f=1; char c=getchar();
for(;!isdigit(c);c=getchar()) if(c=='-') f=0;
for(; isdigit(c);c=getchar()) x=(x<<3)+(x<<1)+(c^'0');
if(f) return x;
return 0-x;
}
char s[200002];
int n,m,na,nb,a[N],b[N],id[N],rt,Lst[N],in[N];
vector<int> g[N]; ll dis[N];
struct edge{int v,nxt;}e[N];
int hd[N],cnt;
inline void add(int u,int v){e[++cnt]=(edge){v,hd[u]}, hd[u]=cnt, in[v]++;}
namespace SAM{
int tot,lst,siz;
int len[N],to[N][27],nxt[N],st[N][22];
bool isA[N];
void extend(int i){
int u=++tot, v=lst, c=s[i]-'a'; lst=u, len[u]=len[v]+1;
for(; v && !to[v][c]; v=nxt[v]) to[v][c]=u;
if(!v) nxt[u]=rt;
else{
int o=to[v][c];
if(len[o]==len[v]+1) nxt[u]=o;
else{
int t=++tot; len[t]=len[v]+1;
memcpy(to[t],to[o],sizeof to[o]);
nxt[t]=nxt[o], nxt[o]=nxt[u]=t;
for(; v && to[v][c]==o; v=nxt[v]) to[v][c]=t;
}
}
}
void ins(bool x){
int l,r;
l=read(), r=read();
r=r-l+1, l=id[l];
dwn(i,19,0) if(st[l][i] && len[st[l][i]]>=r) l=st[l][i];
isA[++siz]=x, len[siz]=r, g[l].push_back(siz);
}
/*
int c[N];
void makeSufTree(){
rep(i,1,tot) c[len[i]]++;
rep(i,1,n) c[i]+=c[i-1];
rep(i,1,tot) ord[c[len[i]]--]=i;
rep(i,1,tot){
int j=ord[i];
st[i][0]=fa[i];
}
}
*/
}
using namespace SAM;
ll ans;
inline bool cmp(const int &x,const int &y){
return len[x]>len[y] || (len[x]==len[y] && isA[x]>isA[y]);
}
int main(){
int T=read();
rt=1;
while(T--){
ans=0;
scanf("%s",s+1);
n=strlen(s+1);
lst=tot=1;
dwn(i,n,1) extend(i), id[i]=lst;
rep(i,1,tot)st[i][0]=nxt[i];
rep(j,1,19)
rep(i,1,tot) st[i][j]=st[st[i][j-1]][j-1];
na=read(), siz=tot;
rep(i,1,na) ins(1), a[i]=siz;
nb=read();
rep(i,1,nb) ins(0), b[i]=siz;
rep(i,1,tot) sort(g[i].begin(),g[i].end(),cmp);
rep(i,1,tot){
int tmp_lst=i;
dwn(j,g[i].size()-1,0){
int now=g[i][j];
add(tmp_lst,now);
if(!isA[now]) tmp_lst=now;
}
Lst[i]=tmp_lst;
}
rep(i,2,tot) add(Lst[nxt[i]],i);
rep(i,1,siz) if(!isA[i]) len[i]=0;
m=read();
int x,y,u,f=0;
rep(i,1,m) x=read(), y=read(), add(a[x],b[y]);
queue<int> Q;
rep(i,1,siz) if(!in[i]) Q.push(i);
while(!Q.empty()){
u=Q.front(), Q.pop();
ans=max(ans,dis[u]+len[u]);
rep_e(i,u){
dis[e[i].v]=max(dis[e[i].v],dis[u]+len[u]);
if(!--in[e[i].v]) Q.push(e[i].v);
}
}
bool flag=0;
rep(i,1,siz) if(in[i]){flag=1; break;}
if(flag) printf("-1\n");
else cout<<ans<<endl;
rep(i,1,tot) nxt[i]=0, memset(to[i],0,sizeof to[i]);
rep(i,1,siz) g[i].clear(), isA[i]=len[i]=hd[i]=dis[i]=in[i]=0;
cnt=siz=0;
}
return 0;
}