后缀数组学习笔记

前言

后缀数组(Suffix Array,简称 SA)是一种解决某些字符串问题的常用工具。解决这些字符串问题时,经常用后缀数组对问题进行一定的转化成其它的模型,然后套用那个模型的解决方法加以解决原问题。

附题单

约定

本文做以下约定:

  • 本文撰写时间跨度较大,有些符号会出现正体、斜体混用的情况,请读者甄别。

  • Σ 为字符集。具体的字符(串)使用⌈打印机字体⌋表示,如 lzyqwq。用 |s| 表示字符串 s 的长度。本文中字符串的下标1 开始,代码中视实现方便程度可能会有所差异。

  • c1c2ck 表示字符 c1ck 从左往右依次拼接形成的字符串。记 si 为字符串 s 从左至右的第 i 个字符。若 i>|s|,则认为它为空字符。

  • LCP(s,t) 为字符串 s,t最长公共前缀。形式化地,|LCP(s,t)|=l,当且仅当 j[1,l],sjtjsl+1tl+1。下文有时会简称为 LCP

  • 称一个字符串 s字典序比字符串 t 小,记 |LCP(s,t)|=l,当且仅当 j[1,l],sjtjsl+1<tl+1认为空字符的字典序极小

  • s[l,r] 为字符串 s 删去前 l1 个字母和后 |s|r 个字母得到的字符串,称其为字符串 slr 的子串。形式化地,s[l,r]=slsl+1sr。显然,s[1,i] 为字符串 s 的一个前缀,s[i,|s|] 为字符串 s 的一个后缀。

  • 对于字符串 s,记 prei=s[1,i]sufi=s[i,|s|]

  • rki 表示将字符串 s 的所有后缀按照字典序从小到大排序后,后缀 s[i,|s|] 的排在第几位。称为后缀 s[i,|s|] 的排名。

  • sai 表示排名为 i 的后缀的起始位置。形式化地,若 sai=j,则 rkj=i,即 sarki=i

构建

后缀数组最初被用来解决这样一个问题:

P3809 【模板】后缀排序

  • 给出一个字符串 s,对于 i[1,|s|],求 sai

  • |s|106

表面上这题要求我们求 sai,其实我们可以求出 rki,然后根据 sarki=i 求出 sai。即我们需要对所有后缀进行排序。

【方法一:取出所有后缀并进行排序】

这就是最暴力求解后缀数组的方法,时间复杂度为 O(|s|2log|s|),空间复杂度为 O(|s|2)

提交记录(UNAC 45pts)

代码

【方法二:字符串哈希加速比较】

方法一的效率主要低在比较两个字符串的大小。根据前文对字典序的定义,我们可以用二分 + 字符串哈希找到两个后缀的 LCP,然后比较下一位字符。

这样一来,单次比较的时间复杂度为 O(log|s|),总时间复杂度为 O(|s|log2|s|),空间复杂度为 O(|s|)

提交记录(卡常后 AC)

代码

【方法三:倍增法】

考虑将 s 的所有后缀的长度用空字符补齐至长度为 |s| 后,再用空字符继续补齐为 2log2|s|+1,显然后缀之间的大小关系不变。

问题转化为,将以某个位置开头,长度为 2log2|s|+1 的用空字符补齐后的字符串排序。

str(p)i 表示以 i 为开头,长度为 2p 的用空字符补齐后的字符串,rk(p)i 为其排名。那么 rk(0) 就是单个字符之间的比较,容易求得。考虑如何用 rk(p) 求得 rk(p+1)

l=2p

我们对于每一个位置 i 维护一个二元组 (rk(p)i,rk(p)i+l),并将这些二元组以第一维为第一关键字 ,第二维为第二关键字进行排序,就可以得到 rk(p+1)i 了。若 i>|s|,则对于 p[0,log2|s|+1]rk(p)i=0。这一步是为了令空字符(串)的字典序极小。

简单理解一下,因为字典序是从左往右比较的,如果左边的半段不一样就比较左边半段,否则比较右边半段。严谨证明的话考虑第一个不同的位置位于哪一半,结合字典序大小关系的定义容易得出上面这个结论是对的。

那么我们需要进行 O(log|s|) 层排序,若每层排序时间复杂度为 O(T(|s|)),则总时间复杂度为 O(T(|s|)log|s|)

如果使用 sort / stable_sort,可以做到 O(|s|log2|s|)。如果使用基数排序,可以做到 O(|s|log|s|)

AC 记录

代码

但是你会发现上面这份常数太大了。我们可以转变思路,直接求 sa 数组。具体见模板题第一篇题解代码注释,讲得很清楚。本质上是将基数排序后的下标序列作为本轮的 sa 数组,再根据定义同时求出本轮的 rk 数组。

height 数组

定义:heighti=|LCP(sufsai1,sufsai)|,即排名相邻的两个后缀的最长公共前缀。

特别规定当 i=1heighti=0

代码中有时候 height 数组会以后缀的位置为下标而非后缀的排名,读者需要自行甄别。

本文代码中,常用 ht 作为 height 的简写。

可以说 height 数组是 SA 中最为精华的部分了,正是它使得 SA 能够灵活解决很多字符串问题。

使用哈希,我们容易 O(|s|log|s|) 地求出这个东西。考虑使用一些确定性的算法。

Lemma 1

sufsaisufsaj(其中 i<j)两个后缀存在长度为 L 的公共前缀,则 k[i,j)sufsaksufsaj 存在长度为 L 的公共前缀。

Proof 1

k=i 时显然。当 k(i,j) 时,若它不满足上面这个条件,考虑第一个不同的位置,则会出现 sufsai<sufsak,sufsak>sufsajsufsai>sufsak,sufsak<sufsaj 的情况,这显然与后缀排序的定义矛盾了。

根据这个引理我们可以推出一条关键性质:

heightrkiheightrki11

其中 i2

heightrki1=0 的情况显然。考虑 heightrki11 的情况。

我们考虑 sufi,sufsarki1,sufsarki11,sufi1,sufsarki11+1 这五个后缀。我们将它们简记为 s1,s2,s3,s4,s5

由于 1 的限制,s3s4 的第一位必须相同。

那么 |LCP(s1,s2)|=heightrki,|LCP(s3,s4)|=heightrki1

注意到 s1s4 删去第一个字符,s5s3 删去第一个字符。那么 |LCP(s1,s5)|=heightrki11,因为只需要减掉删掉的那一位,剩下这么多长度的前缀是公共的,且由于原来 |LCP(s3,s4)| 的限制,|LCP(s1,s5)| 不能更大。

此时我们可以说 sufi,sufsarki11+1 两个后缀间存在长度为 heightrki11 的公共前缀。

根据 sark 数组的定义,我们要 s3<s4。据此我们还可以得到 s5<s1。因为 s1s4 删去第一个字符,s5s3 删去第一个字符,且 s3,s4 第一个字符相同。因此需要 s3 的剩余部分(即 s5)更小才能满足 s3<s4

转化成 sark 数组上的关系,就是 rksarki11+1<rki。因此 rki1[rksarki11+1,rki)。利用 sarki=i 这一定义以及上面的引理,可以得到 sufi,sufsarki1 之间存在长度为 heightrki11 的公共前缀。

因为可能存在比它更长的,所以有 heightrkiheightrki1+1

我们考虑最暴力的求解 heightrki 方法,使用一个指针 p 表示匹配了多少位,枚举 sufisufsarki1 这两个后缀的相应位置。一开始为 0,然后一直递增直到不能匹配为止。时间复杂度为 O(|s|2)

但是运用上面这个性质,我们每次可以从 heightrki11 开始枚举 p。而在本轮循环开头 p 仍是 heightrki1,那么我们只要令 p 减一即可(当然要和 0max)。这么一来,对于 |s| 个位置 p 每次至多减一,那么总共至多减少 |s|,然后匹配到不能匹配为止时 p|s|,因此 p 增加的次数也是 O(|s|) 的。所以我们就可以利用一个指针结合上面的结论求出 height 数组。

放一个求解 height 数组的代码。

for (int i = 1, k = 0; i <= n; ++i) {
    if (rk[i] == 1) { k = 0; continue; } if (k) --k;
    while (a[i + k] == a[sa[rk[i] - 1] + k]) ++k; h[rk[i]] = k;
}

代码中 a 为字符串,hheight 数组。

会求解 height 数组之后,我们来考虑这样一件事情

LCP theory

|LCP(sufsai,sufsaj)|=mink=i+1jheighti,其中 i<j

Proof

首先容易证明它们存在这么多长度的 LCP,严谨证明的话考虑归纳法和 min 的性质以及等号的传递性。

然后我们可以结合 Lemma 1 通过反证法证明它们不存在更长的 LCP

结合 LCP theory,根据 min 的单调性,我们可以得到:

与某个后缀 sufiLCP 长度大于等于某个定值 k 的后缀的 rk 构成一个连续的区间。

换句话说,若排名最小的与 sufiLCP 长度 k 的排名为 L,最大的排名为 R,则排名在 [L,R] 内的后缀都满足其与 sufiLCP 长度大于等于该定值 k

在求解这样的区间时,我们可以建立 height 数组的 ST 表,然后二分出两个端点。

到此为止 SA 的所有组成部分就讲解完毕了,附上一份完整的 SA 板子。

//M 为最大数据范围。
template<class T> struct STmin {
    T b[22][M];
    void build(T *a, int n) { // 对长度为 n 的数组 a 建立 min ST 表。
        for (int i = 1; i <= n; ++i) b[0][i] = a[i];
        for (int i = 1; (1 << i) <= n; ++i)
            for (int j = 1; j + (1 << i) - 1 <= n; ++j)
                b[i][j] = min(b[i - 1][j], b[i - 1][j + (1 << i - 1)]);
    }
    T qry(int l, int r) {
        int k = __lg(r - l + 1); return min(b[k][l], b[k][r - (1 << k) + 1]);
    }
};
struct SA {
    int n, sa[M], rk[M], y[M], cnt[M], h[M]; STmin<int> rmq;
    void build(int *a, int m) { // 对长度为 m 的字符串 a 建立 SA,默认字符集与串长同阶。
        n = m;
        for (int i = 1; i <= n; ++i) ++cnt[a[i]];
        for (int i = 1; i < M; ++i) cnt[i] += cnt[i - 1];
        for (int i = n; i >= 1; --i) sa[cnt[a[i]]--] = i;
        for (int i = 2, t = rk[sa[1]] = 1; i <= n; ++i)
            rk[sa[i]] = (a[sa[i]] == a[sa[i - 1]] ? t : ++t);
        for (int w = 1, t; w <= n; w <<= 1) {
            t = 0; for (int i = n - w + 1; i <= n; ++i) y[++t] = i;
            for (int i = 1; i <= n; ++i) if (sa[i] > w) y[++t] = sa[i] - w;
            memset(cnt, 0, sizeof cnt); for (int i = 1; i <= n; ++i) ++cnt[rk[i]];
            for (int i = 1; i <= n; ++i) cnt[i] += cnt[i - 1];
            for (int i = n; i >= 1; --i) sa[cnt[rk[y[i]]]--] = y[i];
            swap(rk, y); t = rk[sa[1]] = 1;
            for (int i = 2; i <= n; ++i)
                rk[sa[i]] = (y[sa[i]] == y[sa[i - 1]] &&
                             y[sa[i] + w] == y[sa[i - 1] + w] ? t : ++t);
            if (t == n) break;
        }
        for (int i = 1, k = 0; i <= n; ++i) {
            if (rk[i] == 1) { k = 0; continue; } if (k) --k;
            while (a[i + k] == a[sa[rk[i] - 1] + k]) ++k; h[rk[i]] = k;
        }
        rmq.build(h, n);
    }
    int lcp(int x, int y) {
        if (x == y) return n - sa[x] + 1;
        if (x > y) swap(x, y); ++x; return rmq.qry(x, y);
    }
    pair<int, int> range(int x, int y) {
        int l = 1, r = x, m, f, g;
        while (l <= r) {
            m = l + r >> 1;
            if (lcp(m, x) >= y) f = m, r = m - 1; else l = m + 1;
        }
        l = x; r = n;
        while (l <= r) {
            m = l + r >> 1;
            if (lcp(m, x) >= y) g = m, l = m + 1; else r = m - 1;
        }
        return make_pair(f, g);
    }
} S;

