[雅礼集训 2017 Day7]事情的相似度
\(\text{update on 21/8/17}:\)处理了一下排版问题,以及新增 \(\rm LCT\) 做法。
如果没有原题测试,还不知道这道题还要被咕多久......本来就已经咕了一个寒假了......
壹、题目描述 ¶
贰、题解 ¶
启发式合并
最长公共后缀,很难不让人想到 \(\tt SAM\) 的 \(\text{parent tree}\),因为在 \(\text{parent tree}\) 上有:两个串的最长公共后缀,即为他们所代表的点在树上的 \(LCA\) 对应的 \(\tt maxlen\).
有了这个东西怎么做?我们考虑如果我们在树上有一对终点对 \(\lang x, y\rang(x<y)\),他们在树上代表的点的 \(LCA\) 是 \(u\),那么我们就已知 \(LCS(x,y)=\tt len[u]\),同时考察这一对终点对于哪些询问会产生贡献:类似二维偏序,对于询问 \(\lang L, R\rang\),只要满足 \(L\le x,y\le r\) 便可以考虑一下这一对终点是否是最优解。
那么我们有一种暴力做法:暴力维护每个的 \(\tt endpos\),然后将 \(\tt endpos\) 集合中的每对终点都记录一下,这样最多会有 \(\mathcal O(n^2)\) 级别的点对,但是考虑一下二维偏序的过程,编号相邻的终点对才是能够被最多的集合包含的点对,所以对于不相邻的点对,我们实际上没有必要去保存他们。
而维护 \(\tt endpos\) 集合,我们考虑使用启发式合并,处理询问用扫描线加上树状数组,最后复杂度 \(\mathcal O(n\log^2 n)\).
LCT
事实上也可以暴力维护。在 Luogu 上有大佬给出这种题型的一般处理方法:
对于一个固定的右端点 \(i\),贪心地只考虑 \(i\) 之前每种元素最后出现的位置。按下标从左到右扫描线,用一棵线段树维护每个位置是否在前缀 \(i\) 种最后一次出现。加入第 \(i\) 个元素时,在线段树中把当前位置 \(+1\),把上一个相同元素的位置 \(-1\). 询问直接区间查询即可。
- 事实上我们也可以理解为一种扫描线,依次扫描每个右端点,对于每个右端点,处理左端点的值,当该右端点处理完之后,又将其当做下一个 \(r\) 的待选左端点;
此处,我们也可以采用此类方法,依次扫描每个右端点,对于一个确定的 \(r\),一个询问 \([L,r]\) 事实上就是找所有 \(l\ge L\) 的最大的 \(LCS(s[1:l],s[1:r])\),但是有几个问题:
- 如何快速处理每个 \(\text{LCS}(s[1:l],s[1:r])\);
- 如何快速询问 \(\max\{\text{LCS}(s[1:l],s[1:r])\mid l\ge L\}\) ?
不难发现询问就是后缀最大值,我们可以使用 \(\rm BIT\) 进行维护,但是难点在于处理每个 \(\rm LCS\).
都涉及 \(\rm LCS\) 了,很显然需要用到 \(\rm SAM\),在 \(\rm SAM\) 的树上,两个前缀的 \(\rm LCS\) 就是他们所在点的 \(\rm LCA\) 的 \(\rm len\),并且,我们可以采用 这道题 的做法,将每个左端点到根都染上颜色,处理右端点的时候,看从它对应点到根的路径上,遇到了哪些左端点,遇到了就在 \(\rm BIT\) 上进行更新。在染色的时候,显然编号越大的左端点比编号小的更优(更容易满足 \(i\ge L\) 的条件),所以可以直接进行颜色覆盖。
至于染色和爬树的过程,就和 \(\rm LCT\) 的 \(\rm access\) 函数特别相似,我们考虑采用 \(\rm LCT\) 进行维护。
时间复杂度 \(\mathcal O(n\log^2 n)\),和启发式合并一样呢(但是代码难实现很多)(其实也不是特别难实现)。
叁、参考代码 ¶
启发式合并
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<set>
using namespace std;
const int maxn=100000+10;
const int maxm=100000+10;
const int sigma=2;
int trie[maxn<<1][sigma];
int fa[maxn<<1];
int len[maxn<<1];
set<int>endpos[maxn<<1];
int lst=1, ncnt=1;
inline void add(const int pos, const int c){
int p=lst;
int u=lst=++ncnt;
len[u]=len[p]+1;
endpos[u].insert(pos);
for(; p && !trie[p][c]; p=fa[p])
trie[p][c]=u;
if(!p) fa[u]=1;
else{
int q=trie[p][c];
if(len[q]==len[p]+1) fa[u]=q;
else{
int x=++ncnt;
fa[x]=fa[q], len[x]=len[p]+1;
for(int i=0; i<sigma; ++i) trie[x][i]=trie[q][i];
fa[u]=fa[q]=x;
for(; p && trie[p][c]==q; p=fa[p])
trie[p][c]=x;
}
}
}
struct edge{
int to, nxt;
edge(){}
edge(const int T, const int N): to(T), nxt(N){}
}e[maxn<<1];
int tail[maxn<<1], ecnt;
inline void add_edge(const int u, const int v){
e[ecnt]=edge(v, tail[u]); tail[u]=ecnt++;
}
int n, m;
char s[maxn];
inline void input(){
scanf("%d %d", &n, &m);
scanf("%s", s+1);
for(int i=1; i<=n; ++i)
add(i, s[i]-'0');
}
inline void buildtre(){
memset(tail, -1, sizeof tail);
for(int i=2; i<=ncnt; ++i)
add_edge(fa[i], i);
}
struct data{
int u, v, len;
data(){}
data(const int U, const int V, const int L): u(U), v(V), len(L){}
inline int operator <(const data rhs) const{
if(v!=rhs.v) return v<rhs.v;
if(u!=rhs.u) return u<rhs.u;
return len<rhs.len;
}
}p[maxn<<5];
int pcnt;
struct query{
int l, r, id;
query(){}
query(const int L, const int R, const int I): l(L), r(R), id(I){}
inline int operator <(const query rhs) const{
if(r!=rhs.r) return r<rhs.r;
return l<rhs.l;
}
}q[maxn];
int ans[maxn];
inline void dfs(const int u){
for(int i=tail[u], v; ~i; i=e[i].nxt){
dfs(v=e[i].to);
if(!len[u]) continue;
if(endpos[u].size()<endpos[v].size())
swap(endpos[u], endpos[v]);
set<int>::iterator it, now, pre, nxt;
for(it=endpos[v].begin(); it!=endpos[v].end(); ++it){
endpos[u].insert(*it);
now=pre=nxt=endpos[u].find(*it);
if(pre!=endpos[u].begin()){
--pre;
p[++pcnt]=data(*pre, *now, len[u]);
}
if(++nxt!=endpos[u].end()){
p[++pcnt]=data(*now, *nxt, len[u]);
}
endpos[u].erase(*it);
}
for(it=endpos[v].begin(); it!=endpos[v].end(); ++it)
endpos[u].insert(*it);
}
}
int c[maxn+5];
#define lowbit(i) (i&(-i))
inline void modify(int i, const int x){
for(; i; i-=lowbit(i)) c[i]=max(c[i], x);
}
inline int query(int i){
int ret=0;
for(; i<=n; i+=lowbit(i)) ret=max(ret, c[i]);
return ret;
}
signed main(){
input();
buildtre();
dfs(1);
sort(p+1, p+pcnt+1);
for(int i=1; i<=m; ++i){
scanf("%d %d", &q[i].l, &q[i].r);
q[i].id=i;
}
sort(q+1, q+m+1);
for(int i=1, j=1; i<=m; ++i){
while(j<=pcnt && p[j].v<=q[i].r)
modify(p[j].u, p[j].len), ++j;
ans[q[i].id]=query(q[i].l);
}
for(int i=1; i<=m; ++i) printf("%d\n", ans[i]);
return 0;
}
LCT
const int maxn=2e5;
const int sigma=2;
int n, m;
int a[maxn+5], pos[maxn+5];
vector<pii>Q[maxn+5];
int ans[maxn+5];
namespace SAM{
int trie[maxn+5][sigma], fa[maxn+5];
int len[maxn+5], ncnt=1, lst=1;
inline int add(int c){ // return the corresponding node
int p=lst, u=lst=++ncnt;
len[u]=len[p]+1;
for(; p && !trie[p][c]; p=fa[p])
trie[p][c]=u;
if(!p) fa[u]=1;
else{
int q=trie[p][c];
if(len[q]==len[p]+1) fa[u]=q;
else{
int nq=++ncnt;
for(int j=0; j<sigma; ++j)
trie[nq][j]=trie[q][j];
fa[nq]=fa[q], len[nq]=len[p]+1;
fa[q]=fa[u]=nq;
for(; p && trie[p][c]==q; p=fa[p])
trie[p][c]=nq;
}
}
return u;
}
}
namespace BIT{
int mx[maxn+5];
#define lowbit(i) ((i)&(-(i)))
inline void modify(int i, int v){
for(; i; i-=lowbit(i))
mx[i]=max(mx[i], v);
}
inline int query(int i){
int ret=0;
for(; i<=n; i+=lowbit(i)) ret=max(ret, mx[i]);
return ret;
}
}
namespace lct{
int son[maxn+5][2], fa[maxn+5];
int col[maxn+5];
inline bool isroot(int x){
return son[fa[x]][0]!=x && son[fa[x]][1]!=x;
}
inline void paint(int x, int c){ col[x]=c; }
inline void pushdown(int x){
if(~col[x]){
if(son[x][0]) paint(son[x][0], col[x]);
if(son[x][1]) paint(son[x][1], col[x]);
}
}
inline void connect(int x, int y, int d){
son[x][d]=y, fa[y]=x;
}
/** @warning should pushdown first */
inline void rotate(int x){
int y=fa[x], z=fa[y], d=(son[y][1]==x);
fa[x]=z; if(!isroot(y)) son[z][son[z][1]==y]=x;
connect(y, son[x][d^1], d);
connect(x, y, d^1);
}
inline void splay(int x){
static int stk[maxn+5], ed=0; int now=x;
while(stk[++ed]=now, !isroot(now)) now=fa[now];
while(ed) pushdown(stk[ed--]);
int y, z;
while(!isroot(x)){
y=fa[x], z=fa[y];
if(!isroot(y))
(son[z][1]==y)^(son[y][1]==x)? rotate(x): rotate(y);
rotate(x);
}
}
inline void access(int x, int new_col){
for(int pre=0; x; pre=x, x=fa[x]){
// should update the son first
// cause we should color the new chain instead of the old one
// but for the speciality of the option, update son later is alse okay but wrong
splay(x), son[x][1]=pre;
if(~col[x]) BIT::modify(col[x], SAM::len[x]);
paint(x, new_col);
}
}
inline void init(){
rep(i, 1, SAM::ncnt)
fa[i]=SAM::fa[i], col[i]=-1;
}
}
inline void input(){
n=readin(1), m=readin(1);
rep(i, 1, n) scanf("%1d", &a[i]);
rep(i, 1, n) pos[i]=SAM::add(a[i]);
}
inline void getQuery(){
int l, r;
rep(i, 1, m){
l=readin(1), r=readin(1);
Q[r].push_back({l, i});
}
}
inline void scanLine(){
for(int r=1; r<=n; ++r){
lct::access(pos[r], r);
for(auto [l, id]: Q[r])
ans[id]=BIT::query(l);
}
}
inline void print(){
rep(i, 1, m) writc(ans[i]);
}
signed main(){
input();
getQuery();
lct::init();
scanLine();
print();
return 0;
}
肆、用到の小 \(\tt trick\) ¶
十分经常被用到的 \(\tt trick\),关于 \(\text{parent tree}\) 的结论:
两个串的最长公共后缀,即为他们所代表的点在树上的 \(LCA\) 对应的 \(\tt maxlen\).
而后就是,我们除了从询问考虑有哪些情况符合条件以外,我们还可以反着进行考虑 —— 一种情况会对哪些询问产生可能的贡献,在这道题中,我们使用这种思路发现了二维偏序的关系,进而引导我们对算法进行优化。