给出只包含小写字母 \(a,b\) 的字符串 \(s,t\),\(q\) 次询问,每次询问 \(s[l,r]\) 和 \(t\) 的最长公共子串长度。\(|s|,|t|,q\leqslant 2\times 10^5\)。
首先,我们有一个很经典的套路就是用SA或者SAM求出 \(s\) 每个位置最多能向前扩展多长,使得这个子串是 \(t\) 的子串。
说一下用SAM实现的做法:先把 \(t\) 的自动机建出来,然后让 \(s\) 在 \(t\) 的自动机上匹配。我们设每一位的答案为 \(ans_i\) ,再开一个变量 \(now\) 表示当前在哪个节点。
我们一位一位添加进 \(s\) ,假设本次添加的字符为 \(c\),当 \(now\) 有 \(c\) 的出边时,让 \(ans_i=ans_{i-1}+1\) (注意这里并不是 \(len_{now}+1\) ,因为当前匹配上的串不一定是 \(now\) 节点代表的最长串),然后 \(now=ch[now][c]\);
如果没有,我们沿着后缀链接向上跳 \(fa\) ,直到跳到虚拟节点或者找到了一个有 \(c\) 的出边的节点。
如果跳到了虚拟节点,即 \(t\) 根本就不含 \(c\) 这个字符,那么使 \(ans[i]=0,now=0\) (\(0\) 为根节点,\(-1\) 为虚拟节点);
如果找到了 \(c\) 的出边,则 \(ans_i=len_{now}+1,now=ch[now][c]\) (此处的 \(now\) 是你跳到的那个点)。这里为什么是 \(len_{now}+ 1\) 呢?因为我们是从某一个儿子节点跳到这里的,而儿子节点代表的子串都严格比父节点的子串长,所以只要跳一次开始,就包含了这个节点的所有串。这样的总复杂度是线性的,因为匹配长度最多只增加 \(|s|\) 次,所以跳 \(fa\) 的总复杂度也是 \(O(|s|)\) 的。
现在我们有了 \(ans\) 数组,怎么求出真正的答案呢?不难发现答案就是 \(\max\limits_{i=l}^r\min(ans_i,i-l+1)\) ,我们把所有位置按取得 \(\min\) 的是哪一种分成两类,我们发现随着 \(l\) 的增加,会由 \(ans_i\) 取得 \(\min\) 变为 \(i-l+1\) 取得 \(\min\) ,而这个位置为 \(i-ans_i+1\) ,所以我们可以按 \(l\) 离线下来所有询问,开两棵线段树维护两种的答案(单点修改和查询区间 \(\max\)),再给每个位置开个桶,把 \(k-ans_k+1=i\) 的都扔进去,然后当 \(l\) 扫到这个位置就更改就好了。当然你也可以用可持久化线段树来在线处理询问,就是空间多了个 \(\log\) ,代码采用的是离线方法。
代码如下:
1 |
|