简单应用

本质不同子串个数

P2408 不同子串个数

  • 给出长度为 n 的字符串 s,求 s 有多少个本质不同的子串。

  • n105

首先,任意一个子串一定是一个后缀的前缀。比如说 s[l,r] 这个子串,它就是 sl 开头的后缀长度为 rl+1 的前缀。

那么,对于任意一种本质不同的子串,考虑所有存在它为前缀出现的后缀,在这样的排名最小的后缀中统计它。

那么就是要对于排名为 i 的后缀,计算它有多少个前缀没有在排名更小的后缀中出现过。这样的前缀个数是 nsai+1heighti 个。考虑这个后缀一共有 nsai+1 个前缀,其中长度为 1heighti 的前缀在排名为 i1 的后缀中出现过,对于剩下的后缀,根据上一节中的 Lemma 1,可以证明它们是第一次出现。

那么累加每一个后缀的贡献即可。

时间复杂度为 O(nlogn)(不过代码里使用的是 O(nlog2n) 的 SA),空间复杂度为 O(n)

AC Link

Code

处理多串问题

ABC362G Count Substring Query

  • 给出字符串 sn 个字符串 (t1,tn),求 tis 中出现了多少次。

  • |s|,n,i=1n|ti|5×105

对于 ti 的答案,考虑其在 s 的后缀中作为前缀出现,就是求 s 有多少个后缀满足它和 tiLCP 长度 |ti|

但是我们无法直接求解来自于两个串的后缀 LCP 长度问题,于是考虑将它们中间用分隔符顺次拼接成一个大串 S。其中 |S|=|s|+n+i=1n|ti|

对于某个串以 i 为开头的后缀,我们称其在大串中的对应后缀为大串中以 i 对应位置开头的后缀。

我们需要保证两点:

  • 任意两个后缀在大串中的对应后缀的大小关系和原串一致。

  • 任意两个后缀在大串中的对应后缀的 LCP 长度和原串相同。

这里直接给出拼接的方法:

我们用数去表示字母,这样适用于更大的字符集。对于第 i 和第 i+1 个串之间的拼接,我们使用 i 这个数。然后每个字母在新字符集中对应的字符就是它在字典序中的编号加上最大的分隔字符。

首先来看第一点。若两个后缀 i,j 之间不存在一个是另一个的前缀,那么考虑它们的失配位置,在大串中它们的对应后缀显然也存在这个失配位置因此大小关系不变。否则,作为前缀的那个串原本失配位置上的字符由空字符变为分隔符。显然分隔符的字典序还是小于字母的。因此第一点成立。

然后看第二点,若两个后缀不存在前缀包含的关系,同样考虑失配位置在大串中仍然存在,且在其之前的字符都能作为 LCP;否则,在分隔符那一位强制失配。因此第二点也是成立的。

那么对于原问题,我们可以求,有多少个 s 的对应后缀与 tiLCP 长度 |ti|。根据前文的内容,满足后者的后缀排名形如一个区间。那么此时需要满足,s 的对应后缀的排名在这个区间内。求出这个区间后,我们可以计算区间内由多少个后缀来自于 s。这个是容易前缀和维护的。

时间复杂度为 O(|S|log|S|),空间复杂度为 O(|S|)

AC Link & Code

习题

SP10419 POLISH - Polish Language

  • 给出字符串 s,求有多少个序列 a 满足:

    • i[1,|a|],1ai|s|

    • i(1,|a|],sufai>sufai1

    • i(1,|a|],|sufai|>|sufai1|

    数量对 109+7 取模。其中字符串的比较均基于字典序大小。

  • 多组数据,|s|105

看到后缀之间的字典序比较,先想到后缀数组。处理完之后,考虑一个一个解决限制条件。

  • i[1,|a|],1ai|s|,不用转化。

  • i(1,|a|],sufai>sufai1,等价于 i(1,|a|],rkai>rkai1

  • i(1,|a|],|sufai|>|sufai1|,等价于 i(1,|a|],ai>ai1

典型二维偏序,考虑 dp。设 fi 表示 a|a|=i 的符合条件的序列数量,显然有 fi=i<jnrki>rkjfj+1。简单来说就是考虑第 i 位接上怎样的序列,+1 表示单独成为一个序列。

倒序枚举维护 i<j,用树状数组维护 rki>rkj 即可。

设数据组数为 T,时间复杂度为 O(T|s|log|s|),空间复杂度为 O(|s|)

提交记录 代码

P5353 树上后缀排序

  • 给出一棵 n 个节点,以 1 为根的树,点 i 上有字符 ci。定义点 i 的字符串 si 为从点 i 走到点 1 路径上所有点上的字符拼接而成的字符串。

  • 形式化的,若点 i 到点 1 的路径为 p1,p2,,pk(p1=i,pk=1),则 si=cp1cpk

  • 你要对 1n 这些点按照 si 的字典序进行排序,若字典序相同,则父亲排名小的点排名小。若仍相同,编号小的点排名小。

  • n5×105

看到对字符串排序想到后缀排序。我们可以类比后缀排序,利用倍增的思想,每次将两条长度为 2i 的连续的、向上的链拼起来成为长度为 2i+1 的链。所以要处理树上的倍增数组,记 fai,u 为点 u 向上走 2i 条边到达的祖先。这部分的代码:

for (int l = 0, id; (1 << l) <= n; ++l) {
    for (int i = 1; i <= n; ++i) 
        p[i] = {{rk[i], fa[l][i] ? rk[fa[l][i]] : 0}, i}; // 先比前半段,前半段相同再比后半段。
    memset(cnt, 0, sizeof cnt); 
    for (int i = 1; i <= n; ++i) ++cnt[p[i].fi.se];
    for (int i = 1; i <= n; ++i) cnt[i] += cnt[i - 1];
    for (int i = n; i >= 1; --i) tmp[cnt[p[i].fi.se]--] = p[i];
    for (int i = 1; i <= n; ++i) p[i] = tmp[i];
    memset(cnt, 0, sizeof cnt); 
    for (int i = 1; i <= n; ++i) ++cnt[p[i].fi.fi];
    for (int i = 1; i <= n; ++i) cnt[i] += cnt[i - 1];
    for (int i = n; i >= 1; --i) tmp[cnt[p[i].fi.fi]--] = p[i];
    for (int i = 1; i <= n; ++i) p[i] = tmp[i]; id = 0;
    for (int i = 1; i <= n; ++i)
        { if (i == 1 || p[i].fi != p[i - 1].fi) ++id; rk[p[i].se] = id; }
    if (id == n) { op = 1; break; }
}

关键是如何去重。这个代码求出来的 rki 表示将所有字符串去重后si 的排名(排名定义为比它小的数的个数 +1)。我们先通过以下代码求得不去重的排名:

memset(cnt, 0, sizeof cnt); for (int i = 1; i <= n; ++i) ++cnt[rk[i]];
for (int i = 1; i <= n; ++i) cnt[i] += cnt[i - 1];
for (int i = 1; i <= n; ++i) rk[i] = cnt[rk[i] - 1] + 1;

然后考虑两个字典序相同的字符串,它们的深度一定相同。因此用 vector 存放深度为 d 的点编号,然后从小往大处理,因为深度父亲的深度大于儿子。先将同一深度内的点按照 rk 排序。然后从头开始扫,扫出一段 rk 相同的区间,然后再对这个区间内的点以父亲排名作为第一关键字、编号作为第二关键字排序。那么这些点的排名就是递增的,且第一个点的排名就是自己的 rk。代码如下:

bool cmp1(int u, int v) { return rk[u] < rk[v]; }
bool cmp2(int u, int v) {
    return rk[fa[0][u]] != rk[fa[0][v]] ? rk[fa[0][u]] < rk[fa[0][v]] : u < v;
}
for (int i = 0; i < n; ++i) {
    sort(h[i].begin(), h[i].end(), cmp1);
    for (int j = 0, k, l = h[i].size(), id; j < l; j = k) {
        for (k = j; k < l && rk[h[i][k]] == rk[h[i][j]]; ++k);
        sort(h[i].begin() + j, h[i].begin() + k, cmp2); id = rk[h[i][j]] - 1;
        for (int d = j; d < k; ++d) rk[h[i][d]] = ++id;
    }
}

去完重之后,如果用后缀排序中的名称来说,别忘记你输出的是 sa 数组而不是 rk 数组,你要再做一遍 sarki=i

现在来算时间复杂度,倍增处理 fa 数组以及倍增求 rk 都是 O(nlogn) 的,至于去重,其实就是把“将长度为 n 的数组分成若干段,对每段分别进行排序并从左到右扫描”这个操作做了两次,根据乘法分配律和加法结合律,可以算出这部分的时间复杂度为 O(nlogn)

综上,本做法时间、空间复杂度均为 O(nlogn)

评测记录 代码

AT_s8pc_2_e 部分文字列

  • 给出一个字符串 S,求 S 的所有本质不同子串的长度之和。

  • |S|105

看到本质不同子串,先做后缀排序把 height 数组求出来,然后排名为 i 的后缀能带来的本质不同子串个数为 |S|sai+1heighti,这些子串的长度为 heighti+1|S|sai+1,等差数列求和计算即可。

时间复杂度为 O(|S|log|S|),空间复杂度为 O(|S|)。由于是 AT 远古题,所以行末要输出换行。

提交记录(含代码)

CF149E Martian Strings

  • 给出字符串 s,以及 m 个询问串 pi,每次询问是否能找到两个不交的区间 [a,b],[c,d] 使得 sasa+1sbscsc+1sd=pi

  • m102|pi|103|s|105

考虑将所有串拼成一个大串 S,做一遍后缀排序,求出 sa,rk,height 数组。

对于每一组询问,考虑枚举 j 表示找到 [a,b],[c,d] 使得 sasa+1sb=pi1pi2pij,sbsc+1sd=pij+1pij+2pi|pi|。可以通过二分 + ST 表求出满足条件的后缀的排名区间。

我们考虑选择最左边的 [a,b] 以及最右边的 [c,d],这样一定不劣。因此考虑以排名为下标建立 ST 表维护某个排名区间内,n 最左、左右后缀的位置。然后判断这两个位置截取出来的子串是否相交即可。

时间、空间复杂度均为 O(|S|log|S|)

提交记录(含代码)

CF316G3 Good Substrings

