字尾陣列學習筆記

蒟蒻·廖子阳發表於2024-07-17

前言

字尾陣列(Suffix Array,簡稱 SA)是一種解決某些字串問題的常用工具。解決這些字串問題時,經常用字尾陣列對問題進行一定的轉化成其它的模型,然後套用那個模型的解決方法加以解決原問題。

附題單

約定

本文做以下約定:

  • 本文撰寫時間跨度較大,有些符號會出現正體、斜體混用的情況,請讀者甄別。

  • \(\Sigma\) 為字符集。具體的字元(串)使用⌈印表機字型⌋表示,如 \(\texttt{lzyqwq}\)。用 \(|s|\) 表示字串 \(s\) 的長度。本文中字串的下標\(\boldsymbol 1\) 開始,程式碼中視實現方便程度可能會有所差異。

  • \(\overline{c_1c_2\dots c_k}\) 表示字元 \(c_1\sim c_k\) 從左往右依次拼接形成的字串。記 \(s_i\) 為字串 \(s\) 從左至右的第 \(i\) 個字元。若 \(i>|s|\),則認為它為空字元。

  • \(\text{LCP}(s,t)\) 為字串 \(s,t\)最長公共字首。形式化地,\(|\text{LCP}(s,t)|=l\),當且僅當 \(\nexists\,j\in[1,l],s_j\ne t_j\)\(s_{l+1}\ne t_{l+1}\)。下文有時會簡稱為 \(\text{LCP}\)

  • 稱一個字串 \(s\)字典序比字串 \(t\) 小,記 \(|\text{LCP}(s,t)|=l\),當且僅當 \(\nexists \, j\in[1,l],s_j \ne t_j\)\(s_{l+1}<t_{l+1}\)認為空字元的字典序極小

  • \(s[l,r]\) 為字串 \(s\) 刪去前 \(l-1\) 個字母和後 \(|s|-r\) 個字母得到的字串,稱其為字串 \(s\)\(l\)\(r\) 的子串。形式化地,\(s[l,r]=\overline{s_ls_{l+1}\dots s_r}\)。顯然,\(s[1,i]\) 為字串 \(s\) 的一個字首,\(s[i,|s|]\) 為字串 \(s\) 的一個字尾。

  • 對於字串 \(s\),記 \(pre_i=s[1,i]\)\(suf_i=s[i,|s|]\)

  • \(rk_i\) 表示將字串 \(s\) 的所有字尾按照字典序從小到大排序後,字尾 \(s[i,|s|]\) 的排在第幾位。稱為字尾 \(s[i,|s|]\) 的排名。

  • \(sa_i\) 表示排名為 \(i\) 的字尾的起始位置。形式化地,若 \(sa_i=j\),則 \(rk_j=i\),即 \(sa_{rk_i}=i\)

構建

字尾陣列最初被用來解決這樣一個問題:

P3809 【模板】字尾排序

  • 給出一個字串 \(s\),對於 \(i\in[1,|s|]\),求 \(sa_i\)

  • \(|s|\le 10^6\)

表面上這題要求我們求 \(sa_i\),其實我們可以求出 \(rk_i\),然後根據 \(sa_{rk_i}=i\) 求出 \(sa_i\)。即我們需要對所有字尾進行排序。

【方法一:取出所有字尾並進行排序】

這就是最暴力求解字尾陣列的方法,時間複雜度為 \(\mathcal{O}(|s|^2\log |s|)\),空間複雜度為 \(\mathcal{O}(|s|^2)\)

提交記錄(UNAC 45pts)

程式碼

【方法二:字串雜湊加速比較】

方法一的效率主要低在比較兩個字串的大小。根據前文對字典序的定義,我們可以用二分 + 字串雜湊找到兩個字尾的 \(\text{LCP}\),然後比較下一位字元。

這樣一來,單次比較的時間複雜度為 \(\mathcal{O}(\log|s|)\),總時間複雜度為 \(\mathcal{O}(|s|\log ^2 |s|)\),空間複雜度為 \(\mathcal{O}(|s|)\)

提交記錄(卡常後 AC)

程式碼

【方法三:倍增法】

考慮將 \(s\) 的所有字尾的長度用空字元補齊至長度為 \(|s|\) 後,再用空字元繼續補齊為 \(2^{\lfloor\log_2|s|\rfloor+1}\),顯然字尾之間的大小關係不變。

問題轉化為,將以某個位置開頭,長度為 \(2^{\lfloor\log_2|s|\rfloor+1}\) 的用空字元補齊後的字串排序。

\(str(p)_i\) 表示以 \(i\) 為開頭,長度為 \(2^p\) 的用空字元補齊後的字串,\(rk(p)_i\) 為其排名。那麼 \(rk(0)\) 就是單個字元之間的比較,容易求得。考慮如何用 \(rk(p)\) 求得 \(rk(p+1)\)

\(l=2^{p}\)

我們對於每一個位置 \(i\) 維護一個二元組 \((rk(p)_i,rk(p)_{i+l})\),並將這些二元組以第一維為第一關鍵字 ,第二維為第二關鍵字進行排序,就可以得到 \(rk(p+1)_i\) 了。若 \(i>|s|\),則對於 \(p\in[0,\lfloor\log_2|s|\rfloor+1]\)\(rk(p)_i=0\)。這一步是為了令空字元(串)的字典序極小。

簡單理解一下,因為字典序是從左往右比較的,如果左邊的半段不一樣就比較左邊半段,否則比較右邊半段。嚴謹證明的話考慮第一個不同的位置位於哪一半,結合字典序大小關係的定義容易得出上面這個結論是對的。

那麼我們需要進行 \(\mathcal{O}(\log |s|)\) 層排序,若每層排序時間複雜度為 \(\mathcal{O}(T(|s|))\),則總時間複雜度為 \(\mathcal{O}(T(|s|)\log |s|)\)

如果使用 sort / stable_sort,可以做到 \(\mathcal{O}\left(|s|\log^2|s|\right)\)。如果使用基數排序,可以做到 \(\mathcal{O}(|s|\log |s|)\)

AC 記錄

程式碼

但是你會發現上面這份常數太大了。我們可以轉變思路,直接求 \(\text{sa}\) 陣列。具體見模板題第一篇題解程式碼註釋,講得很清楚。本質上是將基數排序後的下標序列作為本輪的 \(\text{sa}\) 陣列,再根據定義同時求出本輪的 \(\text{rk}\) 陣列。

\(\text{height}\) 陣列

定義:\(\text{height}_i=|\text{LCP}(suf_{sa_{i-1}},suf_{sa_i})|\),即排名相鄰的兩個字尾的最長公共字首。

特別規定當 \(i=1\)\(\text{height}_i=0\)

程式碼中有時候 \(\text{height}\) 陣列會以字尾的位置為下標而非字尾的排名,讀者需要自行甄別。

本文程式碼中,常用 \(\text{ht}\) 作為 \(\text{height}\) 的簡寫。

可以說 \(\text{height}\) 陣列是 SA 中最為精華的部分了,正是它使得 SA 能夠靈活解決很多字串問題。

使用雜湊,我們容易 \(\mathcal{O}(|s|\log|s|)\) 地求出這個東西。考慮使用一些確定性的演算法。

\(\bold{Lemma\space 1}\)

\(\text{suf}_{\text{sa}_i}\)\(\text{suf}_{\text{sa}_j}\)(其中 \(i<j\))兩個字尾存在長度為 \(L\) 的公共字首,則 \(\forall\,k\in[i,j)\)\(\text{suf}_{\text{sa}_k}\)\(\text{suf}_{\text{sa}_j}\) 存在長度為 \(L\) 的公共字首。

\(\bold{Proof\space 1}\)

\(k=i\) 時顯然。當 \(k\in(i,j)\) 時,若它不滿足上面這個條件,考慮第一個不同的位置,則會出現 \(\text{suf}_{\text{sa}_i}<\text{suf}_{\text{sa}_k},\text{suf}_{\text{sa}_k}>\text{suf}_{\text{sa}_j}\)\(\text{suf}_{\text{sa}_i}>\text{suf}_{\text{sa}_k},\text{suf}_{\text{sa}_k}<\text{suf}_{\text{sa}_j}\) 的情況,這顯然與字尾排序的定義矛盾了。

根據這個引理我們可以推出一條關鍵性質:

\[\color{red}\bf{height}_{rk_{\boldsymbol i}}\boldsymbol{\ge}\bf{height}_{rk_{\boldsymbol i-1}}\boldsymbol{-1} \]

其中 \(i\ge 2\)

\(\text{height}_{\text{rk}_{i-1}}=0\) 的情況顯然。考慮 \(\text{height}_{\text{rk}_{i-1}}\ge 1\) 的情況。

我們考慮 \(\text{suf}_{i},\text{suf}_{\text{sa}_{\text{rk}_{i}-1}},\text{suf}_{\text{sa}_{\text{rk}_{i-1}-1}},\text{suf}_{i-1},\text{suf}_{\text{sa}_{\text{rk}_{i-1}-1}+1}\) 這五個字尾。我們將它們簡記為 \(s_1,s_2,s_3,s_4,s_5\)

由於 \(\ge 1\) 的限制,\(s_3\)\(s_4\) 的第一位必須相同。

那麼 \(|\text{LCP}(s_1,s_2)|=\text{height}_{\text{rk}_{i}},|\text{LCP}(s_3,s_4)|=\text{height}_{\text{rk}_{i-1}}\)

注意到 \(s_1\)\(s_4\) 刪去第一個字元,\(s_5\)\(s_3\) 刪去第一個字元。那麼 \(|\text{LCP}(s_1,s_5)|= \text{height}_{\text{rk}_{i-1}}-1\),因為只需要減掉刪掉的那一位,剩下這麼多長度的字首是公共的,且由於原來 \(|\text{LCP}(s_3,s_4)|\) 的限制,\(|\text{LCP}(s_1,s_5)|\) 不能更大。

此時我們可以說 \(\text{suf}_i,\text{suf}_{\text{sa}_{\text{rk}_{i-1}-1}+1}\) 兩個字尾間存在長度為 \(\text{height}_{\text{rk}_{i-1}}-1\) 的公共字首。

根據 \(\text{sa}\)\(\text{rk}\) 陣列的定義,我們要 \(s_3<s_4\)。據此我們還可以得到 \(s_5<s_1\)。因為 \(s_1\)\(s_4\) 刪去第一個字元,\(s_5\)\(s_3\) 刪去第一個字元,且 \(s_3,s_4\) 第一個字元相同。因此需要 \(s_3\) 的剩餘部分(即 \(s_5\))更小才能滿足 \(s_3<s_4\)

轉化成 \(\text{sa}\)\(\text{rk}\) 陣列上的關係,就是 \(\text{rk}_{\text{sa}_{\text{rk}_{i-1}-1}+1}<\text{rk}_i\)。因此 \(\text{rk}_{i-1}\in[\text{rk}_{\text{sa}_{\text{rk}_{i-1}-1}+1},\text{rk}_i)\)。利用 \(\text{sa}_{\text{rk}_i}=i\) 這一定義以及上面的引理,可以得到 \(\text{suf}_{i},\text{suf}_{\text{sa}_{\text{rk}_{i}-1}}\) 之間存在長度為 \(\text{height}_{\text{rk}_{i-1}}-1\) 的公共字首。

因為可能存在比它更長的,所以有 \(\text{height}_{\text{rk}_i}\ge \text{height}_{\text{rk}_{i-1}}+1\)

我們考慮最暴力的求解 \(\text{height}_{\text{rk}_i}\) 方法,使用一個指標 \(p\) 表示匹配了多少位,列舉 \(\text{suf}_i\)\(\text{suf}_{\text{sa}_{\text{rk}_i-1}}\) 這兩個字尾的相應位置。一開始為 \(0\),然後一直遞增直到不能匹配為止。時間複雜度為 \(\mathcal{O}(|s|^2)\)

但是運用上面這個性質,我們每次可以從 \(\text{height}_{\text{rk}_{i-1}}-1\) 開始列舉 \(p\)。而在本輪迴圈開頭 \(p\) 仍是 \(\text{height}_{\text{rk}_{i-1}}\),那麼我們只要令 \(p\) 減一即可(當然要和 \(0\)\(\max\))。這麼一來,對於 \(|s|\) 個位置 \(p\) 每次至多減一,那麼總共至多減少 \(|s|\),然後匹配到不能匹配為止時 \(p \le |s|\),因此 \(p\) 增加的次數也是 \(\mathcal{O}(|s|)\) 的。所以我們就可以利用一個指標結合上面的結論求出 \(\text{height}\) 陣列。

