[雅礼集训 2017 Day7]事情的相似度

\(\text{update on 21/8/17}:\)处理了一下排版问题,以及新增 \(\rm LCT\) 做法。

如果没有原题测试,还不知道这道题还要被咕多久......本来就已经咕了一个寒假了......

壹、题目描述 ¶

传送门 to LOJ

贰、题解 ¶

启发式合并

最长公共后缀,很难不让人想到 \(\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\).

而后就是,我们除了从询问考虑有哪些情况符合条件以外,我们还可以反着进行考虑 —— 一种情况会对哪些询问产生可能的贡献,在这道题中,我们使用这种思路发现了二维偏序的关系,进而引导我们对算法进行优化。

posted @ 2021-03-09 22:08  Arextre  阅读(67)  评论(0编辑  收藏  举报