CF316G1 Good SubstringsCF316G2 Good Substrings

  • 给出字符串 s,以及 n 个限制,每个限制形如 ti,li,ri,一个字符串满足该条限制,当且仅当它在字符串 ti 中的出现次数在 [li,ri] 之间。

  • s 有多少个本质不同的子串满足所有限制。

  • |s|,maxi=1n|ti|5×104n10

s[l,r] 为字符串 ssl 开头,以 sr 结尾的子串,形式化的,s[l,r]=slsr

看到「本质不同子串」,想到后缀数组。先将所有字符串用奇怪字符拼起来(记大串为 S)做后缀排序求出 sa,rk,height 数组并对 height 数组维护 ST 表。

对于一个字符串 s,我们知道排名为 i 的后缀带来的本质不同子串是 s[sai,heighti+1]s[sai,|s|] 这些,然后你会发现这些子串的出现次数随着长度递增不升

因为若有一个长串出现若干次,我的短串也被这个长串包含,至少出现了这么多次。

考虑二分出最短的满足所有限制上界的字符串长度 ansl 以及最长的满足所有限制下界的字符串长度 ansr,那么这个后缀就可以带来 ansransl+1 个满足所有限制的本质不同子串。

维护一个前缀和数组 sumj,i,表示排名为 1i 的后缀中,有多少个后缀是 tj 带来的。我们又知道对于一个子串,出现它的后缀的排名是一段连续的区间。套路用二分和 ST 表求 LCP 得到这个区间 [L,R]。问题变成了判断区间内某个数 i 出现次数是否(不)超过给定的值。用 sumi,Rsumi,L1 表示出其出现次数,由于 n 很小,枚举判断即可。这样这题基本就做完了。

还有一个地方需要注意,我们要求的是原串 s 某个后缀带来的本质不同子串个数,但是现在把所有字符接在一起,height 数组并是大串 S 中两个排名相邻的后缀的 LCP。所以我们要按排名枚举 S 的后缀,如果它是 s 的后缀就统计答案。然后你对 S 后缀排序,原串 s 中的所有后缀也是有序的,不然它们在 S 中也是无序的,相当于你就没排序。 因此直接记录上一个为原串中的某个后缀的排名 la,那么我要的 height,原串 s 中排名相邻两个后缀的 LCP,就是 SS[la,|S|] 和当前后缀的 LCP。后面接的东西不影响,因为你接了奇怪字符,在那一位一定失配,不会影响 LCP 长度。

时间复杂度为 O(|S|log|s|(n+log|S|)),空间复杂度为 O(|S|log|S|+n|S|)

提交记录(含代码)

UVA1502 GRE Words

双倍经验

  • 给出 n 个字符串 s1sn,第 i 个字符串有权值 wi。选出一个子序列 a1ak,满足 i[1,k),ai<ai+1saisai+1 的子串。求 i=1kwai 的最大值。可以为空,此时权值为 0

  • T 组数据,T50。对于单组数据,满足 n2×104i=1n|si|3×105

N=i=1n|si|

考虑 dp。设 fi 表示以 i 开头的最长子序列,则 fi=maxj(i,n]si is a substring of sjfj+wi

将所有串拼成一个大串 S 进行后缀排序。则 j 满足条件,当且仅当 sj 中有一个后缀与 si 的最长公共前缀长度不少于 |sj|

这样的后缀排名形如一个区间 [L,R]。考虑线段树维护。区间 [l,r] 的信息为:当前包含排名为 [l,r] 中的后缀的字符串中,fj 的最大值。

那么转移就是区间最大值。可能会重复贡献一些 fj,但由于是取 max 所以没关系。

转移完后在线段树上对 si 的所有后缀排名对应的位置进行单点修改。

时空复杂度均为 O(NlogN)。可以把 height 数组用线段树维护然后线段树上二分得到排名区间,这样空间是线性。

AC Link / AC Code

SP8093 JZPGYZ - Sevenk Love Oimaster

  • 给出 n 个模板串 s1snm 个查询串 t1tm,每次询问一个查询串是多少个模板串的子串。
  • n104m6×104i=1n|si|105i=1m|ti|3.6×105

先将所有字符串用奇怪字符拼成一个大串 S 然后做一遍后缀排序,求出 sa,rk,height 等数组。

对于每一个询问串,以它为前缀的后缀的排名一定是一个区间,考虑二分出这个区间 [L,R]

我们记排名为 i 的后缀的颜色 coli 为它是哪个模板串的后缀。则要求区间 [L,R] 内有多少种不同的颜色。

主席树维护每一个位置的前驱,数有多少个前驱在区间外即可。但是询问串之间也可能存在包含关系,所以要数的颜色必须是 [1,n] 内的颜色。

因此主席树的一个节点的定义为:当前版本中,有多少个位置满足这个位置的颜色在 [1,n] 内,且该位置的前驱在该区间内。插入的时候,若当前位置的颜色在 [1,n] 内则插入,否则继承上一个版本。

时间、空间复杂度均为 O(|S|log|S|)

提交记录 代码

CF232D Fence

  • 给出序列 a1an,有 q 次询问,每次询问给出 [l,r],求有多少个区间 [x,y] 满足 yx=rl[x,y][l,r]=i[0,rl],al+i+ax+i=al+ax

  • n,q105

tags:

  • binary search

  • data structures

  • string suffix structures

  • 2900

原题就是让我们求出有多少个满足条件的左端点。

我们记原数组的差分数组 di=aiai1(i(1,n])认为 d1 没有意义,即不存在,其值不与任何一个 di 相同。则满足第二个条件的充要条件是 i(0,rl],dl+i=dx+i

  • 证明:

根据已知条件可以推出:

  • al+i+ax+i=al+axal+ial=axax+i

  • al+i1+ax+i1=al+axal+i1al=axax+i1

两式相减即可得到 al+ial+i1=ax+i1ax+i,即 dl+i=dx+i

我们若倍长 d,且令 di=din(i(n,2n]),则上述条件等价于 dl+i=dx+n+i。我们要统计有多少个 x,就可以去统计有多少个 x+n同理可以去统计有多少个 x+n+1

为什么要做这一步转化呢?我们发现,对于 d[l+1,2n]d[x+n+1,2n] 这两个后缀,它们存在 d[l+1,r]d[x+n+1,x+n+rl] 这一段长度为 rl 的公共前缀。考虑对差分数组进行后缀排序,则可以二分 + ST 表求出与后缀 d[l+1,2n]LCP 长度不小于 rl 的排名区间。然后根据不交、长度相等的限制以及差分数组的定义,可以得到 x+n+1 的范围是 [n+2,n+2lr][n+r+2,2n+lr+1]

这就是个二维数点,在线主席树或离线扫描线 + 树状数组维护一下就行了。

  • 注意

使用上述统计方法的前提是存在差分数组。当 l=r 时,区间内不存在差分数组,不能这样统计。

不过容易得知此时答案即为 n1,特判一下即可。

代码里用的是主席树,时间、空间复杂度均为 O(nlogn)

提交记录(Accepted 483ms/73952KB,含代码)

P4143 采集矿石

  • 给出字符串 s,以及数组 a1a|s|

  • 定义一个子串的排名为:字典序比它大的本质不同的子串个数 +1

  • 定义一个子串 s[l,r] 的权值为 i=lrai

  • 求有多少个子串的排名等于权值。

  • |s|105,0ai1000

首先对 s 进行后缀排序,然后考虑每一个左端点 l,不难发现随着右端点 r 的增大,子串的排名单调递减,权值单调不降。

所以可以二分出满足条件的最小 / 大右端点。

考虑如何求出一个子串 t 的排名。可以用本质不同子串数减去比它小的。

前半部分运用经典结论即为 i=1n(|s|sai+1heighti),我们考虑如何求比它小的本质不同子串数。

可以二分出以这个子串为前缀的后缀排名区间 [L,R]答案即为排名为 [1,L) 的后缀带来的本质不同子串个数。

  • 充分性:

    若一个子串 str 在排名为 [1,L) 的后缀中作为前缀出现,那么这个后缀 s[i,|s|]s[l,|s|]LCP 长度一定小于 |t|。即两个后缀可以在第 |t| 个位置之前可以找到不相同的位置。而由于 s[i,|s|] 这个后缀排名更小,在这个位置一定 s[i,|s|] 这个后缀小于 s[l,|s|]

    考虑 str 是否跨过这个位置,若不是,则在前 |str| 位两串相同,第 |str|+1str 为空,字典序极小。

    若跨过,则 str 在这个位置小于 t

  • 必要性:

    考虑这两个子串第一次不同是在某个位置,这个位置一定在两个后缀中。

正确性证好了。这个东西也是考虑每个后缀带来的本质不同子串。即可以这么求:

i=1L1(|s|sai+1heighti)

于是做完了。时间复杂度为 O(|s|log2|s|),空间复杂度为 O(|s|)

提交记录 代码

ABC280Ex Substring Sort

  • 给出 n 个字符串 s1sn。记 F(i,l,r) 表示 si[l,r] 这个子串。将所有存在的 F(i,l,r) 非降排序。q 次询问,求出排在第 k 位的 F(i,l,r)。如有多解输出任意一个。

  • n,i=1n|si|105q2×105ki=1n|si|(|si|+1)2

  • 2 s / 1 GB

先将所有字符串拼成大串 S。其中 O(|S|)=O(n+i=1n|si|)。对 S 进行后缀排序。

称一个串在排名为 i 的后缀中第一次作为前缀出现,当且仅当:不存在 j<i,使得这个串在排名为 j 的后缀中作为前缀出现。

对于 S 中排名为 i 的后缀,预处理 sumi 表示有多少种本质不同的原串中的子串在这个后缀中第一次作为前缀出现;预处理 toti 表示有多少个原串中的子串在这个后缀中作为前缀出现。然后分别对这两个数组求前缀和,记为 ssumistoti(代码中用的是同一个数组)。

考虑求出答案是第几大的本质不同子串。容易发现排在第 k 位等价于,它是最大的一种子串,使得小于它的子串数量小于 k。这个有单调性,可以二分利用 ssumi 判断求。

记二分的子串为 str。考虑对于每个后缀统计小于 str 的子串数量。先找到它第一次出现在哪个排名的后缀中,设这个排名为 p。分为 [1,p)[p,|S|] 两部分。

对于第一部分,这些后缀的所有前缀都小于 str。因为若某个前缀 prestr 前缀,其必然是真前缀。不然 p 就不是 str 第一次出现的位置。那么 pre 字典序小于 str。否则,它们跨过了 LCP,又因为 [1,p) 的后缀排名更小,所以 preLCP 后的那个位置上一定小于 str,因此 pre 字典序小于 str

对于第二部分,答案为 i=p|S|min{|LCP(S[i,|S|],S[p,|S|])|,|str|1}。同样考虑为 prestr 真前缀以及跨过 LCP 的情况。

第一部分答案即为 stotp1。第二部分考虑将 min 拆开。由于 |LCP(S[i,|S|],S[p,|S|])|=minj=p+1iheightj,这个式子的值是单调不升的。可以二分出一个分界点 pos 使得当 i[p,pos]min{|LCP(S[i,|S|],S[p,|S|])|,|str|1}=|str|1;当 i(pos,|S|)min{|LCP(S[i,|S|],S[p,|S|])|,|str|1}=minj=p+1iheightj。显然有 posp,因为 p 这个后缀以 str 为前缀。

