[usOJ3382]洪流

题目

传送门 to usOJ

题目描述
给定一个长度为 N N N 的字符串 S S S M M M 个询问,每次询问一个区间 [ l , r ] [l,r] [l,r] 的最大字典序子串。

输入格式
输入共两行。第一行包含两个正整数 N N N M M M ;第二行包含一个长度为 N N N 的字符串 S S S

接下来的 M M M 行,每行包含两个数字 l l l r r r ,表示一个询问。

保证输入的字符串 只由小写字母组成

输出格式
输出共 M M M 行,第 i i i 行输出一个整数,表示你的答案子串的起始位置。

数据范围与约定
对于 20 % 20\% 20% 的数据, N , M ≤ 300 N,M ≤ 300 N,M300
对于 60 % 60\% 60% 的数据, N , M ≤ 10000 N,M ≤ 10000 N,M10000
对于另外 10 % 10\% 10% 的数据,字符串只由 a b c abc abc 三个字母组成。
对于 100 % 100\% 100% 的数据, 1 ≤ N , M ≤ 200000 1 ≤ N,M ≤ 200000 1N,M200000

思路

首先,对于每一个区间 [ l , r ] [l, r] [l,r] ,答案必然是这个区间的一个后缀。否则一定不优。

我们把所有询问离线,按照右端点排序。

从左到右扫描字符串,设当前位置是 p p p ,那么我们处理所有 r = p r=p r=p 的询问。

动态的考虑是非常困难的。我们从更简单的情况出发。

静态询问

每次询问输入一个正整数 x x x ,求左端点不小于 x x x 的后缀中字典序最大的一个。

你可能会说后缀数组。但是很抱歉,后缀数组不适合推广到动态的情况(如果您做到了,说不定可以拿个奖,希望到时候你不会忘记我)。我们考虑朴实的做法——处理一个单调栈。

显然,如果一个后缀在右边,并且其字典序较大,那么一定更优。所以我们维护一个字典序递减的单调栈,每次询问使用二分查找即可。

虽然这样很慢,但是这是可以推广到动态询问的。

动态询问

考虑推广单调栈做法。动态地维护单调栈

说明:请允许我用 ( s a , s a + 1 , s a + 2 , … , s b ) (s_a,s_{a+1},s_{a+2},\dots,s_b) (sa,sa+1,sa+2,,sb) 表示 s s s 的子串,起始位置和终止位置分别为 a , b a,b a,b ( s a , s a + 1 , s a + 2 , … , s p ) (s_a,s_{a+1},s_{a+2},\dots,s_p) (sa,sa+1,sa+2,,sp) 称作 “ 后缀 a a a ” ;单调栈中保存的是一个后缀的左端点。

考虑单调栈中两个相邻的值 a , b ( a < b ) a,b(a<b) a,b(a<b) 。显然,后缀 a a a 的字典序比后缀 b b b 的字典序大。

但是如果 r = n r=n r=n 的时候,后缀 b b b 翻盘了!也就是说,
( s a , s a + 1 , s a + 2 , … , s n ) < ( s b , s b + 1 , s b + 2 , … , s n ) (s_a,s_{a+1},s_{a+2},\dots,s_n)<(s_b,s_{b+1},s_{b+2},\dots,s_n) (sa,sa+1,sa+2,,sn)<(sb,sb+1,sb+2,,sn)

那么随着 p p p 的变大,后缀 b b b 迟早会大于后缀 a a a 的!而且这个 大小关系是稳定的:一旦靠后的后缀变的更大,以后都一直更大。

什么时候 b b b 当老大?设 ( s a , s a + 1 , s a + 2 , … , s n ) (s_a,s_{a+1},s_{a+2},\dots,s_n) (sa,sa+1,sa+2,,sn) ( s b , s b + 1 , s b + 2 , … , s n ) (s_b,s_{b+1},s_{b+2},\dots,s_n) (sb,sb+1,sb+2,,sn) 的最长公共前缀长度为 x x x ,显然有 s a + x < s b + x s_{a+x}<s_{b+x} sa+x<sb+x 。当 p = b + x p=b+x p=b+x 的时候, a a a b b b 干掉了,就可以删掉 a a a 了(最长公共前缀可以用二分 + + +蛤希,不要虚,没有奇技淫巧 但是还是很难)。

这种潜在的威胁,我们称 b b b 支配 a a a 。把支配关系存成边,我们删除所有较劣的就行了。

即使这样,我们似乎还是有 O ( n 2 ) \mathcal O(n^2) O(n2) 条边。复杂度还是不可接受的。

研究一下这个支配关系,很有意思的一点是,如果 a a a b b b 干掉了,那么所有被 a a a 支配的也可以被干掉。这就是在说,支配关系有传递性。有图为证:

支配传递
最下面的是 b b b ,中间的是 a a a ,最上面的是被 a a a 支配的 c c c 。因为 b b b 支配 a a a ,不妨设 R e d \color{Red}{Red} Red 的部分就是 a , b a,b a,b 的最长公共前缀,一定有 O r a n g e \color{Orange}{Orange} Orange 的位置 b b b 更大(即 B i g \color{Orange}{Big} Big S m a l l \color{orange}{Small} Small)。