放一個求解 \(\text{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\) 為字串,\(h\)\(\text{height}\) 陣列。

會求解 \(\text{height}\) 陣列之後,我們來考慮這樣一件事情

\(\bf{LCP \space theory}\)

\(|\text{LCP}(\text{suf}_{\text{sa}_i},\text{suf}_{\text{sa}_j})|=\min\limits_{k=i+1}^j\text{height}_i\),其中 \(i<j\)

\(\bf{Proof}\)

首先容易證明它們存在這麼多長度的 \(\text{LCP}\),嚴謹證明的話考慮歸納法和 \(\min\) 的性質以及等號的傳遞性。

然後我們可以結合 \(\bf{Lemma\space 1}\) 透過反證法證明它們不存在更長的 \(\text{LCP}\)

結合 \(\bf{LCP\space theory}\),根據 \(\min\) 的單調性,我們可以得到:

與某個字尾 \(\text{suf}_i\)\(\text{LCP}\) 長度大於等於某個定值 \(k\) 的字尾的 \(\text{rk}\) 構成一個連續的區間。

換句話說,若排名最小的與 \(\text{suf}_i\)\(\text{LCP}\) 長度 \(\ge k\) 的排名為 \(L\),最大的排名為 \(R\),則排名在 \([L,R]\) 內的字尾都滿足其與 \(\text{suf}_i\)\(\text{LCP}\) 長度大於等於該定值 \(k\)

在求解這樣的區間時,我們可以建立 \(\text{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;

本質不同子串個數

習題

SP10419 POLISH - Polish Language

  • 給出字串 \(s\),求有多少個序列 \(a\) 滿足:

    • \(\forall i\in[1,|a|],1\le a_i\le |s|\)

    • \(\forall i\in(1,|a|],suf_{a_i}>suf_{a_{i-1}}\)

    • \(\forall i\in (1,|a|],|suf_{a_i}|>|suf_{a_{i-1}}|\)

    數量對 \(10^9+7\) 取模。其中字串的比較均基於字典序大小。

  • 多組資料,\(|s|\le 10^5\)

看到字尾之間的字典序比較,先想到字尾陣列。處理完之後,考慮一個一個解決限制條件。

  • \(\forall i\in[1,|a|],1\le a_i\le |s|\),不用轉化。

  • \(\forall i\in(1,|a|],suf_{a_i}>suf_{a_{i-1}}\),等價於 \(\forall i\in(1,|a|],rk_{a_i}>rk_{a_{i-1}}\)

  • \(\forall i\in (1,|a|],|suf_{a_i}|>|suf_{a_{i-1}}|\),等價於 \(\forall i\in (1,|a|],a_i>a_{i-1}\)

典型二維偏序,考慮 dp。設 \(f_i\) 表示 \(a_{|a|}=i\) 的符合條件的序列數量,顯然有 \(f_i=\sum\limits_{i<j\le n\,\land\, rk_i>rk_j}f_j+1\)。簡單來說就是考慮第 \(i\) 位接上怎樣的序列,\(+1\) 表示單獨成為一個序列。

倒序列舉維護 \(i<j\),用樹狀陣列維護 \(rk_i>rk_j\) 即可。

設資料組數為 \(T\),時間複雜度為 \(\mathcal{O}(T|s|\log |s|)\),空間複雜度為 \(\mathcal{O}(|s|)\)

提交記錄 程式碼

P5353 樹上字尾排序

  • 給出一棵 \(n\) 個節點,以 \(1\) 為根的樹,點 \(i\) 上有字元 \(c_i\)。定義點 \(i\) 的字串 \(s_i\) 為從點 \(i\) 走到點 \(1\) 路徑上所有點上的字元拼接而成的字串。

  • 形式化的,若點 \(i\) 到點 \(1\) 的路徑為 \(p_1,p_2,\dots ,p_k\,(p_1=i,p_k=1)\),則 \(s_i=\overline{c_{p_1}\dots c_{p_k}}\)

  • 你要對 \(1\sim n\) 這些點按照 \(s_i\) 的字典序進行排序,若字典序相同,則父親排名小的點排名小。若仍相同,編號小的點排名小。

  • \(n\le 5\times 10^5\)

看到對字串排序想到字尾排序。我們可以類比字尾排序,利用倍增的思想,每次將兩條長度為 \(2^i\) 的連續的、向上的鏈拼起來成為長度為 \(2^{i+1}\) 的鏈。所以要處理樹上的倍增陣列,記 \(fa_{i,u}\) 為點 \(u\) 向上走 \(2^i\) 條邊到達的祖先。這部分的程式碼:

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; }
}

關鍵是如何去重。這個程式碼求出來的 \(rk_i\) 表示將所有字串去重後\(s_i\) 的排名(排名定義為比它小的數的個數 \(+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\) 排序。然後從頭開始掃,掃出一段 \(\boldsymbol{rk}\) 相同的區間,然後再對這個區間內的點以父親排名作為第一關鍵字、編號作為第二關鍵字排序。那麼這些點的排名就是遞增的,且第一個點的排名就是自己的 \(\boldsymbol {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;
    }
}

去完重之後,如果用字尾排序中的名稱來說,別忘記你輸出的是 \(\boldsymbol{sa}\) 陣列而不是 \(\boldsymbol{rk}\) 陣列,你要再做一遍 \(sa_{rk_i}=i\)

現在來算時間複雜度,倍增處理 \(fa\) 陣列以及倍增求 \(rk\) 都是 \(\mathcal{O}(n\log n)\) 的,至於去重,其實就是把“將長度為 \(n\) 的陣列分成若干段,對每段分別進行排序並從左到右掃描”這個操作做了兩次,根據乘法分配律和加法結合律,可以算出這部分的時間複雜度為 \(\mathcal{O}(n\log n)\)

綜上,本做法時間、空間複雜度均為 \(\mathcal{O}(n\log n)\)

評測記錄 程式碼

AT_s8pc_2_e 部分文字列

  • 給出一個字串 \(S\),求 \(S\) 的所有本質不同子串的長度之和。

  • \(|S| \le 10^5\)

看到本質不同子串,先做字尾排序把 \(\text{height}\) 陣列求出來,然後排名為 \(i\) 的字尾能帶來的本質不同子串個數為 \(|S|-sa_i+1-\text{height}_i\),這些子串的長度為 \(\text{height}_i+1\sim |S|-sa_i+1\),等差數列求和計算即可。

時間複雜度為 \(\mathcal{O}(|S|\log |S|)\),空間複雜度為 \(\mathcal{O}(|S|)\)。由於是 AT 遠古題,所以行末要輸出換行。

提交記錄(含程式碼)

CF149E Martian Strings

  • 給出字串 \(s\),以及 \(m\) 個詢問串 \(p_i\),每次詢問是否能找到兩個不交的區間 \([a,b],[c,d]\) 使得 \(\overline{s_as_{a+1}\dots s_bs_cs_{c+1}\dots s_d} = p_i\)

  • \(m\le 10^2\)\(|p_i|\le10^3\)\(|s|\le 10^5\)

考慮將所有串拼成一個大串 \(S\),做一遍字尾排序,求出 \(sa,rk,\text{height}\) 陣列。

對於每一組詢問,考慮列舉 \(j\) 表示找到 \([a,b],[c,d]\) 使得 \(\overline{s_as_{a+1}\dots s_b}=\overline{p_{i_1}p_{i_2}\dots p_{i_{j}}},\overline{s_bs_{c+1}\dots s_d}=\overline{p_{i_{j+1}}p_{i_{j+2}}\dots p_{i_{|p_i|}}}\)。可以透過二分 + ST 表求出滿足條件的字尾的排名區間。

我們考慮選擇最左邊的 \([a,b]\) 以及最右邊的 \([c,d]\),這樣一定不劣。因此考慮以排名為下標建立 ST 表維護某個排名區間內,\(\boldsymbol{\le n}\) 最左、左右字尾的位置。然後判斷這兩個位置擷取出來的子串是否相交即可。

時間、空間複雜度均為 \(\mathcal{O}(|S|\log |S|)\)

提交記錄(含程式碼)

CF316G3 Good Substrings

CF316G1 Good SubstringsCF316G2 Good Substrings

  • 給出字串 \(s\),以及 \(n\) 個限制,每個限制形如 \(t_i,l_i,r_i\),一個字串滿足該條限制,當且僅當它在字串 \(t_i\) 中的出現次數在 \([l_i,r_i]\) 之間。

  • \(s\) 有多少個本質不同的子串滿足所有限制。

  • \(|s|,\max\limits_{i=1}^n |t_i|\le 5\times 10^4\)\(\boldsymbol{n\le 10}\)

\(s[l,r]\) 為字串 \(s\)\(s_l\) 開頭,以 \(s_r\) 結尾的子串,形式化的,\(s[l,r]=\overline{s_l\dots s_r}\)

看到「本質不同子串」,想到字尾陣列。先將所有字串用奇怪字元拼起來(記大串為 \(S\))做字尾排序求出 \(sa,rk,\text{height}\) 陣列並對 \(\text{height}\) 陣列維護 ST 表。

對於一個字串 \(s\),我們知道排名為 \(i\) 的字尾帶來的本質不同子串是 \(s[sa_i,\text{height}_i+1]\sim s[sa_i,|s|]\) 這些,然後你會發現這些子串的出現次數隨著長度遞增不升

因為若有一個長串出現若干次,我的短串也被這個長串包含,至少出現了這麼多次。

考慮二分出最短的滿足所有限制上界的字串長度 \(ans_l\) 以及最長的滿足所有限制下界的字串長度 \(ans_r\),那麼這個字尾就可以帶來 \(ans_r-ans_l+1\) 個滿足所有限制的本質不同子串。

維護一個字首和陣列 \(sum_{j,i}\),表示排名為 \(1\sim i\) 的字尾中,有多少個字尾是 \(t_j\) 帶來的。我們又知道對於一個子串,出現它的字尾的排名是一段連續的區間。套路用二分和 ST 表求 \(\text{LCP}\) 得到這個區間 \([L,R]\)。問題變成了判斷區間內某個數 \(i\) 出現次數是否(不)超過給定的值。用 \(sum_{i,R}-sum_{i,L-1}\) 表示出其出現次數,由於 \(n\) 很小,列舉判斷即可。這樣這題基本就做完了。

還有一個地方需要注意,我們要求的是原串 \(\boldsymbol{s}\) 某個字尾帶來的本質不同子串個數,但是現在把所有字元接在一起,\(\text{height}\) 陣列並是大串 \(S\) 中兩個排名相鄰的字尾的 \(\text{LCP}\)。所以我們要按排名列舉 \(S\) 的字尾,如果它是 \(s\) 的字尾就統計答案。然後你對 \(S\) 字尾排序,原串 \(\boldsymbol{s}\) 中的所有字尾也是有序的,不然它們在 \(S\) 中也是無序的,相當於你就沒排序。 因此直接記錄上一個為原串中的某個字尾的排名 \(la\),那麼我要的 \(\text{height}\),原串 \(s\) 中排名相鄰兩個字尾的 \(\text{LCP}\),就是 \(S\)\(S[la,|S|]\) 和當前字尾的 \(\text{LCP}\)。後面接的東西不影響,因為你接了奇怪字元,在那一位一定失配,不會影響 \(\text{LCP}\) 長度。

時間複雜度為 \(\mathcal{O}(|S|\log |s| (n+\log|S|))\),空間複雜度為 \(\mathcal{O}(|S|\log |S|+n|S|)\)

提交記錄(含程式碼)

UVA1502 GRE Words

雙倍經驗

  • 給出 \(n\) 個字串 \(s_1\sim s_n\),第 \(i\) 個字串有權值 \(w_i\)。選出一個子序列 \(a_1\sim a_k\),滿足 \(\forall\,i\in[1,k),a_i<a_{i+1}\)\(s_{a_i}\)\(s_{a_{i+1}}\) 的子串。求 \(\sum\limits_{i=1}^kw_{a_i}\) 的最大值。可以為空,此時權值為 \(\boldsymbol 0\)

  • \(T\) 組資料,\(T\le 50\)。對於單組資料,滿足 \(n\le 2\times 10^4\)\(\sum\limits_{i=1}^n|s_i|\le 3\times 10^5\)

\(N=\sum\limits_{i=1}^n|s_i|\)

考慮 dp。設 \(f_i\) 表示以 \(i\) 開頭的最長子序列,則 \(f_i=\max\limits_{j\in(i,n]\land \,s_i\text{ is a substring of }s_j} f_j+w_i\)

將所有串拼成一個大串 \(S\) 進行字尾排序。則 \(j\) 滿足條件,當且僅當 \(s_j\) 中有一個字尾與 \(s_i\) 的最長公共字首長度不少於 \(|s_j|\)

這樣的字尾排名形如一個區間 \([L,R]\)。考慮線段樹維護。區間 \([l,r]\) 的資訊為:當前包含排名為 \([l,r]\) 中的字尾的字串中,\(f_j\) 的最大值。

那麼轉移就是區間最大值。可能會重複貢獻一些 \(f_j\),但由於是取 \(\max\) 所以沒關係。

轉移完後線上段樹上對 \(s_i\) 的所有字尾排名對應的位置進行單點修改。

時空複雜度均為 \(\mathcal{O}(N\log N)\)。可以把 \(\text{height}\) 陣列用線段樹維護然後線段樹上二分得到排名區間,這樣空間是線性。

AC Link / AC Code

SP8093 JZPGYZ - Sevenk Love Oimaster

  • 給出 \(n\) 個模板串 \(s_1\sim s_n\)\(m\) 個查詢串 \(t_1\sim t_m\),每次詢問一個查詢串是多少個模板串的子串。
  • \(n\le 10^4\)\(m\le 6\times 10^4\)\(\sum\limits_{i=1}^n|s_i|\le 10^5\)\(\sum\limits_{i=1}^m|t_i|\le 3.6\times 10^5\)

先將所有字串用奇怪字元拼成一個大串 \(S\) 然後做一遍字尾排序,求出 \(sa,rk,\text{height}\) 等陣列。

對於每一個詢問串,以它為字首的字尾的排名一定是一個區間,考慮二分出這個區間 \([L,R]\)

我們記排名為 \(i\) 的字尾的顏色 \(col_i\) 為它是哪個模板串的字尾。則要求區間 \([L,R]\) 內有多少種不同的顏色。

主席樹維護每一個位置的前驅,數有多少個前驅在區間外即可。但是詢問串之間也可能存在包含關係,所以要數的顏色必須是 \([1,n]\) 內的顏色。

因此主席樹的一個節點的定義為:當前版本中,有多少個位置滿足這個位置的顏色在 \([1,n]\) 內,且該位置的前驅在該區間內。插入的時候,若當前位置的顏色在 \([1,n]\) 內則插入,否則繼承上一個版本。

時間、空間複雜度均為 \(\mathcal{O}(|S|\log |S|)\)

提交記錄 程式碼

CF232D Fence

  • 給出序列 \(a_1\sim a_n\),有 \(q\) 次詢問,每次詢問給出 \([l,r]\),求有多少個區間 \([x,y]\) 滿足 \(y-x=r-l\)\([x,y] \bigcap \,[l,r]=\varnothing\)\(\forall \,i\in[0,r-l],a_{l+i}+a_{x+i}=a_{l}+a_x\)

  • \(n,q\le 10^5\)

tags:

  • \(\text{binary search}\)

  • \(\text{data structures}\)

  • \(\text{string suffix structures}\)

  • \(\color{red}*2900\)

原題就是讓我們求出有多少個滿足條件的左端點。

我們記原陣列的差分陣列 \(d_i=a_i-a_{i-1}\,(i\in(1,n])\)認為 \(\boldsymbol{d_1}\) 沒有意義,即不存在,其值不與任何一個 \(\boldsymbol{d_i}\) 相同。則滿足第二個條件的充要條件是 \(\forall \,i\in(0,r-l],d_{l+i}=-d_{x+i}\)

  • 證明:

根據已知條件可以推出:

  • \(a_{l+i}+a_{x+i}=a_l+a_x\Leftrightarrow a_{l+i}-a_l=a_x-a_{x+i}\)

  • \(a_{l+i-1}+a_{x+i-1}=a_l+a_x\Leftrightarrow a_{l+i-1}-a_l=a_{x}-a_{x+i-1}\)

兩式相減即可得到 \(a_{l+i}-a_{l+i-1}=a_{x+i-1}-a_{x+i}\),即 \(d_{l+i}=-d_{x+i}\)

我們若倍長 \(d\),且令 \(d_i=-d_{i-n}\,(i\in(n,2n])\),則上述條件等價於 \(d_{l+i}=d_{x+n+i}\)。我們要統計有多少個 \(x\),就可以去統計有多少個 \(x+n\)同理可以去統計有多少個 \(\boldsymbol{x+n+1}\)

為什麼要做這一步轉化呢?我們發現,對於 \(d[l+1,2n]\)\(d[x+n+1,2n]\) 這兩個字尾,它們存在 \(\boldsymbol{d[l+1,r]}\)\(\boldsymbol{d[x+n+1,x+n+r-l]}\) 這一段長度為 \(\boldsymbol{r-l}\) 的公共字首。考慮對差分陣列進行字尾排序,則可以二分 + ST 表求出與字尾 \(d[l+1,2n]\)\(\text{LCP}\) 長度不小於 \(r-l\) 的排名區間。然後根據不交、長度相等的限制以及差分陣列的定義,可以得到 \(x+n+1\) 的範圍是 \([n+2,n+2l-r]\bigcup\, [n+r+2,2n+l-r+1]\)

這就是個二維數點,線上主席樹或離線掃描線 + 樹狀陣列維護一下就行了。

  • 注意

使用上述統計方法的前提是存在差分陣列。當 \(l=r\) 時,區間內不存在差分陣列,不能這樣統計。

不過容易得知此時答案即為 \(n-1\),特判一下即可。

程式碼裡用的是主席樹,時間、空間複雜度均為 \(\mathcal{O}(n\log n)\)

提交記錄(\(\color{limegreen} \bf{Accepted}\) \(\bold{483\,\text{ms}\,/\,73952\,\text{KB}}\),含程式碼)

P4143 採集礦石

  • 給出字串 \(s\),以及陣列 \(a_1\sim a_{|s|}\)

  • 定義一個子串的排名為:字典序比它大的本質不同的子串個數 \(+1\)

  • 定義一個子串 \(s[l,r]\) 的權值為 \(\sum\limits_{i=l}^ra_i\)

  • 求有多少個子串的排名等於權值。

  • \(|s|\le 10^5,0\le a_i\le 1000\)

首先對 \(s\) 進行字尾排序,然後考慮每一個左端點 \(l\),不難發現隨著右端點 \(r\) 的增大,子串的排名單調遞減,權值單調不降。

所以可以二分出滿足條件的最小 / 大右端點。

考慮如何求出一個子串 \(t\) 的排名。可以用本質不同子串數減去比它小的。

前半部分運用經典結論即為 \(\sum\limits_{i=1}^n (|s|-sa_i+1-\text{height}_i)\),我們考慮如何求比它小的本質不同子串數。

可以二分出以這個子串為字首的字尾排名區間 \([L,R]\)答案即為排名為 \(\boldsymbol{[1,L)}\) 的字尾帶來的本質不同子串個數。

  • 充分性:

    若一個子串 \(str\) 在排名為 \([1,L)\) 的字尾中作為字首出現,那麼這個字尾 \(s[i,|s|]\)\(s[l,|s|]\)\(\text{LCP}\) 長度一定小於 \(\boldsymbol{|t|}\)。即兩個字尾可以在第 \(|t|\) 個位置之前可以找到不相同的位置。而由於 \(s[i,|s|]\) 這個字尾排名更小,在這個位置一定 \(s[i,|s|]\) 這個字尾小於 \(s[l,|s|]\)

    考慮 \(str\) 是否跨過這個位置,若不是,則在前 \(|str|\) 位兩串相同,第 \(|str|+1\)\(str\) 為空,字典序極小。

    若跨過,則 \(str\) 在這個位置小於 \(t\)

  • 必要性:

    考慮這兩個子串第一次不同是在某個位置,這個位置一定在兩個字尾中。

正確性證好了。這個東西也是考慮每個字尾帶來的本質不同子串。即可以這麼求:

\[\sum\limits_{i=1}^{L-1}(|s|-sa_i+1-\text{height}_i) \]

於是做完了。時間複雜度為 \(\mathcal{O}(|s|\log^2|s|)\),空間複雜度為 \(\mathcal{O}(|s|)\)

提交記錄 程式碼

ABC280Ex Substring Sort

  • 給出 \(n\) 個字串 \(s_1\sim s_n\)。記 \(F(i,l,r)\) 表示 \(s_i[l,r]\) 這個子串。將所有存在的 \(F(i,l,r)\) 非降排序。\(q\) 次詢問,求出排在第 \(k\) 位的 \(F(i,l,r)\)。如有多解輸出任意一個。

  • \(n,\sum\limits_{i=1}^n|s_i|\le 10^5\)\(q\le 2\times 10^5\)\(k\le \sum\limits_{i=1}^n \dfrac{|s_i|(|s_i|+1)}{2}\)

  • \(\text{2 s / 1 GB}\)

先將所有字串拼成大串 \(S\)。其中 \(\mathcal{O}(|S|)=\mathcal{O}\left(n+\sum\limits_{i=1}^n|s_i|\right)\)。對 \(S\) 進行字尾排序。

稱一個串在排名為 \(i\) 的字尾中第一次作為字首出現,當且僅當:不存在 \(j<i\),使得這個串在排名為 \(j\) 的字尾中作為字首出現。

對於 \(S\) 中排名為 \(i\) 的字尾,預處理 \(sum_i\) 表示有多少種本質不同的原串中的子串在這個字尾中第一次作為字首出現;預處理 \(tot_i\) 表示有多少個原串中的子串在這個字尾中作為字首出現。然後分別對這兩個陣列求字首和,記為 \(ssum_i\)\(stot_i\)(程式碼中用的是同一個陣列)。

考慮求出答案是第幾大的本質不同子串。容易發現排在第 \(k\) 位等價於,它是最大的一種子串,使得小於它的子串數量小於 \(k\)。這個有單調性,可以二分利用 \(ssum_i\) 判斷求。

記二分的子串為 \(str\)。考慮對於每個字尾統計小於 \(str\) 的子串數量。先找到它第一次出現在哪個排名的字尾中,設這個排名為 \(p\)。分為 \([1,p)\)\([p,|S|]\) 兩部分。

對於第一部分,這些字尾的所有字首都小於 \(str\)。因為若某個字首 \(pre\)\(str\) 字首,其必然是真字首。不然 \(p\) 就不是 \(str\) 第一次出現的位置。那麼 \(pre\) 字典序小於 \(str\)。否則,它們跨過了 \(\text{LCP}\),又因為 \([1,p)\) 的字尾排名更小,所以 \(pre\)\(\text{LCP}\) 後的那個位置上一定小於 \(str\),因此 \(pre\) 字典序小於 \(str\)

對於第二部分,答案為 \(\sum\limits_{i=p}^{|S|}\min\{|\text{LCP}(S[i,|S|],S[p,|S|])|,|str|-1\}\)。同樣考慮為 \(pre\)\(str\) 真字首以及跨過 \(\text{LCP}\) 的情況。

第一部分答案即為 \(stot_{p-1}\)。第二部分考慮將 \(\min\) 拆開。由於 \(|\text{LCP}(S[i,|S|],S[p,|S|])|=\min\limits_{j=p+1}^i \text{height}_j\),這個式子的值是單調不升的。可以二分出一個分界點 \(pos\) 使得當 \(i\in[p,pos]\)\(\min\{|\text{LCP}(S[i,|S|],S[p,|S|])|,|str|-1\}=|str|-1\);當 \(i\in(pos,|S|)\)\(\min\{|\text{LCP}(S[i,|S|],S[p,|S|])|,|str|-1\}=\min\limits_{j=p+1}^i \text{height}_j\)。顯然有 \(pos\ge p\),因為 \(p\) 這個字尾以 \(str\) 為字首。

那麼第一種情況的貢獻就是 \((pos-p+1)(|str|-1)\);至於後一種情況,根據 \(\min\) 的結合律,可以將其改寫成 \(\sum\limits_{i=pos+1}^{|S|}\min\limits_{j=pos+1}^{i}\text{height}_j\)。記為 \(f_x=\sum\limits_{i=x}^{|S|}\min\limits_{j=x}^{i}\text{height}_j\)。由於沒有修改,考慮預處理。

可以用單調棧,對於每個 \(x\) 求出最小的 \(y\),滿足 \(y>x\)\(\text{height}_y<\text{height}_x\)。記這個 \(y\)\(nxt_x\)。同樣將 \(f_x\) 分為 \(i\in[x,nxt_x)\)\(i\in[nxt_x,|S|]\) 兩部分。對於第一部分,根據 \(nxt_x\) 的定義可知這些 \([x,i]\) 的最小值均為 \(\text{height}_x\),貢獻為 \((nxt_x-x)\text{height}_x\);對於第二部分,再運用 \(\min\) 的結合律寫成 \(\sum\limits_{i=nxt_x}^{|S|}\min\limits_{j=nxt_x}^i \text{height}_i\),你會發現它就是 \(f_{nxt_x}\)。於是我們得到了 \(f_x\) 的求法:

\[f_x=(nxt_x-x)\text{height}_x+f_{nxt_x} \]

\(f_x\) 求出來之後,就可以將 \(\sum\limits_{i=pos+1}^{|S|}\min\limits_{j=pos+1}^{i}\text{height}_j\)\(f_{pos+1}\) 帶入求解了。

這樣一來我們就找到有多少個子串小於給定的串 \(str\)。結合第一次二分就可以得到答案是哪一種本質不同的子串。為了方便,這一步在返回小於 \(str\) 的串個數時,同時返回 \(str\) 第一次出現字尾屬於原串中的第幾個串的哪個位置,便於得到 \(F(i,l,r)\)。這些都是容易求的,可以考慮記錄 \(S\) 中每個排名字尾屬於哪個原串、原串在 \(S\) 中出現的位置。

那麼這題就做完了,時間複雜度為 \(\mathcal{O}((q+|S|)\log^2|S|)\),空間複雜度為 \(\mathcal{O}(|S|\log |S|)\)

AC Code

CF1037H Security

  • 給出一個字串 \(s\),有 \(q\) 次詢問,第 \(i\) 次詢問給出 \(l_i,r_i,t_i\),求一個字典序最小的字串 \(str\),使得它是 \(s[l_i,r_i]\) 的子串,且 \(str>t_i\)

  • \(|s|\le 10^5\)\(\sum\limits_{i=1}^q|t_i|,q\le 2\times 10^5\)

\(|\text{LCP}(str,t_i)|=l\)\(str>t_i\) 當且僅當 \(str_{l+1}>t_{i_{l+1}}\),為了使 \(str\) 儘量小,我們希望 \(\boldsymbol l\) 儘量大

證明很簡單,假設有一個串 \(T\) 滿足 \(\text{LCP}(T,t_i)=L<l\),則 \(\bold {LCP}\boldsymbol{(str,T)=L}\),且 \(\boldsymbol{T_{L+1}>str_{L+1}}\),因此 \(\boldsymbol{T>str}\),所以 \(\bold{|LCP|}\) 大的字串字典序更小

先將 \(s\) 和所有 \(t_i\) 中間用奇怪字元拼接成大串 \(S\),這樣不改變任意兩個字尾的 \(\text{LCP}\)。然後做一遍字尾排序,求出 \(sa,rk,\text{height}\) 陣列以及維護 ST 表輔助求字尾之間的 \(|\text{LCP}|\)

對於每一組詢問,考慮列舉 \(l\,(0\le l \le r_i-l_i)\) 以及下一位拼上什麼字元 \(c\),滿足 \(c>t_{i_{l+1}}\)(一定是僅拼上一個字元,因為空字元字典序最小)。可以先二分 + ST 表求出與 \(t_i\)\(S\) 中的字尾的 \(|\text{LCP}|\) 至少為 \(l\) 的字尾排名區間 \([L,R]\)。那麼在 \(\text{LCP}\) 末尾拼上一個字元 \(c\) 後(記這個字串為 \(p\)),以 \(p\) 為字首的字尾的排名仍然是一個連續的區間。由於字尾排序過,因此 \([L,R]\) 排名區間內的字尾的第 \(l+1\) 位的字元一定單調不減

考慮繼續二分出這個連續區間 \([ql,qr]\),可以二分找到最小的排名 \(mn\) 使得 \(S_{sa_{mn}+l}\ge c\) 以及最大的排名 \(mx\) 使得 \(S_{sa_{mx}+l}\le c\)。則 \(ql=mn,qr=mx\)。若不存在 \([ql,qr]\) 這個區間,則跳過。

我們要求 \(s[l_i,r_i]\) 中是否存在一個子串 \(str\)\(p\) 為字首,相當於求 \(suf_{l_i}\sim suf_{r_i-l}\) 這些字尾中,是否存在一個字尾 \(suf_j\) 使得 \(ql\le j\le qr\)。至此原問題轉化成了二維數點,用主席樹維護即可。

可以從小到大列舉 \(c\),對於列舉的 \(l\),我們找到一個最小的字元 \(c\) 滿足條件之後,即可停止當前 \(l\) 的列舉。因為要求字典序最小。

對於多種 \(l\) 的答案,上面已經說過,選最大的那一種。

預設 \(|S|,q\) 同階,時間複雜度為 \(\mathcal{O}(|\Sigma|\cdot (\sum_{i=1}^q|t_i|)\log |S|+|S|\log^2 |S|)\),空間複雜度為 \(\mathcal{O}(|S|\log |S|)\)。由於不是瓶頸,字尾排序部分未使用基數排序最佳化。

提交記錄(含程式碼)

P4770 [NOI2018] 你的名字

  • 給出字串 \(s\) 以及 \(q\) 個詢問,第 \(i\) 個詢問給出一個串 \(t_i\) 以及一個區間 \([l_i,r_i]\)

  • \(s[l,r]\) 為字串 \(s\)\(l\) 位到第 \(r\) 位字元順次拼接而成的子串。形式化地,\(s[l,r]=\overline{s_ls_{l+1}\dots s_r}\)

  • 對於每個詢問,求 \(t_i\) 有多少種本質不同的子串沒有在 \(s[l_i,r_i]\) 中出現。

  • \(|s|\le 5\times 10^5,q\le 10^5,\sum\limits_{i=1}^q|t_i|\le 10^6\)

  • \(\text{5.00 s / 768.00 MB}\)

神仙字串題。

首先把所有字串用特殊字元接起來,得到一個大串 \(S\)。對 \(S\) 進行字尾排序。這樣不改變任意兩個字尾的 \(\text{LCP}\)

對於每一組詢問,考慮容斥,即用 \(t_i\) 中的本質不同子串個數減去在 \(s[l_i,r_i]\) 中出現過的。

前半部分是平凡的,即按排名考慮每一個字尾帶來的本質不同子串個數,根據經典結論就是這個字尾的字首數減去它的 \(\text{height}\)

至於後半部分,同樣這樣考慮每個字尾帶來的本質不同子串中有多少個在 \(s[l_i,r_i]\) 中出現。我們發現若一個字尾 \(\boldsymbol{t_i[j,e]}\)\(\boldsymbol{s[l_i,r_i]}\) 中出現,則 \(\boldsymbol{t_i[j,e-1]}\) 也在 \(\boldsymbol{s[l_i,r_i]}\) 中出現。所以可以考慮二分這個最大的結束位置 \(e\)。判斷 \(t_i[j,e]\) 是否在 \(s[l_i,r_i]\) 中出現就是判斷是否存在一個位置 \(k\) 使得 \(k\in[l_i,r_i-e+j]\)\(|\text{LCP}(S[k,|S|],t_i[j,e])|\ge e-j+1\)

二分出排名區間,主席樹二維數點檢查即可。得到這個值後,\(t_i[j,j+\text{height}_j]\sim t_i[j,e]\) 這些本質不同子串在 \(s[l_i,r_i]\) 中出現,直接減去個數即可。

但是這樣回答單組詢問的時間複雜度為 \(\mathcal{O}(|t_i| \log |S|\log |t_i|)\),無法接受。

思考一下二分的目的,我們想要對於 \(t_i\) 的每個字尾,得到一個最大的長度 \(L_j\),使得 \(t_i[j,j+L_j-1]\)\(s[l_i,r_i]\) 中出現。

我們發現一個關鍵性質,那就是 \(\boldsymbol{L_j\ge L_{j-1}-1}\)。因為這兩個字尾只差了開頭的這一位。

我們可以類似於 \(\text{height}\) 陣列那樣,用一個指標 \(k\) 表示當前 \(t_i[j,j+k-1]\)\(s[l_i,r_i]\) 中出現,檢查是否可行時仍然二分排名區間 + 主席樹。若可行則 \(k\) 右移。

由於最多遞減 \(\mathcal{O}(|t_i|)\),因此 \(k\) 最多移動 \(\mathcal{O}(|t_i|)\) 次,這樣單組詢問的時間複雜度就是 \(\mathcal{O}(|t_i|\log |S|)\)

綜上,我們得到了一個時間、空間複雜度均為 \(\mathcal{O}(|S|\log |S|)\) 的做法。

提交記錄(\(\color{limegreen}\bf Accepted\space100\)\(\bf{4.62\, s / 606.29\, MB}\) 程式碼

P4022 [CTSC2012] 熟悉的文章

  • 給出 \(n\) 個文字串 \(s_1\sim s_n\)\(m\) 個詢問串 \(t_1\sim t_m\)

  • 稱一個字串 \(\text{str}\) 是“\(L\) 熟悉的”,當且僅當 \(|\text{str}|\ge L\),且 \(\text{str}\) 是文字串的子串,此時記 \(P(\text{str},L)=1\)。否則 \(P(\text{str},L)=0\)

  • 對於每個詢問串 \(t_i\),求出最大的整數 \(L_i\),使得將其劃分為若干個子串後,所有“\(L_i\) 熟悉的”子串長度之和不小於 \(\dfrac{9|t_i|}{10}\)

  • 形式化地,記一種劃分 \(t_i\) 的方式為 \(S=\{[l_1,r_1],\dots,[l_{|S|},r_{|S|}]\}\),滿足 \(\forall \,j\in[1,|S|)\cup \mathbb{Z},r_j+1=l_{j+1}-1\),且 \(l_1=1,r_{|S|}=|t_i|\)。記所有劃分方案構成的集合為 \(U\)。你要找到最大的整數 \(L_i\),滿足 \(\exists\,T\in U,\sum\limits_{j=1}^{|T|}[P(t_i[l_j,r_j],L_i)\cdot (r_j-l_j+1)]\ge \dfrac{9|t_i|}{10}\)

  • \(N=\sum\limits_{i=1}^n|s_i|,M=\sum\limits_{i=1}^m|t_i|\),滿足 \(N,M\le 1.1\times 10^6\)

  • \(\text{1 s / 250 MB}\)

預設 \(\mathcal{O}(n)=\mathcal{O}(m)=\mathcal{O}(N)=\mathcal{O}(M)\)。字符集大小 \(\mathcal{O}(|\Sigma|)=\mathcal{O}(1)\)

對於每一個詢問,容易發現答案有單調性,因為若 \(x\) 是合法的,則在 \(x-1\) 時仍然按照這種方式劃分,式子的值是不減的。所以二分答案。

對於一個已知的 \(L_i\),我們可以 dp 求出上面式子的最大值然後判斷是否合法。

\(f_j\) 表示將 \(t_i[1,j]\) 分成若干段,上面式子的最大值。記 \(\text{mx}_j\) 表示以 \(t_i[1,j]\) 為字首的最長字尾 \(\text{suf}\) 的長度,使得 \(\text{suf}\) 在文字串中出現過。那麼考慮列舉上一段的末尾(為 \(0\) 表示這一段是開頭),有:

\[f_j=\max\left\{\max\limits_{k\in[0,j-\text{mx}_j)\cup(j-L_i,j)\cup\mathbb{Z}}f_k,\max\limits_{k\in[j-\text{mx}_j,j-L_i]\cup\mathbb{Z}}f_k+j-k\right\} \]

就是去考慮這一段能否成為“\(L_i\) 熟悉的”。容易發現 \(f_j\ge f_{j-1}\),因為我將 \(j\) 這個位置單獨分一段,答案是不減的。所以前一部分的轉移可以用 \(f_{j-1}\) 代替。

至於後面那部分,先將 \(\text{mx}_j\) 求出來。

將所有串用分隔符拼在一起形成大串 \(A\) 進行字尾排序。我們記 \(\text{MX}_k\) 表示 \(A[1,k]\) 這個字首最長的字尾,使得它在文字串中出現。可以發現 \(\text{mx}_j\) 就等於 \(t_{i,j}\)\(A\) 中對應位置的 \(\text{MX}\) 值,因為 \(A[1,k]\) 存在。因為 \(A[1,k]\) 中存在 \(\text{mx}_j\) 長度的字尾滿足條件,且不存在更長的字尾滿足條件。

進一步發現,若 \(A[1,k]\) 存在長度為 \(a\) 的字尾滿足條件,則長度為 \(a-1\) 的字尾也滿足條件。因為後者是前者的子串。所以初步想法是二分 \(\text{MX}_k\),然後判斷是否合法。

考慮如何判斷一個長度 \(\text{len}\) 是否合法。若合法當且僅當 \(A\) 中存在一個來自於文字串的字尾,使得它與 \(A[k-\text{len}+1,|A|]\) 這個字尾的最長公共字首長度不少於 \(\text{len}\)

滿足後面那個條件的字尾排名形如一段區間 \([\text{ql},\text{qr}]\),可以二分 + \(\text{height}\) 陣列 rmq 得到。記 \(B_i=1/0\) 表示排名為 \(i\) 的字尾是否來自文字串。那麼判斷 \(B\) 陣列的區間和是否為正即可,字首和維護。

但是這樣對於每個字尾做一遍時間複雜度為 \(\mathcal{O}\left(n\log^2 n\right)\),無法接受。

注意到一個關鍵性質,\(\text{MX}_k\ge \text{MX}_{k+1}-1\)。因為 \(A[1,k+1]\) 的那個字尾長度為 \(\text{MX}_{k+1}-1\) 的字首是 \(A[1,k]\) 的字尾。

那麼這樣用個指標掃一下即可,指標最多遞增 \(\mathcal{O}(n)\) 次,所以這樣時間複雜度是 \(\mathcal{O}(n\log n)\)

這樣我們就求出了 \(\text{mx}_j\)。根據上面那個性質,可以發現第二類轉移區間左端點 \(j-\text{mx}_j\) 是不降的。同時右端點 \(j-L\) 也是不降的。那麼對於這樣的區間求 \(f_k-k\) 最大值,單調佇列維護即可。

時空複雜度均為 \(\mathcal{O}(n\log n)\)。可以把二分 + ST 表換成線段樹上二分做到線性空間,但是常數太大無法接受。一開始一直在為空間糾結,但事實上並不卡空間。

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

AC Link

AC Code

CF1483F Exam

  • 給出 \(n\) 個字串 \(s_1\sim s_n\),求有多少對 \((i,j)\),滿足:
    • \(1\le i,j\le n\)
    • \(s_j\)\(s_i\)子串。
    • 不存在 \(k\)\(i,j,k\) 兩兩不同)使得 \(s_j\)\(s_k\) 的真子串,且 \(s_k\)\(s_i\) 的真子串。
  • \(n,\sum\limits_{i=1}^n|s_i|\le 10^6\)。若 \(i\ne j\),則 \(s_i\ne s_j\)
  • \(\text{2 s / 512 MB}\)

先將所有串拼成一個大串 \(S\) 進行字尾排序。考慮列舉 \(i\),求出哪些 \(j\) 會產生貢獻。

考慮對於 \(s_i\) 的每個字尾 \(s_i[x,|s_i|]\),在 \(s_1\sim s_n\) 中,找到一個最長的字串 \(s_y\),滿足它是 \(s_i[x,|s_i|]\)字首,記為 \(l_{i,x}=|s_y|\)。若找不到這樣的 \(s_y\),則稱 \(l_{i,x}\) 無意義。若某個 \(l_{i,x}\) 能出現在等式或不等式中進行運算,當且僅當 \(l_{i,x}\) 有意義。

構造一個二元組不可重集合 \(T_i\),一個二元組 \((u,v)\)\(T_i\) 中出現,當且僅當滿足以下四個條件:

  • \(1\le u,v\le |s_i|\)
  • \(l_{i,u}\) 有意義。
  • \(v=u+l_{i,u}-1\)
  • 不存在 \(w\in[1,u)\),使得 \(u+l_{i,u}-1\le w+l_{i,w}-1\)

\(s_j\)\(T_i\) 中出現,當且僅當存在 \((u,v)\in T_i\) 使得 \(s_i[u,v]=s_j\)

再構造一個二元組不可重集合 \(R_{i,j}\),一個二元組 \((u,v)\)\(R_{i,j}\) 中出現,當且僅當滿足以下兩個條件:

  • \(1\le u\le v\le |s_i|\)
  • \(s_i[u,v]=s_j\)

那麼原題目中的二元組 \((i,j)\) 滿足條件,當且僅當 \(\boldsymbol{R_{i,j}\ne \varnothing}\)\(\boldsymbol{R_{i,j}\subseteq T_i}\)

\(R_{i,j}\ne \varnothing\) 很顯然,就不證了。

證明:

  • 充分性:

    考慮反證,假設當 \(R_{i,j} \subseteq T_i\) 時存在 \(k\) 使得 \(s_j\)\(s_k\) 的子串且 \(s_k\)\(s_i\) 的真子串。

    \(s_i[a,b]=s_k\)\(s_k[c,d]=s_j\)。那麼 \(s_i[a+c-1,a+d-1]=s_j\)。根據已知條件可以得到 \((a+c-1,a+d-1)\in R_{i,j},T_i\)

    \(l_{i,a+c-1}\ne d-c+1\),則與 \((a+c-1,a+d-1)\in T_i\) 的第三個條件不符。

    否則,此時 \(c>1\)。根據 \(l_{i,a}\) 的定義可知 \(l_{i,a}\ge b-a+1\),即 \(a+l_{i,a}-1\ge b\)。但是 \(a+c-1+l_{i,a+c-1}-1=a+c-1+d-c+1-1=a+d-1\)。根據 \(d\le b-a+1\) 可以得到 \(a+d-1\le b\)。與 \((a+c-1,a+d-1)\in T_i\) 的第四個條件不符。

    因此假設不成立。當 \(R_{i,j} \subseteq T_i\) 時一定不存在 \(k\) 使得 \(s_j\)\(s_k\) 的子串且 \(s_k\)\(s_i\) 的真子串。

  • 必要性:

    考慮 \((u,v)\in R_{i,j}\) 但是 \((u,v)\notin T_i\)。此時 \((u,v)\) 一定滿足某個二元組在 \(T_i\) 中的前兩個條件。

    \(l_{i,u}\ne v-u+1\),則有一個更長的字串 \(s_y\)\(s_i[u,|s_i|]\) 的字首。此時 \(s_j\)\(s_y\) 的真子串。

    否則,一定存在 \(w\in[1,u)\) 使得 \(u+l_{i,u}-1\le w+l_{i,w}-1\)。說明存在一個字串 \(s_y=s_i[w,w+l_{i,w}-1]\)。此時 \(s_y\)\(s_j\) 包含在中間,即 \(s_j\)\(s_y\) 的真子串(能保證是真子串是因為 \(w\in[1,u)\))。

    所以不滿足 \(R_{i,j}\subseteq T_i\) 一定不會滿足原來的條件,這是一個必要條件。

光有這個結論還不夠,總不可能求出集合然後列舉判斷。

進一步推理可以得到,它其實等價於 \(\boldsymbol{\sum\limits_{(u,v)\in T_i}P(s_i[u,v]=s_j)=|R_{i,j}|}\)

為了區分中括號和艾弗森括號,使用 \(P(A)\) 表示 \(A\) 命題是否為真。當且僅當 \(A\) 為真命題時 \(P(A)=1\);當且僅當 \(A\) 為假命題時 \(P(A)=0\)

為什麼呢?不難發現 \(\sum\limits_{(u,v)\in T_i}P(s_i[u,v]=s_j)=|T_i\bigcap R_{i,j}|\le |R_{i,j}|\)。而 \(|T_i\bigcap R_{i,j}|=|R_{i,j}|\) 等價於 \(R_{i,j}\subseteq T_i\)

那麼我們只需要對於一個 \(j\),求出 \(\sum\limits_{(u,v)\in T_i}P(s_i[u,v]=s_j)\)\(|R_{i,j}|\) 即可。

  • 前者的求法:

    首先要得到 \(T_i\)。可以考慮對於每個字串 \(s_a\),它會對哪些排名的字尾的產生貢獻。這個字尾要包含 \(s_a\),等價於兩者 \(|\text{LCP}|\ge |s_a|\)。可以維護 \(\text{height}\) 陣列的 ST 表然後二分得到排名區間,讓這個區間內的 \(l\) 值對 \(|s_a|\)\(\max\)。線段樹維護即可。

    然後就可以從左往右掃,維護字首的 \(u+l_{i,u}-1\le w+l_{i,w}-1\) 最大值 \(pre\)。線上段樹上單點查詢當前字尾排名那個位置的值得到 \(l_{i,x}\)。若 \(x+l_{i,x}-1>pre\),則將 \((x,x+l_{i,x}-1)\) 加入 \(T_i\)

    然後使用桶維護 \(T_i\)\(s_1\sim s_n\) 中每種字串各作為多少個字尾的最長字首。

  • 後者的求法:

    考慮 \(s_j\) 作為某個字尾的字首出現,同樣可以求出包含它的字尾排名區間。然後變成求區間內有多少個排名使得這個排名的字尾來自於編號為 \(i\) 的字串。

    對於每個 \(i\) 用一個 vector 從小到大存放其字尾出現的位置,二分得到左右端點 \(l,r\),答案即為 \(r-l+1\)

這樣仍需要列舉 \(j\)。但你注意到:

由於 \(R_{i,j}\ne \varnothing\),那些沒在 \(T_i\) 中出現的 \(s_j\) 一定沒有貢獻。因為此時 \(|T_i\bigcap R_{i,j}|=0< |R_{i,j}|\)。只需要考慮那些在 \(T_i\) 中出現的 \(s_j\)。而對於每個 \(u\),只會有一個 \(v\) 使得 \((u,v)\in T_i\),因此 \(|T_i|=\mathcal{O}(|s_i|)\)。這樣一來總共只需要處理 \(\mathcal{O}(|S|)\) 次。

為了不算重算漏,考慮對於每個在 \(T_i\) 中出現的字串,在其第一次出現的位置統計。

最後對 \(s_1\sim s_n\) 的每個字串都這樣做一遍,就能得到正確答案了。

時空複雜度均為 \(\mathcal{O}(|S|\log |S|)\)

AC code

CF1608G Alphabetic Tree

  • 給出一棵 \(n\) 個點的樹,邊 \(i\) 上有字母 \(c_i\)。定義 \(str(u,v)\) 為從點 \(u\) 走到點 \(v\) 途徑邊上的字母順次拼接得到的字串。形式化的,若點 \(u\) 到 點 \(v\) 路徑上的邊依次為 \(p_1,p_2,\dots,p_k\),則 \(str(u,v) = \overline{c_{p_1}c_{p_2}\dots c_{p_k}}\)

  • 你有 \(m\) 個字串 \(s_1\sim s_m\)\(q\) 個詢問,每個詢問形如 \(u\texttt{ }v\texttt{ }l\texttt{ }r\),你要回答 \(str(u,v)\)\(s_l\sim s_r\) 中出現了幾次。在一個串中重複出現算多次。

  • \(n,m,q,\sum\limits_{i=1}^m|s_i|\le 10^5\)

考慮將答案表示成差分的形式並離線計算,即 \([1,r]\) 的答案減去 \([1,l)\) 的答案。掃描右端點,每次將端點的串加入貢獻,然後回答所有端點為該點的詢問。這是大體思路。

先套路將所有串用奇怪字元拼接起來(拼成的大串記為 \(S\)),做一遍字尾排序,求出 \(sa,rk,\text{height}\) 陣列以及 \(\text{height}\) 陣列的 ST 表。根據題意,下文預設 \(n,m,q,|S|\) 同階。

\(hd_i\)\(s_i\) 開頭的字尾在 \(S\) 中出現的位置,\(suf_i\)\(S\)\(i\) 開頭的字尾。則每個詢問的答案為 \([1,hd_r+|s_r|]\) 的答案減去 \([1,hd_l)\) 的答案。

我們試圖把 \(str(u,v)\) 表示成 \(s_l\sim s_r\) 的某個字尾的字首的形式。而這些字尾的排名一定是連續的。

因此,我們想要求出這些字尾排名的區間。我們發現 \(str(u,v)\) 的字典序一定不大於這些字尾。因為這些字尾以 \(str(u,v)\) 為字首,相當於在 \(str(u,v)\) 後面接上某個串,而 \(str(u,v)\) 本身可以看作 \(str(u,v)\) 接上一個空串(在字尾排序種認為空串的字典序最小),肯定是這些串中字典序最小的。

考慮二分出字典序大於等於 \(str(u,v)\) 的字尾的最小排名 \(rkl\)。設當前二分的排名為 \(mid\),我們要比較這兩個串的大小。由於是樹上詢問,因此 \(\sum|str(u,v)|\) 可能會很大,不能將它們和 \(s_1\sim s_m\) 一起字尾排序。想象一下我們是怎麼手動比較字典序大小的,肯定是先找一段 \(\mathbf{LCP}\),然後比較下一個不相等的位置。

考慮使用雜湊。若按照常規方法直接使用樹剖、線段樹或樹上倍增維護路徑的雜湊值再二分 \(|\text{LCP}|\),單次比較的時間複雜度會達到 \(\mathcal{O}(\log n \cdot \log |S|)\)。這麼一來總的時間複雜度為 \(\mathcal{O}(q\log n\cdot \log^2|S|)\),無法接受。

我們發現二分 \(|\text{LCP}|\) 會產生很多無用的比較,所以考慮把這個 \(\log |S|\) 最佳化掉。我們知道,\(u\)\(v\) 的路徑會被分成 \(\mathcal{O}(\log n)\) 條重鏈。我們可以按照順序比較一條條重鏈的雜湊值,如果整條重鏈能匹配上就直接跳過,否則再二分找那個失配的位置。

具體地,先計算 \(S\) 的字首雜湊值 \(H\),再對樹做一遍重鏈剖分,預處理每條重鏈自上而下的字首雜湊值 \(H_u\) 以及自下而上的字尾雜湊值 \(H_d\)。還需要有三個求區間雜湊值的函式。

每次詢問 \(u,v\) 時,將路徑上的重鏈、當前路徑在該重鏈上經過了第幾個位置到第幾個位置該重鏈是自上而下走還是自下而上走按順序存放在一個容器裡。注意兩個點跳到同一重鏈上的情況。這個過程還需要求出 \(\text{LCA}\)

然後遍歷容器,一段段與排名為 \(mid\) 的字尾的對應位置匹配。若能夠匹配上,則將已匹配長度加上當前路徑在該重鏈上的長度,並跳到下一條鏈繼續匹配。否則二分尋找失配位置,將已匹配長度加上 \(|\text{LCP}|\),並比較失配位置的兩個字元的大小。需要記一個 \(p\) 表示已經匹配好到該字尾的第 \(p\) 個位置。注意 \(\boldsymbol{p}\) 和已匹配長度的關係。你會感覺這個過程有點像值域分塊。

這部分細節繁多,比如匹配的方向、需要匹配的長度、還未匹配的長度、以及一方匹配完之後如何比較大小等。為了方便,這個過程需要求出兩個值,\(|\text{LCP}|\) 以及大小關係。

求出來後,若 \(|\text{LCP}|\) 不等於路徑長度 \(len\),則不存在這樣的字尾使得 \(str(u,v)\) 為其字首。否則再二分找到最大的排名 \(rkr\) 使得 \(\text{LCP}(suf_{sa_{rkl}},suf_{sa_{rkr}}) \ge len\),這個用 \(\text{height}\) 陣列的 ST 表求 \(\text{LCP}\) 就行了。

設當前掃描到的右端點為 \(rpos\),現在求出來了 \(rkl\)\(rkr\),問題變成在 \(suf_{1}\sim suf_{rpos}\) 中,有多少字尾的排名在 \([rkl,rkr]\) 之間。這是一個平凡的二維數點問題,使用樹狀陣列維護,掃描時將 \(rk_{rpos}\) 插入到樹狀陣列中,詢問就是樹狀陣列上區間 \([1,rkr]\) 的和減去區間 \([1,rkl)\) 的和。至此問題被解決。

時間複雜度為 \(\mathcal{O}(q\log n\cdot \log |S|)\),空間複雜度為 \(\mathcal{O}(|S|)\)。可以接受。

離線樹狀陣列二維數點做法提交記錄(含程式碼)

考慮如何線上地解決這個問題。

這個時候我們把離線樹狀陣列換成主席樹即可,將所有字尾 \(suf_i\) 插入主席樹版本 \([1,i]\)\(rk_i\) 位置。仍然求出 \(rkl\)\(rkr\),然後把原來的離線差分變成 \([1,hd_r+|s_r|]\)\([1,hd_l-1]\) 兩個版本相減即可。

時間複雜度為 \(\mathcal{O}(q\log n\cdot \log |S|)\),空間複雜度為 \(\mathcal{O}(|S|\log |S|)\)。可以接受。

線上主席樹二維數點做法提交記錄(含程式碼)

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

  • 給出長度為 \(n\) 的字串 \(s\)\(m\) 組詢問對 \(s[l,r]\) 這個子串進行字尾排序後,(這個子串的)字尾 \(s[k,r]\) 的排名。排名定義為比它小的字尾的個數 \(+1\)

  • 多組資料,記 \(N=\sum n\)\(M=\sum m\)\(N,M\le 5\times 10^4\)

  • \(\text{5.00 s / 256.00 MB}\)

這個 \(N\le 5\times 10^4\)\(\text{5.00\,s}\) 時限是不是為了放時間複雜度 \(\mathcal{O}((N+M)\log^3 N)\) 的做法過啊,是的話就太不牛了 /qd。

先對原串進行字尾排序。

考慮從排名的定義入手,求出子串中有多少個字尾比詢問的字尾小。對於這些子串中的字尾,考慮找到它們在原串中的字尾,嘗試尋找充要條件。

設有(子串的)字尾 \(s[i,r]\),其中 \(i\in[l,k)\bigcup \,(k,r]\)。按兩類情況考慮。

  • \(rk_i<rk_k\)

    此時 \(s[i,r]<s[k,r]\)當且僅當 \(\boldsymbol{i<k}\)\(\bold{|LCP}\boldsymbol{(s[i,n],s[k,n])|\le r-k}\),或 \(\boldsymbol{i>k}\)

    • 充分性

      \(i<k\)\(|\text{LCP}(s[i,n],s[k,n])|\le r-k\) 時,兩個字尾第一個不同的位置一定均在 \(s[i,r]\)\(s[k,r]\) 中出現,此時比較兩個串也是比較這兩位,因為 \(rk_i<rk_k\),故 \(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\),則一定有 \(|\text{LCP}(s[i,n],s[k,n])|\le r-k\),否則 \(s[k,r]\)\(s[i,r]\) 字首,此時 \(s[k,r]<s[i,r]\)。若 \(i>k\),則已經滿足條件。

    • 做法

      \(i<k\)\(i>k\) 討論。

      \(i<k\),則需要統計有多少個字尾 \(s[i,n]\) 滿足 \(i\in[l,k)\)\(rk_i<rk_k\)\(\text{|LCP}(s[i,n],s[k,n])|\le r-k\)。降第三個限制轉化為 \(\text{height}\) 陣列的限制,其等價於 \(\min\limits_{j=rk_i+1}^{rk_k}\text{height}_j\le r-k\)。容易發現此時滿足條件的 \(i\)\(rk_i\) 在一個字首 \([1,x]\) 中,其中 \(x<rk_k\)。二分 + RMQ 求出這個 \(x\),問題轉化成統計有多少個點對滿足 \(i\in[l,k)\)\(rk_i\in[1,x]\),主席樹維護即可。

      \(i>k\),則需要統計有多少個字尾 \(s[i,n]\) 滿足 \(i\in(k,r]\)\(rk_i<rk_k\),同樣主席樹維護。

  • \(rk_i>rk_k\)

    此時 \(s[i,r]<s[k,r]\)當且僅當 \(\boldsymbol{i>k}\)\(\bold{|LCP}\boldsymbol{(s[i,n],s[k,n])|\ge r-i+1}\)

    • 充分性

      容易發現此時 \(s[i,r]\)\(s[k,r]\) 字首,故 \(s[i,r]<s[k,r]\)

    • 必要性

      考慮證明不滿足上述條件則 \(s[i,r]>s[k,r]\)

      \(i<k\),如果兩個串第一個不同的位置均在 \([l,r]\) 中出現,因為 \(rk_k<rk_i\),所以 \(s[i,r]>s[k,r]\)。否則,\(s[k,r]\)\(s[i,r]\) 字首,此時 \(s[i,r]>s[k,r]\)

      \(i>k\)\(\text{|LCP}(s[i,n],s[k,n])|\le r-i\),則兩個串第一個不同的位置一定均在 \([l,r]\) 中出現,因為 \(rk_k<rk_i\),所以 \(s[i,r]>s[k,r]\)

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

      以排名為下標做一遍序列分治,將詢問掛在 \(rk_k\) 上,每層分治考慮右半邊對左半邊的貢獻(很像 cdq 分治)並左右遞迴下去統計,則對於任意一個合法的字尾,根據分治樹的形態,一定存在且僅存在一層分治,使得詢問在左半邊,字尾在右半邊,此時它被統計到。並且,在每層分治中我們統計合法的貢獻,可以做到不重不漏。

      設分治區間為 \([L,R]\),中點 \(mid=\left\lfloor\dfrac{L+R}{2}\right\rfloor\)

      對於左半邊的一個詢問 \((l,r,k)\),我們要統計右半邊有多少個 \(i\),滿足:

      • \(sa_i\in(k,r]\)

      • \(\text{|LCP}(s[k,n],s[sa_i,n])|\ge r-sa_i+1\Leftrightarrow \min\limits_{j=rk_k+1}^i \text{height}_j\ge r-sa_i+1\)

      採用序列分治的一般套路,從 \(mid\rightarrow L\) 掃描詢問。設當前掃到的排名為 \(K\)。維護變數 \(mn=\min\limits_{j=K+1}^{mid}\text{height}_j\)。對於右半區間維護字首 \(\text{height}\) 最小值,即 \(pmn_i=\min\limits_{j=mid+1}^{i}\text{height}_j\)。則對於當前掃到的排名上的詢問,條件中的 \(\min\limits_{j=rk_k+1}^i \text{height}_j\ge r-i+1\) 可以轉化為 \(\min\{mn,pmn_i\}\)

      容易發現 \(pmn_i\) 具有單調(不升)性。可以找到一個分界點 \(p\),使得當 \(i\in[mid+1,p)\) 時,\(\min\{mn,pmn_i\}=mn\);當 \(i\in[p,R]\) 時,\(\min\{mn,pmn_i\}=pmn_i\)

      對於分界點左邊的情況,就是統計有多少 \(i\) 滿足:

      • \(sa_i\in(k,r]\)

      • \(mn \ge r-sa_i+1\Leftrightarrow sa_i\ge r-mn+1\)

      • \(i\in[mid+1,p)\)

      整理一下就是:

      • \(sa_i\in[\max\{r-mn,k\}+1,r]\)

      • \(i\in[mid+1,p)\)

      容易主席樹維護。

      對於分界點右邊的情況,就是統計有多少 \(i\) 滿足:

      • \(sa_i\in(k,r]\)

      • \(pmn_i\ge r-sa_i+1\Leftrightarrow sa_i+pmn_i\ge r+1\)

      • \(i\in [p,R]\)

      你發現這是個三維數點,好像行不通啊!

      然後就是一個很妙的轉化了。考慮正難則反。你發現對於分界點右邊的情況,\(sa_i+pmn_i\ge r+1\Rightarrow sa_i+mn\ge r+1\),因為在分界點右邊 \(pmn_i=\min\{pmn_i,mn\}\)。所以可以先統計滿足以下條件的 \(i\) 的個數:

      • \(sa_i\in[\max\{r-mn,k\}+1,r]\)

      • \(i\in [p,R]\)

      算上分界點左邊的統計,相當於要統計右半邊滿足 \(sa_i\in[\max\{r-mn,k\}+1,r]\)\(i\) 個數。可以 vector + 二分統計。考慮哪些不合法的被統計了,顯然它滿足:

      • \(sa_i\in[\max\{r-mn,k\}+1,r]\)

      • \(sa_i+pmn_i\le r\)

      • \(i\in[p,R]\)

      於是就要減去這樣的 \(i\) 的個數。實際上這還是個三維數點,不過你發現,\(\boldsymbol{\nexists\,i\in[mid+1,p),sa_i\in[\max\{r-mn,k\}+1,r]\land sa_i+pmn_i\le r}\)。即分界點左邊不存在滿足前兩個條件的 \(i\)

      為什麼呢?首先 \(sa_i\in[\max\{r-mn,k\}+1,r]\) 的必要條件是 \(sa_i\ge r-mn+1\)。你考慮分界點左邊 \(mn\le pmn_i\),若 \(sa_i\ge r-mn+1\)\(sa_i+mn\ge r+1\),則一定有 \(sa_i+pmn_i\ge r+1\)。反之,若 \(sa_i+pmn_i\le r\),則一定有 \(sa_i+mn \le r\)\(sa_i\le r-mn\)。因此兩個條件不能被同時滿足

      所以我們直接大膽忽略 \(i\in[p,R]\) 這個條件,統計全域性(當前分治區間) \(sa_i\in[\max\{r-mn,k\}+1,r]\)\(sa_i+pmn_i\le r\)\(i\) 的個數。同樣是二維數點,主席樹維護即可。

至此兩類統計都解決了。接下來算複雜度。因為有主席樹和 ST 表,所以空間複雜度顯然為 \(\mathcal{O}(N\log N)\)

至於時間複雜度(只說每個部分的瓶頸),字尾排序是 \(\mathcal{O}(N\log^2 N)\) 的(因為不是瓶頸所以沒用基數排序最佳化)。\(rk_i<rk_k\) 的統計需要往主席樹中插入 \(\mathcal{O}(N)\) 個點對,並且每次詢問要進行一次 \(\mathcal{O}(1)\) 檢查(ST 表)的二分以及 \(\mathcal{O}(\log N)\) 的主席樹查詢,時間複雜度為 \(\mathcal{O}((N+M)\log N)\)

對於分治部分,每個詢問會在 \(\mathcal{O}(\log N)\) 層分治中被掃到,每次掃到要做一次主席樹查詢和 vector 二分,單次是 \(\mathcal{O}(\log N)\)。每個點對會在 \(\mathcal{O}(\log N)\) 層分治中被插入主席樹,單次也是 \(\mathcal{O}(\log N)\)。這部分的時間複雜度為 \(\mathcal{O}((N+M)\log^2 N)\)。為了維護主席樹,每層分治需要將點對進行排序,由於每層分治的區間總長度為 \(N\),因此任意一層排序的 \(\log\) 不超過 \(\mathcal{O}(\log N)\)。容易透過乘法分配律得到是 \(\mathcal{O}(N\log^2 N)\) 的。因此,分治部分的總時間複雜度為 \(\mathcal{O}((N+M)\log ^2 N)\)

綜上,本做法時間複雜度為 \(\mathcal{O}((N+M)\log^2 N)\),空間複雜度為 \(\mathcal{O}(N\log N)\),可以接受。

程式碼

AC 記錄

CF1098F Ж-function

  • 給出長度為 \(n\) 的字串 \(s\)。定義 \(Ж(l,r)=\sum\limits_{i=l}^r|\text{lcp}(s[l,r],s[i,r])|\)\(q\) 次詢問,每次給出 \(l,r\),查詢 \(Ж(l,r)\)

  • \(n,q\le 2\times 10^5\)\(\text{6 s / 500 MB}\)

先字尾排序。

將子串的 \(\text{lcp}\) 搞成字尾的 \(\text{lcp}\),則 \(Ж(l,r)=\sum\limits_{i=l}^r\min\{|\text{lcp}(s[l,n],s[i,n])|,r-l+1\}\)

然後將詢問掛在 \(\text{rk}_{l}\) 上,分別計算 \(\text{rk}_i<\text{rk}_l\)\(\text{rk}_i>\text{rk}_l\) 的貢獻,最後算上 \(l\) 本身的貢獻。此時可以將 \(\text{lcp}\) 的限制轉化成 \(\text{height}\) 陣列的限制,即:

\[Ж(l,r)=\sum\limits_{i=1}^{\text{rk}_l-1}\left([l\le \text{sa}_i\le r]\cdot \min\left\{\min\limits_{j=i+1}^{\text{rk}_l}\text{height}_j,r-l+1\right\}\right)+\sum\limits_{i=\text{rk}_l+1}^{n}\left([l\le \text{sa}_i\le r]\cdot \min\left\{\min\limits_{j=\text{rk}_l+1}^{n}\text{height}_j,r-l+1\right\}\right)+(r-l+1) \]

我們先以 \(\text{rk}_i<\text{rk}_l\) 的情況為例講一下怎麼算貢獻。

\(\text{height}\) 陣列進行序列分治(其實此處比較像 cdq 分治),記當前分治區間為 \([L,R]\),中點 \(M=\dfrac{L+R}{2}\)\(N=R-L+1\)。考慮當前層右半邊對左半邊的貢獻。

\(\text{pre}_j=\min\limits_{k=M+1}^R\text{height}_k\)。對於左半邊按 \(M\rightarrow L\) 的順序掃描 \(i\),並同時記錄 \(\text{mn}=\min\limits_{k=i+1}^M\text{height}_j\)

考慮掛在 \(i\) 上的一個詢問 \((l,r)\)

此時,存在 \(p\in[M+1,R]\) 使得當 \(j\in[M+1,p)\)\(\text{mn}\le \text{pre}_j\);當 \(j\in[p,R]\)\(\text{mn}>\text{pre}_j\)

化簡第二層 \(\min\{\}\),那麼右半邊對 \((l,r)\) 的貢獻就是:

\[\sum\limits_{j=M+1}^{p-1}([l\le \text{sa}_j\le r]\cdot\min\{\text{mn},r-\text{sa}_j+1\})+\sum\limits_{j=p}^R([l\le \text{sa}_j\le r]\cdot\min\{\text{pre}_j,r-\text{sa}_j+1\}) \]

由於 \(i\) 遞減,\(\text{mn}\) 不升,因此 \(p\) 不降,最多遞增 \(\mathcal{O}(N)\) 次。

然後將 \(\min\{\}\) 拆開,即討論一下誰是最小值,此處我們只討論前面那個數更小(不等於)的情況。因為兩種討論都是類似的。

對於 \(j\in[M+1,p)\) 的部分,我們要求 \(\sum\limits_{j=M+1}^{p-1}([l\le \text{sa}_j\le r\land \text{mn}<r-\text{sa}_j+1]\cdot \text{mn})\),提取公因式 \(\text{mn}\) 後發現是關於 \(j,\text{sa}_j\) 的二維偏序。可以用樹狀陣列維護 \(\text{sa}_j\),在 \(p\) 移動時更新樹狀陣列(就是掃描線)。

對於 \(j\in[p,R]\) 的部分,我們要求 \(\sum\limits_{j=p}^R([l\le \text{sa}_j\le r\land \text{pre}_j<r-\text{sa}_j+1]\cdot \text{pre}_j)\)

接下來是重點,也是這個套路最巧妙的一步。

如果按照之前的方法找偏序關係,發現是關於 \(j,\text{sa}_j,\text{sa}_j+\text{pre}_j\) 的三維偏序。你要是在分治內部再套個樹套樹 / cdq 分治的話複雜度肯定爆炸。

我們先求 \(\sum\limits_{j=p}^R([l\le \text{sa}_j\le r\land \text{mn}<r-\text{sa}_j+1]\cdot \text{pre}_j)\)。這東西拆開後是二維偏序,樹狀陣列類似維護。

由於右半邊 \(\text{pre}_j<\text{mn}\),漏算的貢獻是 \(\sum\limits_{j=p}^R([l\le \text{sa}_j\le r\land \text{pre}_j<r-\text{sa}_j+1\le \text{mn}]\cdot \text{pre}_j)\)。你發現這還是個三維偏序,那不是白搞?別急,你發現當 \(j\in[M+1,p)\) 時,\(\text{mn}\le \text{pre}_j\),即 \(\text{pre}_j<r-\text{sa}_j+1\le \text{mn}\) 不成立。因此直接忽略掉 \(j\) 這一維限制即可!那麼剩下的就是關於 \(\text{sa}_j,\text{sa}_j+\text{pre}_j\) 的二維偏序,由於掃描的是 \(p\),所以離線下來再樹狀陣列維護即可。

那麼這種情況就討論完了。剩下的一種情況是類似的,尤其是對於 \([p,R]\) 這部分貢獻三維偏序轉二維偏序的時候,都是將 \(\text{mn}\) 代入二維偏序,再加上 \((\text{pre}_j,\text{mn}]\) 漏算的 / 減去 \((\text{pre}_j,\text{mn}]\) 多算的,然後透過不同區間 \(\text{pre}_j,\text{mn}\) 大小關係忽略 \(j\) 那一維限制。

至於 \(\text{rk}_i>\text{rk}_l\) 的情況,只是需要再分治的時候換成掃描右半邊,對左半邊維護字尾最小值,計算貢獻部分經過瞪眼觀察或手推後都可以發現是一模一樣的。

那麼這題就做完了。

\(i\) 這個位置上掛了 \(Q_i\) 個詢問。可以發現一層分治的時間複雜度為 \(\mathcal{O}\left(\left(N+\sum \limits_{i=L}^RQ_i\right)\log n\right)\)。考慮到分治樹的深度為 \(\mathcal{O}(\log n)\),且對於同一深度的區間而言 \(\sum N=n,\sum Q_i=q\)。所以總的時間複雜度為 \(\mathcal{O}\left((n+q)\log ^2 n\right)\),空間複雜度為 \(\mathcal{O}(n+q)\)

AC Link & Code

P8203 [傳智杯 #4 決賽] DDOSvoid 的饋贈

  • 給出 \(n\) 個模板串 \((s_1,\dots,s_n)\)\(m\) 個查詢串 \((t_1,\dots,t_m)\)。有 \(m\) 次詢問,每次給出 \(x,y\),求有多少個模板串同時是 \(t_x,t_y\) 的子串。

  • \(n,m,q\le 10^5\)

  • \(\text{4.00 s\space / 512 MB}\)

考慮將所有串用分隔符拼一起字尾排序,然後對於每個排名為 \(i\) 的字尾,記錄 \(c_i\) 表示它來自哪個字串。對於一個模板串 \(s_i\),二分 + ST 表求出包含它的字尾排名區間 \([l_i,r_i]\)。那麼對於 \((x,y)\) 這個詢問,就是求有多少個 \(i\),滿足 \(c\) 陣列 \([l_i,r_i]\) 這個區間內出現了 \(x,y\) 這兩種權值。

其實來自分隔符和查詢串的字尾是沒用的,因此可以在 \(c\) 陣列中刪去這些位置。記新陣列為 \(C\)。在新陣列上,記 \([L_i,R_i]\) 表示 \(C\) 中排名在 \([l_i,r_i]\) 內的字尾的區間。那麼就是對於 \(C\),求有多少個 \([L_i,R_i]\) 內出現了 \(x,y\) 兩種權值。因為 \(C[L_i,R_i]\) 就是 \(c[l_i,r_i]\) 刪去一些不可能為 \(x,y\) 的位置,因此在 \(C[L_i,R_i]\) 中一定也出現了 \(x,y\)

\(c\) 轉化成 \(C\) 純粹是為了卡常。

考慮轉化後的問題,記 \(\text{cnt}_x\) 表示 \(x\)\(C\) 中的出現次數。不妨令 \(\text{cnt}_x\le \text{cnt}_y\),不符則交換兩者即可。接下來考慮欽定 \([L_i,R_i]\) 最左邊的 \(x\) 是哪一個,將每種情況的數量相加。

遍歷 \(C\)\(x\) 的位置 \(j\),記它左邊最後一個 \(x\) 的位置為 \(p_1\),左邊最後一個 \(y\) 的位置為 \(p_2\),右邊第一個 \(y\) 的位置為 \(p_3\)

首先需要滿足 \(L_i\le j\le R_i\)。由於 \(j\) 是最左邊的 \(x\),因此應滿足 \(L_i>p_1\)。接下來考慮 \([L_i,R_i]\) 內是否存在一個比 \(j\) 位置更左的 \(y\),然後將兩種情況的個數相加。

若存在,則應滿足 \(L_i\le p_2\);否則應滿足 \(p_2<L_i\)\(R_i\ge p_3\)。容易證明如果滿足上述條件區間內一定存在 \(x,y\)。否則,因為 \((p_2,p_3)\) 內不存在 \(y\)\([L_i,R_i]\) 就不滿足該情況下的條件。

那麼上面討論的這些情況的答案全部都是二維數點,容易解決。

問題是暴力遍歷 \(x\) 的所有位置真的能接受嗎?

考慮將 \(\mathcal{O}(n)\) 個詢問去重。若 \(\text{cnt}_x\le \sqrt{n}\),則最多帶來 \(\mathcal{O}(n\sqrt{n})\) 個詢問。否則,考慮那些 \(\text{cnt}_x>\sqrt{n}\) 的詢問的 \(y\),此時只有 \(\mathcal{O}(\sqrt{n})\) 個本質不同的 \(y\)。對於這些 \(y\),和它構成詢問的每個 \(x\) 會帶來 \(\mathcal{O}(\text{cnt}_x)\) 個詢問。而因為去過重,因此這些 \(x\) 都是不同的。因此 \(\text{cnt}_x\) 的和不超過 \(\mathcal{O}(n)\),所以詢問數量還是 \(\mathcal{O}(n\sqrt{n})\) 個。

那麼變成 \(\mathcal{O}(n)\) 個點 \(\mathcal{O}(n\sqrt{n})\) 個詢問的二維數點,且都是 3-side 矩形,掃描 1-side 那一側,剩下一維維護字首和,使用 \(\mathcal{O}(\sqrt{n})\) 區間加,\(\mathcal{O}(1)\) 單點查的分塊即可做到 \(\mathcal{O}(n\sqrt{n})\)

還有一個問題,怎麼快速求出 \(p_2,p_3\)?考慮離線,對於每一種 \(y\) 單獨求解。維護此時每個位置對應的 \(p_2,p_3\),那麼對於一個 \(y\) 的位置 \(j\),它可以對它後面位置的 \(p_2\) 和它前面位置的 \(p_3\) 產生貢獻。分別使用 \(\mathcal{O}(\sqrt{n})\) 區間取 \(\min/\max\)\(\mathcal{O}(1)\) 單點查的分塊維護,由於每個 \(y\) 做一次,因此 \(C\) 中每個位置至多帶來一次區間修改,而單點查詢數和上面詢問數同階,因此這部分仍是 \(\mathcal{O}(n\sqrt{n})\)

那麼我們得到了一個時空都是 \(\mathcal{O}(n\sqrt{n})\) 的做法,空間爆炸。原因是存不下那麼多詢問。可以考慮設定一個閾值 \(B\),每產生 \(B\) 個詢問就數一次點並清空。這樣修改部分常數會變大因為每數一次點就要修改一次。但是空間的問題解決了,那部分常數也可以忽略不計。

事實上 \(B\)\(10^7\) 左右可以透過本題。

AC Link

Code

CF917E Upside Down

  • 給出 \(n\) 個點的樹,第 \(i\) 條邊上有字母 \(c_i\)。有 \(m\) 個字串 \(s_1\sim s_m\) 以及 \(q\) 組詢問。每次詢問給出 \(x,y,k\)

  • \(\text{str}(x,y)\)\(x,y\) 簡單有向路徑邊上的字母按順序拼接得到的字串,形式化地,若 \(x,y\) 簡單有向路徑上一共有 \(E\) 條邊,記 \(e_i\)\(x,y\) 有向路徑上的第 \(i\) 條邊,則 \(\text{str}(x,y)=\overline{c_{e_1}c_{e_2}\dots c_{e_E}}\)

  • \(s_k\)\(\text{str}(x,y)\) 中出現了多少次。形式化地,求有多少個正整數 \(i\in[1,|str(x,y)|-|s_k|+1]\) 使得 \(\forall\,j\in[0,|s_k|-1]\bigcup \mathbb{Z},(s_k)_{i+j}=[\text{str}(x,y)]_{i+j}\)

  • \(M=\sum\limits_{i=1}^m |s_i|\)\(n,m,M,q\le 10^5\)

  • \(\text{3 s / 512 MB}\)

約定:

  • 本文中所有下標均從 \(1\) 開始。欽定 \(1\) 為根。用印表機字型(\texttt)表示具體的字元 / 字串。

  • 預設 \(\mathcal{O}(n)=\mathcal{O}(m)=\mathcal{O}(M)=\mathcal{O}(q)\)\(\mathcal{O}(\sqrt{n})>\mathcal{O}\left(\log^2 n\right)\)

  • 記一個點 \(u\) 的父親為 \(\text{fa}_u\),深度(到根的邊數)為 \(\text{dep}_u\)

  • \(x\rightsquigarrow y\) 表示 \(x\)\(y\) 的簡單有向路徑,\(u\longleftrightarrow \text{fa}_u\) 這條邊上的字元為 \(\text{val}_u\)。特別地,\(\text{val}_1=\texttt{1}\)

  • \(\text{lca}(x,y)\) 表示 \(x,y\) 兩點的最近公共祖先。\(\text{lcp}(x,y)\) 表示兩個字串 \(x,y\) 的最長公共字首。

  • 記一個串 \(s\) 的反串為 \(s^R\)。形式化地,\(\left|s^R\right|=|s|\)\(s^R_i=s_{|s|-i+1}\)

  • \(\text{anc}(k,x)\) 表示 \(x\) 向上走 \(k\) 條邊到達的點,即 \(x\) 的樹上 \(k\) 級祖先。

  • 若字元參與運算,則其值等於其 \(\text{ASCII}\) 值。


考慮弱化版 CF1045J 的做法,\(s_k\) 出現的位置要麼完全包含在 \(x\rightsquigarrow \text{lca}(x,y)\)\(\text{lca}(x,y)\rightsquigarrow y\) 兩條直鏈內,要麼跨過了 \(\text{lca}(x,y)\)

\(\bf{Case\space1}\):完全包含在直鏈內的情況

在這部分中考慮使用雜湊實現字串匹配。我們的雜湊方式為多項式雜湊,即對於字串 \(s\),其雜湊值為:

\[H(s)=\sum\limits_{i=1}^{|s|}\left(s_i\cdot \text{base}^{|s|-i}\right)\bmod \text{MOD} \]

其中 \(\text{base}\) 為乘數,\(\text{MOD}\) 為模數。本題卡乘數和模數,我使用了【】生日的日期做乘數,\(10^9+9\) 做模數。不能自然溢位,因為後面需要用到逆元。

在弱化版中,我們運用 \(|S|\le 100\) 的條件,對於每一種長度的字串單獨處理。在此題中我們也可以如法炮製,需要運用到一個引理:

\(\bf{Lemma\space 1}\)

在字串總長度為 \(n\) 的長為 \(m\) 的字串序列 \(s_1\sim s_m\) 中,本質不同的字串長度種數為 \(\mathcal{O}(\sqrt{n})\) 級別。

\(\bf{Proof\space 1}\)

考慮 \(s_1\sim s_m\) 種出現了 \(k\) 種本質不同的長度,從小到大依次是 \(\text{len}_1\sim \text{len}_k\),記 \(\text{cnt}_i\) 表示第 \(i\) 種長度的出現次數,形式化地,\(\text{cnt}_i=\sum\limits_{j=1}^m[|s_j|=\text{len}_i]\)

那麼有:\(\sum\limits_{i=1}^k(\text{len}_i\cdot \text{cnt}_i)=n\)

可以發現 \(\text{len}_i\ge i,\text{cnt}_i\ge 1\)。後面那個很顯然,因為這種長度出現時一定存在一個字串滿足其長度為 \(\text{len}_i\)

至於前面那個使用歸納法證明:

  • \(i=1\)\(\text{len}_1\ge 1\) 顯然成立。

  • 假設對於 \(i\in[1,p]\cup\mathbb{Z}\) 時成立,則對於 \(i\in[1,p+1]\cup\mathbb{Z}\) 時,由於 \(\text{len}_1\sim \text{len}_k\) 中的每一個數都是一種本質不同的長度,且從小到大排列,所以 \(\text{len}_{p+1}>\text{len}_p\ge p\)。由於都是整數,所以 \(\text{len}_{p+1}\ge p+1\)

由於這裡涉及到的量都是正的,所以 \(\text{len}_i\cdot \text{cnt}_i\ge i\),因此 \(\sum\limits_{i=1}^ki\le \sum\limits_{i=1}^k(\text{len}_i\cdot \text{cnt}_i)=n\),因此有 \(\dfrac{k^2+k}{2}\le n\)。可以得到 \(k\le \sqrt{2n}=\mathcal{O}(\sqrt{n})\)。注意這裡不是在解不等式,由 \(\dfrac{k^2+k}{2}\le n\) 推匯出一個成立的條件。

證畢。

那麼我們可以對於這 \(\mathcal{O}(\sqrt{n})\) 種長度分別求解。在求解一種長度 \(\text{len}\) 的詢問時,我們對於每個點 \(u\) 預處理 \(\text{str}(u,\text{anc}(\text{len},u))\)\(\text{str}(\text{anc}(\text{len},u),u)\) 的雜湊值 \(\text{uk}_u\)\(\text{dk}_u\)(若不存在則不處理)。需要先預處理 \(\text{up}_u\)\(\text{dwn}_u\) 表示 \(\text{str}(u,1)\)\(\text{str}(1,u)\) 的雜湊值。容易得到:

  • \(\text{up}_u\equiv \text{up}_{\text{fa}_u}+\text{val}_u\cdot \text{base}^{\text{dep}_u-1}\pmod{ \text{MOD}}\)

  • \(\text{dwn}_u\equiv\text{dwn}_{\text{fa}_u}\cdot \text{base}+\text{val}_u\pmod{ \text{MOD}}\)

由於這個做法比較垃圾,我們不能在求解每種長度時重新遍歷樹計算雜湊值,否則會超時。可以考慮犧牲空間,在第一次遍歷樹時就對於每個點存下這些雜湊值。這樣可以省去 \(\mathcal{O}(\sqrt{n})\) 次遍歷樹的時間。

\(\text{uk}_u\)\(\text{dk}_u\) 都可以透過與 \(\text{anc}(\text{len},u)\) 的雜湊值差分得到,注意差分時的移位操作。

具體地:

  • \(\text{uk}_u\equiv \dfrac{\text{up}_u-\text{up}_{\text{anc}(\text{len},u)}}{\text{base}^{\text{dep}_u-\text{len}}}\pmod{\text{MOD}}\)

  • \(\text{dk}_u\equiv \text{dwn}_u-\text{dwn}_{\text{anc}(\text{len},u)}\cdot \text{base}^{\text{len}}\pmod{\text{MOD}}\)

可以預處理 \(\text{base}\) 的若干次方以及對應的逆元。考慮怎麼快速求 \(\text{anc}(\text{len},u)\)。由於查詢次數為 \(\mathcal{O}(n\sqrt{n})\),所以單次查詢必須為 \(\mathcal{O}(1)\)。可以考慮維護 \(1\rightsquigarrow u\) 構成的序列 \(\text{stk}\),使得 \(\text{stk}_i\) 為路徑上的第 \(i\) 個點。則所求即為 \(\text{stk}_{\text{dep}_u-\text{len}}\)。每次遍歷到一個點 \(u\) 時,將 \(u\) 加入序列末尾。結束 \(u\) 的遍歷時,將 \(u\) 從序列末尾刪除。

在求解每種長度時再考慮對於每種詢問串的雜湊值 \(\text{hsh}\) 單獨求解。考慮記 \(\text{num}_{0,u}\) 表示 \(1\rightsquigarrow u\) 上有多少個點 \(v\) 滿足 \(\text{uk}_v=\text{hsh}\)\(\text{num}_{1,u}\) 表示 \(1\rightsquigarrow u\) 上有多少個點 \(v\) 滿足 \(\text{dk}_v=\text{hsh}\)。這個可以考慮從 \(1\)\(n\) 掃描 \(i\),依次維護 \(v\in[1,i]\) 的情況,則每次新掃到一個位置,需要修改(\(+1\))的值拍平成 \(\text{dfn}\) 序後形如一段區間。

至於詢問,對於 \(x\rightsquigarrow \text{lca}(x,y)\) 那條鏈上的貢獻,考慮匹配的起點在哪個位置,容易發現鏈上的一個點 \(u\) 能夠匹配當前詢問串當且僅當 \(\text{dep}_u-\text{dep}_{\text{lca}(x,y)}\ge \text{len}\),且 \(\text{uk}_u=\text{hsh}\)。因為這樣才能包含在直鏈內。進一步發現滿足這個條件的點位於 \(x\rightsquigarrow \text{anc}(\text{dep}_x-\text{len}-\text{dep}_{\text{lca}(x,y)},x)\) 上,那麼拿 \(\text{num}_{0,x}\)\(\text{num}_{0,\text{anc}(\text{dep}_x-\text{len}-\text{dep}_{\text{lca}(x,y)},x)}\) 差分即可。\(\text{lca}(x,y)\rightsquigarrow y\) 的查詢方式類似,注意此時匹配的方向是自上而下,用 \(\text{num}_1\) 值差分計算。

此時,一共有 \(\mathcal{O}(n\sqrt{n})\) 次修改,\(\mathcal{O}(n)\) 次查詢,維護以 \(\text{dfn}\) 序為下標的差分陣列,那麼只需要分塊支援 \(\mathcal{O}(1)\) 單點修改,\(\mathcal{O}(\sqrt{n})\) 字首查詢即可。

值得注意的是,為了將同種雜湊值的詢問一起做,考慮使用排序將它們排在一個連續的區間內時,需要使用基數排序確保排序複雜度線性,才能保證 \(\boldsymbol{\mathcal{O}(\sqrt{n})}\) 次排序的總複雜度為 \(\boldsymbol{\mathcal{O}(n\sqrt{n})}\)


\(\bf {Case\space 2}\):跨過直鏈的情況

考慮分別處理每種串 \(s_k\) 的詢問。

假設跨過直鏈的匹配發生在 \(u\rightsquigarrow v\) 上,其中 \(u,v\)\(x\rightsquigarrow y\) 上的節點且 \(u\)\(v\) 前。此時一定滿足,\(\text{str}(u,\text{lca}(x,y))\)\(s_k\) 的字首,\(\text{str}(\text{lca}(x,y),v)\)\(s_k\) 的字尾。

同時,\(\text{str}(u,\text{lca}(x,y))\)\(\text{str}(x,\text{lca}(x,y))\) 的字尾,\(\text{str}(\text{lca}(x,y),v)\)\(\text{str}(\text{lca}(x,y),y)\) 的字首。

考慮找到最長的長度 \(P,Q\),使得 \(\text{str}(x,\text{lca}(x,y))\) 存在長度為 \(P\) 的字尾為 \(s_k\) 的字首;\(\text{str}(\text{lca}(x,y),y)\) 存在長度為 \(Q\) 的字首為 \(s_k\) 的字尾。

考慮一個基礎問題:

給出字串 \(a,b\),找到 \(b\) 的最長字首使得它是 \(a\) 的字尾。求出這個最長長度。

解決方法是:找到 \(a\) 的一個字尾 \(a[i,|a|]\) 使得 \(|\text{lcp}(a[i,|a|],b)|\) 最大。記 \(L=|\text{lcp}(a[i,|a|],b)|\),則 \(a[i,|a|]\) 最長的長度不超過 \(L\)\(\text{border}\) 的長度即為所求。

接下來證明正確性。

\(\bf {Proof\space 2}\)

首先,這個 \(\text{border}\) 一定是同時是 \(b\) 的字首和 \(a\) 的字尾。因為它是 \(a[i,|a|]\) 的字首又是它的 \(\text{border}\),說明 \(a[i,|a|]\) 存在這個 \(\text{border}\) 作為字尾。自然 \(a\) 也存在這個 \(\text{border}\) 作為字尾。記這裡求出來的長度為 \(\text{len}\)

考慮是否存在更長的答案。假設存在更長的答案長度為 \(\text{tmp}\),其一定不超過 \(L\),不然 \(a[i,|a|]\) 就不是使得 \(\text{lcp}\) 長度更大的字尾了。此時,\(a[i,|a|]\)\(b\) 存在長度為 \(\text{tmp}\)\(\text{lcp}\)。這時候 \(a[i,|a|]\) 開頭的 \(\text{tmp}\) 個字元形成的字串與結尾的 \(\text{tmp}\) 個字元形成的字串相等。此時 \(\text{tmp}\)\(a[i,|a|]\) 的一個更長的、長度不超過 \(L\)\(\text{border}\),矛盾。

因此不存在更長的答案,\(\text{len}\) 即為所求。

證畢。

將原問題轉化成上述形式,那麼 \(P\) 就是 \(\text{str}(\text{lca}(x,y),x)\) 最長的字首長度滿足它是 \(s_k^R\) 的字尾。這個和原問題顯然是等價的,因為兩個詢問串都是原問題詢問串的反串。不妨令新問題答案為 \(w\),則反過來後原串對應的位置也相等,原問題的答案至少\(w\);若原問題存在更長的答案 \(z\),則反串中這些對應的位置也相等,\(z\) 就是新問題的一個更長的答案,與 \(w\) 的定義矛盾。

因此,\(P\) 就是在這個基礎問題中 \(b=\text{str}(\text{lca}(x,y),x),a=s_k^R\) 的情況;\(Q\) 就是在這個基礎問題中 \(b=\text{str}(\text{lca}(x,y),y),a=s_k\) 的情況。

先對 \(s_k\) 以及 \(s_k^R\) 進行字尾排序。

最長的長度不超過 \(L\)\(\text{border}\) 很好求,由於要求的是某個字尾的 \(\text{border}\),在其反串上就變成了字首的 \(\text{border}\),這兩個問題也是等價的,和上面的證明類似。

因此,對於 \(s_k\)\(s_k^R\) 建立失配樹 \(T_k\)\(T_k^R\),並進行輕重鏈剖分。找到滿足條件的字尾後,先看一下它的反串對應的是哪一棵失配樹,然後在失配樹上一條一條重鏈向上跳。失配樹的根鏈是單調遞減的(從節點到根)。若鏈頂大於 \(L\),就整條鏈跳過,否則在鏈上二分,單次詢問時間複雜度為 \(\mathcal{O}(\log n)\)

接著考慮如何找到使得 \(L\) 最大的字尾。這個字尾一定滿足,它要麼是 \(a\) 中最大的字典序大小不超過 \(b\) 的字尾,要麼是 \(a\) 中最小的字典序大小個超過 \(b\) 的字尾。換句話說,設這兩個字尾與 \(b\)\(\text{lcp}\) 長度分別為 \(A,B\),則 \(L=\max\{A,B\}\)

接下來給出證明:

\(\bf{Proof\space 3}\)

設它們的排名分別為 \(i,j\)。則一定有 \(j=i+1\)。因為根據定義,排名為 \(i+1\) 的字尾字典序大小已經超過了 \(b\),但是排名在 \([1,i]\cup\mathbb{Z}\) 內的字尾字典序大小都不超過 \(b\)

考慮反證,假設排名為 \(\text{rnk}(\text{rnk}\ne i\land \text{rnk}\ne i+1)\) 的字尾會得到更大的 \(\text{lcp}\) 長度。

記這個更大的 \(\text{lcp}\) 長度為 \(\text{len}\)。分兩種情況討論:

  • \(\text{rnk}\in[1,i)\cup\mathbb{Z}\),則排名為 \(\text{rnk}\) 的字尾的前 \(\text{len}\) 位均與 \(b\) 的前 \(A\) 位相同。根據 \(\text{len}\) 的定義可知其第 \(A+1\) 位也與 \(b\) 的這一位相同。根據定義,排名為 \(i\) 的字尾的第 \(A+1\) 位小於 \(b\) 的這一位,或者說這一位不存在(空字元)。此時,排名為 \(i,\text{rk}\) 的兩個字尾前 \(A\) 位相同都等於 \(b\) 的前 \(A\) 位。且後者的第 \(A+1\) 位大於前者的這一位。說明後者比前者字典序大,這與 \(\text{rnk}\in[1,i)\cup\mathbb{Z}\) 矛盾。

  • \(\text{rnk}\in(i+1,|a|]\cup \mathbb{Z}\),與上一種情況類似推導得到字典序大小上的矛盾即可證明。

證畢。

於是考慮求得排名 \(i\)。由於經過字尾排序,即這些字尾的字典序遞增,所以答案有單調性,直接二分這個排名即可。

考慮如何求一條鏈上的字串和序列上的字串的最長公共字首長度。對原樹進行輕重鏈剖分,將邊權轉化為深度較深的端點的點權,則這條鏈會被表示成 \(\mathcal{O}(\log n)\) 條連續的重鏈區間。對於 \(\text{dfn}\) 序列形成的字串維護雜湊,對 \(s_k\)\(s_k^R\) 也維護雜湊。

一條一條重鏈匹配,若能全部匹配上,就算上這些長度,否則二分第一個不同的位置。只有第一條不匹配的重鏈需要二分,因此時間複雜度為 \(\mathcal{O}(\log n)\)

這部分細節比較多,尤其是一方匹配完的邊界情況,具體看程式碼中的 qlcp 部分。

此時,這個過程已經求出了 \(\text{lcp}\) 長度,順帶比較一下大小配合套在外面的二分。

那麼 \(P,Q\) 均被我們求出來了。

求出來之後,我們只需要考慮 \(x\rightsquigarrow \text{lca}(x,y)\) 的後 \(P\) 個位置為開頭處形成的匹配,因為不能匹配 \(s_k\) 更長的字首了。

記這 \(P\) 個位置依次為 \(u_1\sim u_P\),滿足它們按照 \(\text{lca}(x,y)\rightsquigarrow x\) 路徑上的順序排列;記後 \(Q\) 個位置依次為 \(v_1\sim v_Q\),滿足它們按照 \(\text{lca}(x,y)\rightsquigarrow y\) 路徑上的順序排列。

\(u_i\) 為開頭處能形成合法的匹配,當且僅當一下三點同時滿足:

  • \(s_k[1,P]\) 存在長度為 \(i\)\(\text{border}\)
  • \(s_k^R[1,Q]\) 存在長度為 \(|s_k|-i\)\(\text{border}\)
  • \(i\ne |s_k|\)

證明:

\(\bf{Proof \space 4}\)

  • 充分性:

    因為 \(i\ne |s_k|\),所以跨過了 \(\text{lca}(x,y)\)。因為 \(s_k[1,P]\) 存在長度為 \(i\)\(\text{border}\),根據 \(P\) 的定義可以得到 \(\text{str}(u_i,\text{lca}(x,y))=s_k[P-i+1,P]=s_k[1,i]\);因為 \(s_k^R[1,Q]\) 存在長度為 \(|s_k|-i\)\(\text{border}\),類似地,\(\text{str}(v_{|s_k|-i},\text{lca}(x,y))=s_k^R[1,|s_k|-i]\),根據反串的定義得到 \(\text{str}(\text{lca}(x,y),v_{|s_k|-1})=s_k[i+1,|s_k|]\)。兩者拼接恰好是 \(s_k\)

  • 必要性:

    \(u_i\) 開頭處可以形成合法的匹配,首先一定有 \(i\ne |s_k|\),因為要跨過 \(\text{lca}(x,y)\)。其次 \(\text{str}(u_i,\text{lca}(x,y))=s_k[1,i]\),根據 \(P\) 的定義,\(\text{str}(u_i,\text{lca}(x,y))=s_k[P-i+1,P]\),因此 \(s_k[1,i]=s_k[P-i+1,P]\),即 \(s_k[1,P]\) 存在長度為 \(i\)\(\text{border}\);類似地,\(\text{str}(v_{|s_k|-i},\text{lca}(x,y))=s_k^R[1,|s_k|-i]=s_k^R[Q-|s_k|+i+1,Q]\),因此 \(s_k^R[1,Q]\) 存在長度為 \(|s_k|-i\)\(\text{border}\)

證畢。

所以,我們要統計有多少 \(i\) 合法,就是要統計有多少 \(i\) 滿足這三個條件。

轉化成失配樹上的限制,就是要求有多少 \(i\) 滿足 \(i\)\(T_k\)\(P\) 的根鏈上,且 \(|s_k|-i\)\(T^R_k\)\(Q\) 的根鏈上。

考慮離線 + 掃描線。對於所有 \(s_k\) 的詢問,將它掛在 \(T_k\)\(P\) 節點上。考慮深度優先搜尋 \(T_k\),在過程中一併維護陣列 \(a_0\sim a_{|s_k|}\)。其中 \(a_j\) 表示有多少 \(i\),滿足:

  • \(i\)\(T_k\) 的當前搜到的點 \(u\) 的根鏈上。
  • \(|s_k|-i\)\(T_k^R\) 中點 \(j\) 的根鏈上。
  • \(i\ne |s_k|\)

則只要在 \(P\) 處單點查 \(a_Q\) 即可。

每次新掃到一個點 \(u\),則和上一層深搜相比根鏈上增加了一個點 \(u\) 的貢獻。考慮 \(|s_k|-u\) 的貢獻,此時首先滿足 \(u\ne |s_k|\),發現只有它子樹內的點的根鏈經過它,即只要這些點的 \(a_j\) 值要增加 \(u\) 的貢獻。拍平成 \(\text{dfn}\) 序後將 \(a\) 對映過去,再進行差分,則需要支援的操作形如區間加、單點查,由於此處修改、詢問同階,樹狀陣列維護即可。每次結束一個點的深搜時,刪去它的貢獻。

這部分就做完了,時間複雜度為 \(\mathcal{O}\left(n\log^2 n\right)\)


綜上,這個做法時空複雜度均為 \(\mathcal{O}(n\sqrt{n})\),可以接受。前面也說過空間可以做到線性,只是需要一些精湛的卡常技藝。

AC Link & Code

相關文章