那么第一种情况的贡献就是 (posp+1)(|str|1);至于后一种情况,根据 min 的结合律,可以将其改写成 i=pos+1|S|minj=pos+1iheightj。记为 fx=i=x|S|minj=xiheightj。由于没有修改,考虑预处理。

可以用单调栈,对于每个 x 求出最小的 y,满足 y>xheighty<heightx。记这个 ynxtx。同样将 fx 分为 i[x,nxtx)i[nxtx,|S|] 两部分。对于第一部分,根据 nxtx 的定义可知这些 [x,i] 的最小值均为 heightx,贡献为 (nxtxx)heightx;对于第二部分,再运用 min 的结合律写成 i=nxtx|S|minj=nxtxiheighti,你会发现它就是 fnxtx。于是我们得到了 fx 的求法:

fx=(nxtxx)heightx+fnxtx

fx 求出来之后,就可以将 i=pos+1|S|minj=pos+1iheightjfpos+1 带入求解了。

这样一来我们就找到有多少个子串小于给定的串 str。结合第一次二分就可以得到答案是哪一种本质不同的子串。为了方便,这一步在返回小于 str 的串个数时,同时返回 str 第一次出现后缀属于原串中的第几个串的哪个位置,便于得到 F(i,l,r)。这些都是容易求的,可以考虑记录 S 中每个排名后缀属于哪个原串、原串在 S 中出现的位置。

那么这题就做完了,时间复杂度为 O((q+|S|)log2|S|),空间复杂度为 O(|S|log|S|)

AC Code

CF1037H Security

  • 给出一个字符串 s,有 q 次询问,第 i 次询问给出 li,ri,ti,求一个字典序最小的字符串 str,使得它是 s[li,ri] 的子串,且 str>ti

  • |s|105i=1q|ti|,q2×105

|LCP(str,ti)|=lstr>ti 当且仅当 strl+1>til+1,为了使 str 尽量小,我们希望 l 尽量大

证明很简单,假设有一个串 T 满足 LCP(T,ti)=L<l,则 LCP(str,T)=L,且 TL+1>strL+1,因此 T>str,所以 |LCP| 大的字符串字典序更小

先将 s 和所有 ti 中间用奇怪字符拼接成大串 S,这样不改变任意两个后缀的 LCP。然后做一遍后缀排序,求出 sa,rk,height 数组以及维护 ST 表辅助求后缀之间的 |LCP|

对于每一组询问,考虑枚举 l(0lrili) 以及下一位拼上什么字符 c,满足 c>til+1(一定是仅拼上一个字符,因为空字符字典序最小)。可以先二分 + ST 表求出与 tiS 中的后缀的 |LCP| 至少为 l 的后缀排名区间 [L,R]。那么在 LCP 末尾拼上一个字符 c 后(记这个字符串为 p),以 p 为前缀的后缀的排名仍然是一个连续的区间。由于后缀排序过,因此 [L,R] 排名区间内的后缀的第 l+1 位的字符一定单调不减

考虑继续二分出这个连续区间 [ql,qr],可以二分找到最小的排名 mn 使得 Ssamn+lc 以及最大的排名 mx 使得 Ssamx+lc。则 ql=mn,qr=mx。若不存在 [ql,qr] 这个区间,则跳过。

我们要求 s[li,ri] 中是否存在一个子串 strp 为前缀,相当于求 suflisufril 这些后缀中,是否存在一个后缀 sufj 使得 qljqr。至此原问题转化成了二维数点,用主席树维护即可。

可以从小到大枚举 c,对于枚举的 l,我们找到一个最小的字符 c 满足条件之后,即可停止当前 l 的枚举。因为要求字典序最小。

对于多种 l 的答案,上面已经说过,选最大的那一种。

默认 |S|,q 同阶,时间复杂度为 O(|Σ|(i=1q|ti|)log|S|+|S|log2|S|),空间复杂度为 O(|S|log|S|)。由于不是瓶颈,后缀排序部分未使用基数排序优化。

提交记录(含代码)

P4770 [NOI2018] 你的名字

  • 给出字符串 s 以及 q 个询问,第 i 个询问给出一个串 ti 以及一个区间 [li,ri]

  • s[l,r] 为字符串 sl 位到第 r 位字符顺次拼接而成的子串。形式化地,s[l,r]=slsl+1sr

  • 对于每个询问,求 ti 有多少种本质不同的子串没有在 s[li,ri] 中出现。

  • |s|5×105,q105,i=1q|ti|106

  • 5.00 s / 768.00 MB

神仙字符串题。

首先把所有字符串用特殊字符接起来,得到一个大串 S。对 S 进行后缀排序。这样不改变任意两个后缀的 LCP

对于每一组询问,考虑容斥,即用 ti 中的本质不同子串个数减去在 s[li,ri] 中出现过的。

前半部分是平凡的,即按排名考虑每一个后缀带来的本质不同子串个数,根据经典结论就是这个后缀的前缀数减去它的 height

至于后半部分,同样这样考虑每个后缀带来的本质不同子串中有多少个在 s[li,ri] 中出现。我们发现若一个后缀 ti[j,e]s[li,ri] 中出现,则 ti[j,e1] 也在 s[li,ri] 中出现。所以可以考虑二分这个最大的结束位置 e。判断 ti[j,e] 是否在 s[li,ri] 中出现就是判断是否存在一个位置 k 使得 k[li,rie+j]|LCP(S[k,|S|],ti[j,e])|ej+1

二分出排名区间,主席树二维数点检查即可。得到这个值后,ti[j,j+heightj]ti[j,e] 这些本质不同子串在 s[li,ri] 中出现,直接减去个数即可。

但是这样回答单组询问的时间复杂度为 O(|ti|log|S|log|ti|),无法接受。

思考一下二分的目的,我们想要对于 ti 的每个后缀,得到一个最大的长度 Lj,使得 ti[j,j+Lj1]s[li,ri] 中出现。

我们发现一个关键性质,那就是 LjLj11。因为这两个后缀只差了开头的这一位。

我们可以类似于 height 数组那样,用一个指针 k 表示当前 ti[j,j+k1]s[li,ri] 中出现,检查是否可行时仍然二分排名区间 + 主席树。若可行则 k 右移。

由于最多递减 O(|ti|),因此 k 最多移动 O(|ti|) 次,这样单组询问的时间复杂度就是 O(|ti|log|S|)

综上,我们得到了一个时间、空间复杂度均为 O(|S|log|S|) 的做法。

提交记录(Accepted 1004.62s/606.29MB 代码

P4022 [CTSC2012] 熟悉的文章

  • 给出 n 个文本串 s1snm 个询问串 t1tm

  • 称一个字符串 str 是“L 熟悉的”,当且仅当 |str|L,且 str 是文本串的子串,此时记 P(str,L)=1。否则 P(str,L)=0

  • 对于每个询问串 ti,求出最大的整数 Li,使得将其划分为若干个子串后,所有“Li 熟悉的”子串长度之和不小于 9|ti|10

  • 形式化地,记一种划分 ti 的方式为 S={[l1,r1],,[l|S|,r|S|]},满足 j[1,|S|)Z,rj+1=lj+11,且 l1=1,r|S|=|ti|。记所有划分方案构成的集合为 U。你要找到最大的整数 Li,满足 TU,j=1|T|[P(ti[lj,rj],Li)(rjlj+1)]9|ti|10

  • N=i=1n|si|,M=i=1m|ti|,满足 N,M1.1×106

  • 1 s / 250 MB

默认 O(n)=O(m)=O(N)=O(M)。字符集大小 O(|Σ|)=O(1)

对于每一个询问,容易发现答案有单调性,因为若 x 是合法的,则在 x1 时仍然按照这种方式划分,式子的值是不减的。所以二分答案。

对于一个已知的 Li,我们可以 dp 求出上面式子的最大值然后判断是否合法。

fj 表示将 ti[1,j] 分成若干段,上面式子的最大值。记 mxj 表示以 ti[1,j] 为前缀的最长后缀 suf 的长度,使得 suf 在文本串中出现过。那么考虑枚举上一段的末尾(为 0 表示这一段是开头),有:

fj=max{maxk[0,jmxj)(jLi,j)Zfk,maxk[jmxj,jLi]Zfk+jk}

就是去考虑这一段能否成为“Li 熟悉的”。容易发现 fjfj1,因为我将 j 这个位置单独分一段,答案是不减的。所以前一部分的转移可以用 fj1 代替。

至于后面那部分,先将 mxj 求出来。

将所有串用分隔符拼在一起形成大串 A 进行后缀排序。我们记 MXk 表示 A[1,k] 这个前缀最长的后缀,使得它在文本串中出现。可以发现 mxj 就等于 ti,jA 中对应位置的 MX 值,因为 A[1,k] 存在。因为 A[1,k] 中存在 mxj 长度的后缀满足条件,且不存在更长的后缀满足条件。

进一步发现,若 A[1,k] 存在长度为 a 的后缀满足条件,则长度为 a1 的后缀也满足条件。因为后者是前者的子串。所以初步想法是二分 MXk,然后判断是否合法。

考虑如何判断一个长度 len 是否合法。若合法当且仅当 A 中存在一个来自于文本串的后缀,使得它与 A[klen+1,|A|] 这个后缀的最长公共前缀长度不少于 len

满足后面那个条件的后缀排名形如一段区间 [ql,qr],可以二分 + height 数组 rmq 得到。记 Bi=1/0 表示排名为 i 的后缀是否来自文本串。那么判断 B 数组的区间和是否为正即可,前缀和维护。

但是这样对于每个后缀做一遍时间复杂度为 O(nlog2n),无法接受。

注意到一个关键性质,MXkMXk+11。因为 A[1,k+1] 的那个后缀长度为 MXk+11 的前缀是 A[1,k] 的后缀。

那么这样用个指针扫一下即可,指针最多递增 O(n) 次,所以这样时间复杂度是 O(nlogn)

这样我们就求出了 mxj。根据上面那个性质,可以发现第二类转移区间左端点 jmxj 是不降的。同时右端点 jL 也是不降的。那么对于这样的区间求 fkk 最大值,单调队列维护即可。

时空复杂度均为 O(nlogn)。可以把二分 + ST 表换成线段树上二分做到线性空间,但是常数太大无法接受。一开始一直在为空间纠结,但事实上并不卡空间。

各种卡常、指令集配合 C++98 艹过去了。

AC Link

AC Code

CF1483F Exam

  • 给出 n 个字符串 s1sn,求有多少对 (i,j),满足:
    • 1i,jn
    • sjsi子串。
    • 不存在 ki,j,k 两两不同)使得 sjsk 的真子串,且 sksi 的真子串。
  • n,i=1n|si|106。若 ij,则 sisj
  • 2 s / 512 MB

先将所有串拼成一个大串 S 进行后缀排序。考虑枚举 i,求出哪些 j 会产生贡献。

考虑对于 si 的每个后缀 si[x,|si|],在 s1sn 中,找到一个最长的字符串 sy,满足它是 si[x,|si|]前缀,记为 li,x=|sy|。若找不到这样的 sy,则称 li,x 无意义。若某个 li,x 能出现在等式或不等式中进行运算,当且仅当 li,x 有意义。

