题目
题目描述
给定一个长度为
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,M≤300 。
对于
60
%
60\%
60% 的数据,
N
,
M
≤
10000
N,M ≤ 10000
N,M≤10000 。
对于另外
10
%
10\%
10% 的数据,字符串只由
a
b
c
abc
abc 三个字母组成。
对于
100
%
100\%
100% 的数据,
1
≤
N
,
M
≤
200000
1 ≤ N,M ≤ 200000
1≤N,M≤200000 。
思路
首先,对于每一个区间 [ 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 |