因为 c c c a a a 支配,并且 c c c 还没有被 a a a 干掉(已经被干掉那还想啥啊),所以橙色的部分及其以前都是 a , c a,c a,c 的公共前缀。所以, c c c O r a n g e \color{Orange}{Orange} Orange 部分也是 S m a l l \color{Orange}{Small} Small 。显然, c c c b b b 支配!

也就是说,既然 c c c 已经被 a a a 所支配,那么 b b b 支配 c c c 这一点,可以通过 b b b 支配 a a a 所体现。那么我们就不用求 b b b 是否支配 c c c 了。

这提示我们,已经被支配的后缀,我们不应该继续考虑它们。所以我们用一个单调栈,如果栈顶被当前的后缀所支配就弹栈。支配关系只由完全后缀(在整个字符串中的后缀)决定,完全后缀字典序大的支配字典序小的,所以我们只需要维护完全后缀字典序递减。

确实还是不懂,请看代码。

数据结构

由于我们的 “单调栈” 需要进行删除操作,所以不可以使用 s t a c k \tt{stack} stack ,而是使用平衡树,比如 s e t \tt{set} set

复杂度

由于是单调栈,每次删除有一次 lcp \text{lcp} lcp 询问,总复杂度 O ( n log ⁡ n ) \mathcal O(n\log n) O(nlogn) ;每个询问要进行二分查找,复杂度 O ( m log ⁡ n ) \mathcal O(m\log n) O(mlogn) set \text{set} set 要进行 O ( n ) \mathcal O(n) O(n) 次修改、删除,总复杂度 O ( n log ⁡ n ) \mathcal O(n\log n) O(nlogn) ;删除时的递归显然只有 O ( n ) \mathcal O(n) O(n) ——边的数量稳定在 O ( n ) \mathcal O(n) O(n)

总结一下,总复杂度 O [ ( n + m ) log ⁡ n ] \mathcal O[(n+m)\log n] O[(n+m)logn] 。完美!

代码

#include <cstdio>
#include <iostream>
#include <vector>
#include <algorithm>
// 据说max和min是algorithm里面的? 
#include <set>
using namespace std;
typedef unsigned long long ULL;
const int MAXL = 200010;
const ULL SEED = 137;
int N, M; char str[MAXL];
ULL hashv[MAXL], powr[MAXL]; // hash的辅助数组 
ULL hinv(int a,int b){ // (s_a,...,s_b)的hash值 
	return hashv[b]-hashv[a-1]*powr[b-a+1];
}
int getLCP(int a,int b){
	// 求最长公共前缀 
	int l = 0, r = N-max(a,b)+1, ans = 0;
	while(l <= r){ // 二分长度 
		int mid = (l+r)>>1;
		if(hinv(a,a+mid-1) == hinv(b,b+mid-1))
		// 前mid个字符相同 
			l = mid+1, ans = mid;
		else r = mid-1;
	}
	return ans;
}
set<int> st; // “单调栈”;存储f值 
vector<pair<int,int> > qry[MAXL]; // 存储询问 
vector<int> del[MAXL]; // 在p=i时删掉所有del[i] 
struct Node{ // 前向星 
	int v, nxt;
}d[MAXL<<1]; // 支配关系当成边 
int head[MAXL], etot;
inline void addEdge(int a,int b){
	etot ++; // 加边(支配关系) 
	d[etot].v = b;
	d[etot].nxt = head[a];
	head[a] = etot;
}
bool vis[MAXL];
void dfs(int u){ // 递归删除被干掉的 
	if(vis[u]) return ; // 已经被干掉了 
	vis[u] = true, st.erase(u);
	for(int e=head[u]; e; e=d[e].nxt)
		dfs(d[e].v);
}
int stk[MAXL], stktop, ans[MAXL];
// 数组模拟栈;答案(因为我们是离线处理) 
int main () {
	scanf("%d %d",&N,&M);
	scanf("%s",str+1);
	for(int i=1,a,b; i<=M; i++){
		scanf("%d %d",&a,&b);
		qry[b].push_back(make_pair(i,a));
		// 离线所有询问 
	}
	powr[0] = 1;
	for(int i=1; i<=N; i++){ // hash一波 
		hashv[i] = hashv[i-1]*SEED+str[i];
		powr[i] = powr[i-1]*SEED;
	} // SEED的i次方=powr[i],hash要用 
	for(int i=1; i<=N; i++){
		while(stktop){ // stktop是闭区间 
			int j = stk[stktop];
			int x = getLCP(j,i);
			// i 不支配 j <=> 后缀 i 字典序较小 
			if(str[j+x] > str[i+x])
				break;
			// 否则i支配j 
			del[i+x].push_back(j);
			// 在p=i+x时,i强于j 
			addEdge(i,j); // 添加支配关系 
			stktop --; // 弹栈 
		}
		stk[++ stktop] = i; // 塞到栈里 
		st.insert(i); // 加入当前点, 作为长度为 1 的后缀 
		for(auto x : del[i]) dfs(x); // 删点 
		for(auto x : qry[i]) // 处理询问
			ans[x.first] = *st.lower_bound(x.second);
	}
	for(int i=1; i<=M; i++)
		printf("%d\n",ans[i]);
}

后记

这是那场考试的 T1 \text{T1} T1,我一来就和它杠上了……

名称第一题第二题第三题总分
OneInDark \text{OneInDark} OneInDark 0 0 0 0 0 0 0 0 0 0 0 0
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值