构造一个二元组不可重集合 Ti,一个二元组 (u,v)Ti 中出现,当且仅当满足以下四个条件:

  • 1u,v|si|
  • li,u 有意义。
  • v=u+li,u1
  • 不存在 w[1,u),使得 u+li,u1w+li,w1

sjTi 中出现,当且仅当存在 (u,v)Ti 使得 si[u,v]=sj

再构造一个二元组不可重集合 Ri,j,一个二元组 (u,v)Ri,j 中出现,当且仅当满足以下两个条件:

  • 1uv|si|
  • si[u,v]=sj

那么原题目中的二元组 (i,j) 满足条件,当且仅当 Ri,jRi,jTi

Ri,j 很显然,就不证了。

证明:

  • 充分性:

    考虑反证,假设当 Ri,jTi 时存在 k 使得 sjsk 的子串且 sksi 的真子串。

    si[a,b]=sksk[c,d]=sj。那么 si[a+c1,a+d1]=sj。根据已知条件可以得到 (a+c1,a+d1)Ri,j,Ti

    li,a+c1dc+1,则与 (a+c1,a+d1)Ti 的第三个条件不符。

    否则,此时 c>1。根据 li,a 的定义可知 li,aba+1,即 a+li,a1b。但是 a+c1+li,a+c11=a+c1+dc+11=a+d1。根据 dba+1 可以得到 a+d1b。与 (a+c1,a+d1)Ti 的第四个条件不符。

    因此假设不成立。当 Ri,jTi 时一定不存在 k 使得 sjsk 的子串且 sksi 的真子串。

  • 必要性:

    考虑 (u,v)Ri,j 但是 (u,v)Ti。此时 (u,v) 一定满足某个二元组在 Ti 中的前两个条件。

    li,uvu+1,则有一个更长的字符串 sysi[u,|si|] 的前缀。此时 sjsy 的真子串。

    否则,一定存在 w[1,u) 使得 u+li,u1w+li,w1。说明存在一个字符串 sy=si[w,w+li,w1]。此时 sysj 包含在中间,即 sjsy 的真子串(能保证是真子串是因为 w[1,u))。

    所以不满足 Ri,jTi 一定不会满足原来的条件,这是一个必要条件。

光有这个结论还不够,总不可能求出集合然后枚举判断。

进一步推理可以得到,它其实等价于 (u,v)TiP(si[u,v]=sj)=|Ri,j|

为了区分中括号和艾弗森括号,使用 P(A) 表示 A 命题是否为真。当且仅当 A 为真命题时 P(A)=1;当且仅当 A 为假命题时 P(A)=0

为什么呢?不难发现 (u,v)TiP(si[u,v]=sj)=|TiRi,j||Ri,j|。而 |TiRi,j|=|Ri,j| 等价于 Ri,jTi

那么我们只需要对于一个 j,求出 (u,v)TiP(si[u,v]=sj)|Ri,j| 即可。

  • 前者的求法:

    首先要得到 Ti。可以考虑对于每个字符串 sa,它会对哪些排名的后缀的产生贡献。这个后缀要包含 sa,等价于两者 |LCP||sa|。可以维护 height 数组的 ST 表然后二分得到排名区间,让这个区间内的 l 值对 |sa|max。线段树维护即可。

    然后就可以从左往右扫,维护前缀的 u+li,u1w+li,w1 最大值 pre。在线段树上单点查询当前后缀排名那个位置的值得到 li,x。若 x+li,x1>pre,则将 (x,x+li,x1) 加入 Ti

    然后使用桶维护 Tis1sn 中每种字符串各作为多少个后缀的最长前缀。

  • 后者的求法:

    考虑 sj 作为某个后缀的前缀出现,同样可以求出包含它的后缀排名区间。然后变成求区间内有多少个排名使得这个排名的后缀来自于编号为 i 的字符串。

    对于每个 i 用一个 vector 从小到大存放其后缀出现的位置,二分得到左右端点 l,r,答案即为 rl+1

这样仍需要枚举 j。但你注意到:

由于 Ri,j,那些没在 Ti 中出现的 sj 一定没有贡献。因为此时 |TiRi,j|=0<|Ri,j|。只需要考虑那些在 Ti 中出现的 sj。而对于每个 u,只会有一个 v 使得 (u,v)Ti,因此 |Ti|=O(|si|)。这样一来总共只需要处理 O(|S|) 次。

为了不算重算漏,考虑对于每个在 Ti 中出现的字符串,在其第一次出现的位置统计。

最后对 s1sn 的每个字符串都这样做一遍,就能得到正确答案了。

时空复杂度均为 O(|S|log|S|)

AC code

CF1608G Alphabetic Tree

  • 给出一棵 n 个点的树,边 i 上有字母 ci。定义 str(u,v) 为从点 u 走到点 v 途径边上的字母顺次拼接得到的字符串。形式化的,若点 u 到 点 v 路径上的边依次为 p1,p2,,pk,则 str(u,v)=cp1cp2cpk

  • 你有 m 个字符串 s1smq 个询问,每个询问形如 u v l r,你要回答 str(u,v)slsr 中出现了几次。在一个串中重复出现算多次。

  • n,m,q,i=1m|si|105

考虑将答案表示成差分的形式并离线计算,即 [1,r] 的答案减去 [1,l) 的答案。扫描右端点,每次将端点的串加入贡献,然后回答所有端点为该点的询问。这是大体思路。

先套路将所有串用奇怪字符拼接起来(拼成的大串记为 S),做一遍后缀排序,求出 sa,rk,height 数组以及 height 数组的 ST 表。根据题意,下文默认 n,m,q,|S| 同阶。

hdisi 开头的后缀在 S 中出现的位置,sufiSi 开头的后缀。则每个询问的答案为 [1,hdr+|sr|] 的答案减去 [1,hdl) 的答案。

我们试图把 str(u,v) 表示成 slsr 的某个后缀的前缀的形式。而这些后缀的排名一定是连续的。

因此,我们想要求出这些后缀排名的区间。我们发现 str(u,v) 的字典序一定不大于这些后缀。因为这些后缀以 str(u,v) 为前缀,相当于在 str(u,v) 后面接上某个串,而 str(u,v) 本身可以看作 str(u,v) 接上一个空串(在后缀排序种认为空串的字典序最小),肯定是这些串中字典序最小的。

考虑二分出字典序大于等于 str(u,v) 的后缀的最小排名 rkl。设当前二分的排名为 mid,我们要比较这两个串的大小。由于是树上询问,因此 |str(u,v)| 可能会很大,不能将它们和 s1sm 一起后缀排序。想象一下我们是怎么手动比较字典序大小的,肯定是先找一段 LCP,然后比较下一个不相等的位置。

考虑使用哈希。若按照常规方法直接使用树剖、线段树或树上倍增维护路径的哈希值再二分 |LCP|,单次比较的时间复杂度会达到 O(lognlog|S|)。这么一来总的时间复杂度为 O(qlognlog2|S|),无法接受。

我们发现二分 |LCP| 会产生很多无用的比较,所以考虑把这个 log|S| 优化掉。我们知道,uv 的路径会被分成 O(logn) 条重链。我们可以按照顺序比较一条条重链的哈希值,如果整条重链能匹配上就直接跳过,否则再二分找那个失配的位置。

具体地,先计算 S 的前缀哈希值 H,再对树做一遍重链剖分,预处理每条重链自上而下的前缀哈希值 Hu 以及自下而上的后缀哈希值 Hd。还需要有三个求区间哈希值的函数。

每次询问 u,v 时,将路径上的重链、当前路径在该重链上经过了第几个位置到第几个位置该重链是自上而下走还是自下而上走按顺序存放在一个容器里。注意两个点跳到同一重链上的情况。这个过程还需要求出 LCA

然后遍历容器,一段段与排名为 mid 的后缀的对应位置匹配。若能够匹配上,则将已匹配长度加上当前路径在该重链上的长度,并跳到下一条链继续匹配。否则二分寻找失配位置,将已匹配长度加上 |LCP|,并比较失配位置的两个字符的大小。需要记一个 p 表示已经匹配好到该后缀的第 p 个位置。注意 p 和已匹配长度的关系。你会感觉这个过程有点像值域分块。

这部分细节繁多,比如匹配的方向、需要匹配的长度、还未匹配的长度、以及一方匹配完之后如何比较大小等。为了方便,这个过程需要求出两个值,|LCP| 以及大小关系。

求出来后,若 |LCP| 不等于路径长度 len,则不存在这样的后缀使得 str(u,v) 为其前缀。否则再二分找到最大的排名 rkr 使得 LCP(sufsarkl,sufsarkr)len,这个用 height 数组的 ST 表求 LCP 就行了。

设当前扫描到的右端点为 rpos,现在求出来了 rklrkr,问题变成在 suf1sufrpos 中,有多少后缀的排名在 [rkl,rkr] 之间。这是一个平凡的二维数点问题,使用树状数组维护,扫描时将 rkrpos 插入到树状数组中,询问就是树状数组上区间 [1,rkr] 的和减去区间 [1,rkl) 的和。至此问题被解决。

时间复杂度为 O(qlognlog|S|),空间复杂度为 O(|S|)。可以接受。

离线树状数组二维数点做法提交记录(含代码)

考虑如何在线地解决这个问题。

这个时候我们把离线树状数组换成主席树即可,将所有后缀 sufi 插入主席树版本 [1,i]rki 位置。仍然求出 rklrkr,然后把原来的离线差分变成 [1,hdr+|sr|][1,hdl1] 两个版本相减即可。

时间复杂度为 O(qlognlog|S|),空间复杂度为 O(|S|log|S|)。可以接受。

在线主席树二维数点做法提交记录(含代码)

UVA10829 L-Gap Substrings

以此题开始连续三题用到的都是同一个套路,做法阐述可能有重复累赘之处,但是都是从原来的题解里复制粘贴的,对于三题的做法都保留了对于相同套路的阐述部分。

  • 给出字符串 s 和常数 g,求出有多少四元组 (l1,r1,l2,r2),满足 s[l1,r1]=s[l2,r2]r1+g+1=l2

  • T 组数据,1T,g10|s|5×104

先后缀排序。

考虑一对合法的 s[l1,r1]s[l2,r2] 来自分别哪个后缀。假设它们分别来自排名ij 的后缀(显然 sai=l1,saj=l2)。令 u=min{i,j},v=max{i,j},此时 u<v。则一定满足,|LCP(s[sau,|s|],s[sav,|s|])||sausav|g。因为它们已经有了 r1l1+1 这个长度的最长公共前缀。这种情况下我们称 (l1,r1,l2,r2) 这个四元组与 (u,v) 这个二元组对应。对于同一对 (l1,r1,l2,r2),由于 l1,l2 都是确定的,所以它只会与唯一一对 (u,v) 对应。

我们再考虑对于一个 (u,v)(u<v) 的二元组,若满足 |LCP(s[sau,|s|],s[sav,|s|])||sausav|g,此时令 i=min{sau,sav},j=max{sau,sav},则 (i,jk1,j,2jki1) 一定是一个合法的四元组。我们让它们对应。则一个对于二元组 (u,v),它们已经确定了两个串的起点,则终点也随之确定了,所以对应了唯一一个合法的四元组。

我们发现合法的二元组与合法的四元组一一对应。

那么只需要统计有多少满足条件的 (u,v)(u<v),任意一个合法的四元组都会在与之对应的二元组处被统计一次后就不再统计,且任意一个合法的二元组统计的都是与之对应的一个合法的四元组,做到了不多、不重、不漏。

