【十二省联考 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;
}
posted @ 2019-08-06 19:58  大本营  阅读(395)  评论(0编辑  收藏  举报