考虑转化成 height 数组的限制,则要统计满足一下条件的 (u,v) 个数:

{u<v|sausav|gmini=u+1vheighti

套路地分治,设当且分治区间为 [l,r],中点 mid=l+r2。考虑统计跨过 mid 的贡献。考虑固定 u

midl 扫描 u,处理 mn=mini=u+1midheighti,设 prev=minj=mid+1vheightj,钦定 prer+1 极小。此时 pre 是不升的。则一定存在一个 p[mid+1,r+1] 使得 prep<mn。这种情况下,当 v[mid+1,p)mini=u+1vheighti=mn;当 v[p,r]mini=u+1vheighti=prev

对于 v[mid+1,p) 的部分,就是要统计有多少 v 满足 |sausav|gmn。拆绝对值后二维数点即可。

对于 v[p,r] 的部分,就是要统计有多少 v 满足 |sausav|gprev。同样拆绝对值,但是你发现此时变成三维数点,不能接受。

考虑一个智慧的容斥,对于 v[p,r] 的部分,先统计有多少 v 满足 |sausav|gmn,然后对于 prev<|sausav|gmnv 减去它们的贡献。

由于对于 v[mid+1,p) 的部分 prevmn,所以我们只需要减去全局 prev<|sausav|gmnv 的贡献。这样少了一个 v 的限制,拆绝对值后仍是二维数点,离线 + BIT 解决。

此时,两部分 |sausav|gmn 的贡献都要计算,于是全局维护一个 sav 的值域 BIT 即可。

注意这里拆绝对值和解不等式的细节,尤其是不能忽略 sav[1,|s|] 的这个先天限制。

然后往两半递归求解即可。

这题就做完了,时间复杂度为 O(|s|log2|s|),空间复杂度为 O(|s|)。代码不算很难写。

AC Link

AC Code

P9623 [ICPC2020 Nanjing R] Baby's First Suffix Array Problem

  • 给出长度为 n 的字符串 sm 组询问对 s[l,r] 这个子串进行后缀排序后,(这个子串的)后缀 s[k,r] 的排名。排名定义为比它小的后缀的个数 +1

  • 多组数据,记 N=nM=mN,M5×104

  • 5.00 s / 256.00 MB

这个 N5×1045.00\,s 时限是不是为了放时间复杂度 O((N+M)log3N) 的做法过啊,是的话就太不牛了 /qd。

先对原串进行后缀排序。

考虑从排名的定义入手,求出子串中有多少个后缀比询问的后缀小。对于这些子串中的后缀,考虑找到它们在原串中的后缀,尝试寻找充要条件。

设有(子串的)后缀 s[i,r],其中 i[l,k)(k,r]。按两类情况考虑。

  • rki<rkk

    此时 s[i,r]<s[k,r]当且仅当 i<k|LCP(s[i,n],s[k,n])|rk,或 i>k

    • 充分性

      i<k|LCP(s[i,n],s[k,n])|rk 时,两个后缀第一个不同的位置一定均在 s[i,r]s[k,r] 中出现,此时比较两个串也是比较这两位,因为 rki<rkk,故 s[i,r]<s[k,r]

      i>k 时,若两个后缀第一个不同的位置均在 [l,r] 中出现则与上一种情况合理,否则 s[i,r]s[k,r] 的前缀,故 s[i,r]<s[k,r]

    • 必要性

      考虑 s[i,r]<s[k,r] 时,若 i<k,则一定有 |LCP(s[i,n],s[k,n])|rk,否则 s[k,r]s[i,r] 前缀,此时 s[k,r]<s[i,r]。若 i>k,则已经满足条件。

    • 做法

      i<ki>k 讨论。

      i<k,则需要统计有多少个后缀 s[i,n] 满足 i[l,k)rki<rkk|LCP(s[i,n],s[k,n])|rk。降第三个限制转化为 height 数组的限制,其等价于 minj=rki+1rkkheightjrk。容易发现此时满足条件的 irki 在一个前缀 [1,x] 中,其中 x<rkk。二分 + RMQ 求出这个 x,问题转化成统计有多少个点对满足 i[l,k)rki[1,x],主席树维护即可。

      i>k,则需要统计有多少个后缀 s[i,n] 满足 i(k,r]rki<rkk,同样主席树维护。

  • rki>rkk

    此时 s[i,r]<s[k,r]当且仅当 i>k|LCP(s[i,n],s[k,n])|ri+1

    • 充分性

      容易发现此时 s[i,r]s[k,r] 前缀,故 s[i,r]<s[k,r]

    • 必要性

      考虑证明不满足上述条件则 s[i,r]>s[k,r]

      i<k,如果两个串第一个不同的位置均在 [l,r] 中出现,因为 rkk<rki,所以 s[i,r]>s[k,r]。否则,s[k,r]s[i,r] 前缀,此时 s[i,r]>s[k,r]

      i>k|LCP(s[i,n],s[k,n])|ri,则两个串第一个不同的位置一定均在 [l,r] 中出现,因为 rkk<rki,所以 s[i,r]>s[k,r]

    • 做法(本题解最核心部分)

      以排名为下标做一遍序列分治,将询问挂在 rkk 上,每层分治考虑右半边对左半边的贡献(很像 cdq 分治)并左右递归下去统计,则对于任意一个合法的后缀,根据分治树的形态,一定存在且仅存在一层分治,使得询问在左半边,后缀在右半边,此时它被统计到。并且,在每层分治中我们统计合法的贡献,可以做到不重不漏。

      设分治区间为 [L,R],中点 mid=L+R2

      对于左半边的一个询问 (l,r,k),我们要统计右半边有多少个 i,满足:

      • sai(k,r]

      • |LCP(s[k,n],s[sai,n])|rsai+1minj=rkk+1iheightjrsai+1

      采用序列分治的一般套路,从 midL 扫描询问。设当前扫到的排名为 K。维护变量 mn=minj=K+1midheightj。对于右半区间维护前缀 height 最小值,即 pmni=minj=mid+1iheightj。则对于当前扫到的排名上的询问,条件中的 minj=rkk+1iheightjri+1 可以转化为 min{mn,pmni}

      容易发现 pmni 具有单调(不升)性。可以找到一个分界点 p,使得当 i[mid+1,p) 时,min{mn,pmni}=mn;当 i[p,R] 时,min{mn,pmni}=pmni

      对于分界点左边的情况,就是统计有多少 i 满足:

      • sai(k,r]

      • mnrsai+1sairmn+1

      • i[mid+1,p)

      整理一下就是:

      • sai[max{rmn,k}+1,r]

      • i[mid+1,p)

      容易主席树维护。

      对于分界点右边的情况,就是统计有多少 i 满足:

      • sai(k,r]

      • pmnirsai+1sai+pmnir+1

      • i[p,R]

      你发现这是个三维数点,好像行不通啊!

      然后就是一个很妙的转化了。考虑正难则反。你发现对于分界点右边的情况,sai+pmnir+1sai+mnr+1,因为在分界点右边 pmni=min{pmni,mn}。所以可以先统计满足以下条件的 i 的个数:

      • sai[max{rmn,k}+1,r]

      • i[p,R]

      算上分界点左边的统计,相当于要统计右半边满足 sai[max{rmn,k}+1,r]i 个数。可以 vector + 二分统计。考虑哪些不合法的被统计了,显然它满足:

      • sai[max{rmn,k}+1,r]

      • sai+pmnir

      • i[p,R]

      于是就要减去这样的 i 的个数。实际上这还是个三维数点,不过你发现,i[mid+1,p),sai[max{rmn,k}+1,r]sai+pmnir。即分界点左边不存在满足前两个条件的 i

      为什么呢?首先 sai[max{rmn,k}+1,r] 的必要条件是 sairmn+1。你考虑分界点左边 mnpmni,若 sairmn+1sai+mnr+1,则一定有 sai+pmnir+1。反之,若 sai+pmnir,则一定有 sai+mnrsairmn。因此两个条件不能被同时满足

      所以我们直接大胆忽略 i[p,R] 这个条件,统计全局(当前分治区间) sai[max{rmn,k}+1,r]sai+pmniri 的个数。同样是二维数点,主席树维护即可。

至此两类统计都解决了。接下来算复杂度。因为有主席树和 ST 表,所以空间复杂度显然为 O(NlogN)

至于时间复杂度(只说每个部分的瓶颈),后缀排序是 O(Nlog2N) 的(因为不是瓶颈所以没用基数排序优化)。rki<rkk 的统计需要往主席树中插入 O(N) 个点对,并且每次询问要进行一次 O(1) 检查(ST 表)的二分以及 O(logN) 的主席树查询,时间复杂度为 O((N+M)logN)

对于分治部分,每个询问会在 O(logN) 层分治中被扫到,每次扫到要做一次主席树查询和 vector 二分,单次是 O(logN)。每个点对会在 O(logN) 层分治中被插入主席树,单次也是 O(logN)。这部分的时间复杂度为 O((N+M)log2N)。为了维护主席树,每层分治需要将点对进行排序,由于每层分治的区间总长度为 N,因此任意一层排序的 log 不超过 O(logN)。容易通过乘法分配律得到是 O(Nlog2N) 的。因此,分治部分的总时间复杂度为 O((N+M)log2N)

综上,本做法时间复杂度为 O((N+M)log2N),空间复杂度为 O(NlogN),可以接受。

代码

AC 记录

CF1098F Ж-function

  • 给出长度为 n 的字符串 s。定义 Ж(l,r)=i=lr|lcp(s[l,r],s[i,r])|q 次询问,每次给出 l,r,查询 Ж(l,r)

  • n,q2×1056 s / 500 MB

先后缀排序。

将子串的 lcp 搞成后缀的 lcp,则 Ж(l,r)=i=lrmin{|lcp(s[l,n],s[i,n])|,ri+1}

然后将询问挂在 rkl 上,分别计算 rki<rklrki>rkl 的贡献,最后算上 l 本身的贡献。此时可以将 lcp 的限制转化成 height 数组的限制,即:

Ж(l,r)=i=1rkl1([lsair]min{minj=i+1rklheightj,rsai+1})+i=rkl+1n([lsair]min{minj=rkl+1nheightj,rsaj+1})+(rl+1)

我们先以 rki<rkl 的情况为例讲一下怎么算贡献。

height 数组进行序列分治(其实此处比较像 cdq 分治),记当前分治区间为 [L,R],中点 M=L+R2N=RL+1。考虑当前层右半边对左半边的贡献。

prej=mink=M+1Rheightk。对于左半边按 ML 的顺序扫描 i,并同时记录 mn=mink=i+1Mheightj

考虑挂在 i 上的一个询问 (l,r)

此时,存在 p[M+1,R] 使得当 j[M+1,p)mnprej;当 j[p,R]mn>prej

化简第二层 min{},那么右半边对 (l,r) 的贡献就是:

j=M+1p1([lsajr]min{mn,rsaj+1})+j=pR([lsajr]min{prej,rsaj+1})

由于 i 递减,mn 不升,因此 p 不降,最多递增 O(N) 次。

然后将 min{} 拆开,即讨论一下谁是最小值,此处我们只讨论前面那个数更小(不等于)的情况。因为两种讨论都是类似的。

对于 j[M+1,p) 的部分,我们要求 j=M+1p1([lsajrmn<rsaj+1]mn),提取公因式 mn 后发现是关于 j,saj 的二维偏序。可以用树状数组维护 saj,在 p 移动时更新树状数组(就是扫描线)。

对于 j[p,R] 的部分,我们要求 j=pR([lsajrprej<rsaj+1]prej)

接下来是重点,也是这个套路最巧妙的一步。

如果按照之前的方法找偏序关系,发现是关于 j,saj,saj+prej 的三维偏序。你要是在分治内部再套个树套树 / cdq 分治的话复杂度肯定爆炸。

我们先求 j=pR([lsajrmn<rsaj+1]prej)。这东西拆开后是二维偏序,树状数组类似维护。

由于右半边 prej<mn,漏算的贡献是 j=pR([lsajrprej<rsaj+1mn]prej)。你发现这还是个三维偏序,那不是白搞?别急,你发现当 j[M+1,p) 时,mnprej,即 prej<rsaj+1mn 不成立。因此直接忽略掉 j 这一维限制即可!那么剩下的就是关于 saj,saj+prej 的二维偏序,由于扫描的是 p,所以离线下来再树状数组维护即可。

那么这种情况就讨论完了。剩下的一种情况是类似的,尤其是对于 [p,R] 这部分贡献三维偏序转二维偏序的时候,都是将 mn 代入二维偏序,再加上 (prej,mn] 漏算的 / 减去 (prej,mn] 多算的,然后通过不同区间 prej,mn 大小关系忽略 j 那一维限制。

至于 rki>rkl 的情况,只是需要再分治的时候换成扫描右半边,对左半边维护后缀最小值,计算贡献部分经过瞪眼观察或手推后都可以发现是一模一样的。

那么这题就做完了。

i 这个位置上挂了 Qi 个询问。可以发现一层分治的时间复杂度为 O((N+i=LRQi)logn)。考虑到分治树的深度为 O(logn),且对于同一深度的区间而言 N=n,Qi=q。所以总的时间复杂度为 O((n+q)log2n),空间复杂度为 O(n+q)。常数较大,但是目前洛谷最优解第三。实现细节看代码吧。

AC Link & Code

CF587F Duff is Mad

  • 给出字符串 (s1,,sn)q 次询问 sl,,srsk 中作为子串出现的次数。
  • n,i=1n|si|105
  • 4 s / 250 MB

首先将全部询问离线,维护一个前缀中的串在某个串中的出现次数然后差分解决。将所有串拼成大串 S 进行后缀排序。此时 O(|S|)=O(n)

我们可以将在一个字符串中出现转化为在一个后缀作为前缀出现。当 i 加进前缀的时候,可以找到以它为前缀的所有后缀。这样的后缀排名形如一个区间 [L,R],则排名在这个区间内的后缀出现次数都要加 1

串长一定时可以阈值分治。设阈值为 B。则对于 sk 长度不超过 B 询问,可以直接暴力枚举那个串的后缀,设查询的时间复杂度为 O(Q1(n)),则这部分可以做到 O(BQ1(n))

对于 sk 长度大于 B 的询问,显然不能使用上述查询方式。此时询问的串 sk 只有 O(nB) 种。考虑对于这些串单独计算答案,则 sisk 中出现的次数相当于在 sk 的后缀排名集合中查询 [L,R] 内的数的个数。设查询的时间复杂度为 O(Q2(n)),则这部分的时间复杂度为 O(nBQ2(n))

考虑使用分块解决。对于小串,使用 O(n) 区间加、O(1) 单点查询的分块解决。

对于大串,每个都用分块维护一个值域前缀和表示集合中不超过某个值的元素个数,那么对于这个大串排名集合中的一个元素,它产生的影响是一段后缀的值域前缀和加 1。到了查询的时候,直接单点查询 RL1 位置上的信息即可。同样使用 O(n) 区间加、O(1) 单点查询的分块解决。

注意此时空间复杂度为 O(n2B)

此时,取 B=O(n),可以做到 O(nn) 的时空复杂度。但是空间开不下,因此取了个比较接近的,复杂度不变,会带一个较大的常数。结果是没跑过 O(nnlogn)。。。

O(nn) Code

其它复杂度的 SA 做法参见题解区。

P8203 [传智杯 #4 决赛] DDOSvoid 的馈赠

  • 给出 n 个模板串 (s1,,sn)m 个查询串 (t1,,tm)。有 m 次询问,每次给出 x,y,求有多少个模板串同时是 tx,ty 的子串。

  • n,m,q105

  • 4.00 s\space / 512 MB

考虑将所有串用分隔符拼一起后缀排序,然后对于每个排名为 i 的后缀,记录 ci 表示它来自哪个字符串。对于一个模板串 si,二分 + ST 表求出包含它的后缀排名区间 [li,ri]。那么对于 (x,y) 这个询问,就是求有多少个 i,满足 c 数组 [li,ri] 这个区间内出现了 x,y 这两种权值。

其实来自分隔符和查询串的后缀是没用的,因此可以在 c 数组中删去这些位置。记新数组为 C。在新数组上,记 [Li,Ri] 表示 C 中排名在 [li,ri] 内的后缀的区间。那么就是对于 C,求有多少个 [Li,Ri] 内出现了 x,y 两种权值。因为 C[Li,Ri] 就是 c[li,ri] 删去一些不可能为 x,y 的位置,因此在 C[Li,Ri] 中一定也出现了 x,y

c 转化成 C 纯粹是为了卡常。

考虑转化后的问题,记 cntx 表示 xC 中的出现次数。不妨令 cntxcnty,不符则交换两者即可。接下来考虑钦定 [Li,Ri] 最左边的 x 是哪一个,将每种情况的数量相加。

遍历 Cx 的位置 j,记它左边最后一个 x 的位置为 p1,左边最后一个 y 的位置为 p2,右边第一个 y 的位置为 p3

首先需要满足 LijRi。由于 j 是最左边的 x,因此应满足 Li>p1。接下来考虑 [Li,Ri] 内是否存在一个比 j 位置更左的 y,然后将两种情况的个数相加。

若存在,则应满足 Lip2;否则应满足 p2<LiRip3。容易证明如果满足上述条件区间内一定存在 x,y。否则,因为 (p2,p3) 内不存在 y[Li,Ri] 就不满足该情况下的条件。

那么上面讨论的这些情况的答案全部都是二维数点,容易解决。

问题是暴力遍历 x 的所有位置真的能接受吗?

考虑将 O(n) 个询问去重。若 cntxn,则最多带来 O(nn) 个询问。否则,考虑那些 cntx>n 的询问的 y,此时只有 O(n) 个本质不同的 y。对于这些 y,和它构成询问的每个 x 会带来 O(cntx) 个询问。而因为去过重,因此这些 x 都是不同的。因此 cntx 的和不超过 O(n),所以询问数量还是 O(nn) 个。

那么变成 O(n) 个点 O(nn) 个询问的二维数点,且都是 3-side 矩形,扫描 1-side 那一侧,剩下一维维护前缀和,使用 O(n) 区间加,O(1) 单点查的分块即可做到 O(nn)

还有一个问题,怎么快速求出 p2,p3?考虑离线,对于每一种 y 单独求解。维护此时每个位置对应的 p2,p3,那么对于一个 y 的位置 j,它可以对它后面位置的 p2 和它前面位置的 p3 产生贡献。分别使用 O(n) 区间取 min/maxO(1) 单点查的分块维护,由于每个 y 做一次,因此 C 中每个位置至多带来一次区间修改,而单点查询数和上面询问数同阶,因此这部分仍是 O(nn)

那么我们得到了一个时空都是 O(nn) 的做法,空间爆炸。原因是存不下那么多询问。可以考虑设置一个阈值 B,每产生 B 个询问就数一次点并清空。这样修改部分常数会变大因为每数一次点就要修改一次。但是空间的问题解决了,那部分常数也可以忽略不计。

事实上 B107 左右可以通过本题。

AC Link

Code

CF917E Upside Down

  • 给出 n 个点的树,第 i 条边上有字母 ci。有 m 个字符串 s1sm 以及 q 组询问。每次询问给出 x,y,k

  • str(x,y)x,y 简单有向路径边上的字母按顺序拼接得到的字符串,形式化地,若 x,y 简单有向路径上一共有 E 条边,记 eix,y 有向路径上的第 i 条边,则 str(x,y)=ce1ce2ceE

  • skstr(x,y) 中出现了多少次。形式化地,求有多少个正整数 i[1,|str(x,y)||sk|+1] 使得 j[0,|sk|1]Z,(sk)i+j=[str(x,y)]i+j

  • M=i=1m|si|n,m,M,q105

  • 3 s / 512 MB

约定:

  • 本文中所有下标均从 1 开始。钦定 1 为根。用打印机字体(\texttt)表示具体的字符 / 字符串。

  • 默认 O(n)=O(m)=O(M)=O(q)O(n)>O(log2n)

  • 记一个点 u 的父亲为 fau,深度(到根的边数)为 depu

  • xy 表示 xy 的简单有向路径,ufau 这条边上的字符为 valu。特别地,val1=1

  • lca(x,y) 表示 x,y 两点的最近公共祖先。lcp(x,y) 表示两个字符串 x,y 的最长公共前缀。

  • 记一个串 s 的反串为 sR。形式化地,|sR|=|s|siR=s|s|i+1

  • anc(k,x) 表示 x 向上走 k 条边到达的点,即 x 的树上 k 级祖先。

  • 若字符参与运算,则其值等于其 ASCII 值。


考虑弱化版 CF1045J 的做法,sk 出现的位置要么完全包含在 xlca(x,y)lca(x,y)y 两条直链内,要么跨过了 lca(x,y)

Case 1:完全包含在直链内的情况

在这部分中考虑使用哈希实现字符串匹配。我们的哈希方式为多项式哈希,即对于字符串 s,其哈希值为:

H(s)=i=1|s|(sibase|s|i)modMOD

其中 base 为乘数,MOD 为模数。本题卡乘数和模数,我使用了【】生日的日期做乘数,109+9 做模数。不能自然溢出,因为后面需要用到逆元。

在弱化版中,我们运用 |S|100 的条件,对于每一种长度的字符串单独处理。在此题中我们也可以如法炮制,需要运用到一个引理:

Lemma 1

在字符串总长度为 n 的长为 m 的字符串序列 s1sm 中,本质不同的字符串长度种数为 O(n) 级别。

Proof 1

考虑 s1sm 种出现了 k 种本质不同的长度,从小到大依次是 len1lenk,记 cnti 表示第 i 种长度的出现次数,形式化地,cnti=j=1m[|sj|=leni]

那么有:i=1k(lenicnti)=n

可以发现 lenii,cnti1。后面那个很显然,因为这种长度出现时一定存在一个字符串满足其长度为 leni

至于前面那个使用归纳法证明:

  • i=1len11 显然成立。

  • 假设对于 i[1,p]Z 时成立,则对于 i[1,p+1]Z 时,由于 len1lenk 中的每一个数都是一种本质不同的长度,且从小到大排列,所以 lenp+1>lenpp。由于都是整数,所以 lenp+1p+1

由于这里涉及到的量都是正的,所以 lenicntii,因此 i=1kii=1k(lenicnti)=n,因此有 k2+k2n。可以得到 k2n=O(n)。注意这里不是在解不等式,由 k2+k2n 推导出一个成立的条件。

证毕。

那么我们可以对于这 O(n) 种长度分别求解。在求解一种长度 len 的询问时,我们对于每个点 u 预处理 str(u,anc(len,u))str(anc(len,u),u) 的哈希值 ukudku(若不存在则不处理)。需要先预处理 upudwnu 表示 str(u,1)str(1,u) 的哈希值。容易得到:

  • upuupfau+valubasedepu1(modMOD)

  • dwnudwnfaubase+valu(modMOD)

由于这个做法比较垃圾,我们不能在求解每种长度时重新遍历树计算哈希值,否则会超时。可以考虑牺牲空间,在第一次遍历树时就对于每个点存下这些哈希值。这样可以省去 O(n) 次遍历树的时间。

ukudku 都可以通过与 anc(len,u) 的哈希值差分得到,注意差分时的移位操作。

具体地:

  • ukuupuupanc(len,u)basedepulen(modMOD)

  • dkudwnudwnanc(len,u)baselen(modMOD)

可以预处理 base 的若干次方以及对应的逆元。考虑怎么快速求 anc(len,u)。由于查询次数为 O(nn),所以单次查询必须为 O(1)。可以考虑维护 1u 构成的序列 stk,使得 stki 为路径上的第 i 个点。则所求即为 stkdepulen。每次遍历到一个点 u 时,将 u 加入序列末尾。结束 u 的遍历时,将 u 从序列末尾删除。

在求解每种长度时再考虑对于每种询问串的哈希值 hsh 单独求解。考虑记 num0,u 表示 1u 上有多少个点 v 满足 ukv=hshnum1,u 表示 1u 上有多少个点 v 满足 dkv=hsh。这个可以考虑从 1n 扫描 i,依次维护 v[1,i] 的情况,则每次新扫到一个位置,需要修改(+1)的值拍平成 dfn 序后形如一段区间。

至于询问,对于 xlca(x,y) 那条链上的贡献,考虑匹配的起点在哪个位置,容易发现链上的一个点 u 能够匹配当前询问串当且仅当 depudeplca(x,y)len,且 uku=hsh。因为这样才能包含在直链内。进一步发现满足这个条件的点位于 xanc(depxlendeplca(x,y),x) 上,那么拿 num0,xnum0,anc(depxlendeplca(x,y),x) 差分即可。lca(x,y)y 的查询方式类似,注意此时匹配的方向是自上而下,用 num1 值差分计算。

此时,一共有 O(nn) 次修改,O(n) 次查询,维护以 dfn 序为下标的差分数组,那么只需要分块支持 O(1) 单点修改,O(n) 前缀查询即可。

值得注意的是,为了将同种哈希值的询问一起做,考虑使用排序将它们排在一个连续的区间内时,需要使用基数排序确保排序复杂度线性,才能保证 O(n) 次排序的总复杂度为 O(nn)


Case 2:跨过直链的情况

考虑分别处理每种串 sk 的询问。

假设跨过直链的匹配发生在 uv 上,其中 u,vxy 上的节点且 uv 前。此时一定满足,str(u,lca(x,y))sk 的前缀,str(lca(x,y),v)sk 的后缀。

同时,str(u,lca(x,y))str(x,lca(x,y)) 的后缀,str(lca(x,y),v)str(lca(x,y),y) 的前缀。

考虑找到最长的长度 P,Q,使得 str(x,lca(x,y)) 存在长度为 P 的后缀为 sk 的前缀;str(lca(x,y),y) 存在长度为 Q 的前缀为 sk 的后缀。

考虑一个基础问题:

给出字符串 a,b,找到 b 的最长前缀使得它是 a 的后缀。求出这个最长长度。

解决方法是:找到 a 的一个后缀 a[i,|a|] 使得 |lcp(a[i,|a|],b)| 最大。记 L=|lcp(a[i,|a|],b)|,则 a[i,|a|] 最长的长度不超过 Lborder 的长度即为所求。

接下来证明正确性。

Proof 2

首先,这个 border 一定是同时是 b 的前缀和 a 的后缀。因为它是 a[i,|a|] 的前缀又是它的 border,说明 a[i,|a|] 存在这个 border 作为后缀。自然 a 也存在这个 border 作为后缀。记这里求出来的长度为 len

考虑是否存在更长的答案。假设存在更长的答案长度为 tmp,其一定不超过 L,不然 a[i,|a|] 就不是使得 lcp 长度更大的后缀了。此时,a[i,|a|]b 存在长度为 tmplcp。这时候 a[i,|a|] 开头的 tmp 个字符形成的字符串与结尾的 tmp 个字符形成的字符串相等。此时 tmpa[i,|a|] 的一个更长的、长度不超过 Lborder,矛盾。

因此不存在更长的答案,len 即为所求。

证毕。

将原问题转化成上述形式,那么 P 就是 str(lca(x,y),x) 最长的前缀长度满足它是 skR 的后缀。这个和原问题显然是等价的,因为两个询问串都是原问题询问串的反串。不妨令新问题答案为 w,则反过来后原串对应的位置也相等,原问题的答案至少w;若原问题存在更长的答案 z,则反串中这些对应的位置也相等,z 就是新问题的一个更长的答案,与 w 的定义矛盾。

因此,P 就是在这个基础问题中 b=str(lca(x,y),x),a=skR 的情况;Q 就是在这个基础问题中 b=str(lca(x,y),y),a=sk 的情况。

先对 sk 以及 skR 进行后缀排序。

最长的长度不超过 Lborder 很好求,由于要求的是某个后缀的 border,在其反串上就变成了前缀的 border,这两个问题也是等价的,和上面的证明类似。

因此,对于 skskR 建立失配树 TkTkR,并进行轻重链剖分。找到满足条件的后缀后,先看一下它的反串对应的是哪一棵失配树,然后在失配树上一条一条重链向上跳。失配树的根链是单调递减的(从节点到根)。若链顶大于 L,就整条链跳过,否则在链上二分,单次询问时间复杂度为 O(logn)

接着考虑如何找到使得 L 最大的后缀。这个后缀一定满足,它要么是 a 中最大的字典序大小不超过 b 的后缀,要么是 a 中最小的字典序大小个超过 b 的后缀。换句话说,设这两个后缀与 blcp 长度分别为 A,B,则 L=max{A,B}

接下来给出证明:

Proof 3

设它们的排名分别为 i,j。则一定有 j=i+1。因为根据定义,排名为 i+1 的后缀字典序大小已经超过了 b,但是排名在 [1,i]Z 内的后缀字典序大小都不超过 b

考虑反证,假设排名为 rnk(rnkirnki+1) 的后缀会得到更大的 lcp 长度。

记这个更大的 lcp 长度为 len。分两种情况讨论:

  • rnk[1,i)Z,则排名为 rnk 的后缀的前 len 位均与 b 的前 A 位相同。根据 len 的定义可知其第 A+1 位也与 b 的这一位相同。根据定义,排名为 i 的后缀的第 A+1 位小于 b 的这一位,或者说这一位不存在(空字符)。此时,排名为 i,rk 的两个后缀前 A 位相同都等于 b 的前 A 位。且后者的第 A+1 位大于前者的这一位。说明后者比前者字典序大,这与 rnk[1,i)Z 矛盾。

  • rnk(i+1,|a|]Z,与上一种情况类似推导得到字典序大小上的矛盾即可证明。

证毕。

于是考虑求得排名 i。由于经过后缀排序,即这些后缀的字典序递增,所以答案有单调性,直接二分这个排名即可。

考虑如何求一条链上的字符串和序列上的字符串的最长公共前缀长度。对原树进行轻重链剖分,将边权转化为深度较深的端点的点权,则这条链会被表示成 O(logn) 条连续的重链区间。对于 dfn 序列形成的字符串维护哈希,对 skskR 也维护哈希。

一条一条重链匹配,若能全部匹配上,就算上这些长度,否则二分第一个不同的位置。只有第一条不匹配的重链需要二分,因此时间复杂度为 O(logn)

这部分细节比较多,尤其是一方匹配完的边界情况,具体看代码中的 qlcp 部分。

此时,这个过程已经求出了 lcp 长度,顺带比较一下大小配合套在外面的二分。

那么 P,Q 均被我们求出来了。

求出来之后,我们只需要考虑 xlca(x,y) 的后 P 个位置为开头处形成的匹配,因为不能匹配 sk 更长的前缀了。

记这 P 个位置依次为 u1uP,满足它们按照 lca(x,y)x 路径上的顺序排列;记后 Q 个位置依次为 v1vQ,满足它们按照 lca(x,y)y 路径上的顺序排列。

ui 为开头处能形成合法的匹配,当且仅当一下三点同时满足:

  • sk[1,P] 存在长度为 iborder
  • skR[1,Q] 存在长度为 |sk|iborder
  • i|sk|

证明:

Proof 4

  • 充分性:

    因为 i|sk|,所以跨过了 lca(x,y)。因为 sk[1,P] 存在长度为 iborder,根据 P 的定义可以得到 str(ui,lca(x,y))=sk[Pi+1,P]=sk[1,i];因为 skR[1,Q] 存在长度为 |sk|iborder,类似地,str(v|sk|i,lca(x,y))=skR[1,|sk|i],根据反串的定义得到 str(lca(x,y),v|sk|1)=sk[i+1,|sk|]。两者拼接恰好是 sk

  • 必要性:

    ui 开头处可以形成合法的匹配,首先一定有 i|sk|,因为要跨过 lca(x,y)。其次 str(ui,lca(x,y))=sk[1,i],根据 P 的定义,str(ui,lca(x,y))=sk[Pi+1,P],因此 sk[1,i]=sk[Pi+1,P],即 sk[1,P] 存在长度为 iborder;类似地,str(v|sk|i,lca(x,y))=skR[1,|sk|i]=skR[Q|sk|+i+1,Q],因此 skR[1,Q] 存在长度为 |sk|iborder

证毕。

所以,我们要统计有多少 i 合法,就是要统计有多少 i 满足这三个条件。

转化成失配树上的限制,就是要求有多少 i 满足 iTkP 的根链上,且 |sk|iTkRQ 的根链上。

考虑离线 + 扫描线。对于所有 sk 的询问,将它挂在 TkP 节点上。考虑深度优先搜索 Tk,在过程中一并维护数组 a0a|sk|。其中 aj 表示有多少 i,满足:

  • iTk 的当前搜到的点 u 的根链上。
  • |sk|iTkR 中点 j 的根链上。
  • i|sk|

则只要在 P 处单点查 aQ 即可。

每次新扫到一个点 u,则和上一层深搜相比根链上增加了一个点 u 的贡献。考虑 |sk|u 的贡献,此时首先满足 u|sk|,发现只有它子树内的点的根链经过它,即只要这些点的 aj 值要增加 u 的贡献。拍平成 dfn 序后将 a 映射过去,再进行差分,则需要支持的操作形如区间加、单点查,由于此处修改、询问同阶,树状数组维护即可。每次结束一个点的深搜时,删去它的贡献。

这部分就做完了,时间复杂度为 O(nlog2n)


综上,这个做法时空复杂度均为 O(nn),可以接受。前面也说过空间可以做到线性,只是需要一些精湛的卡常技艺。

AC Link & Code

参考资料

posted @   lzyqwq  阅读(95)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
点击右上角即可分享
微信分享提示