P9576 「TAOI-2」Ciallo~(∠・ω< )⌒★ 解题报告


P9576 「TAOI-2」Ciallo~(∠・ω< )⌒★ 解题报告

1. 题目解读:我们要干什么?

首先,我们来弄清楚题目到底要求我们计算什么。

题目描述了一个两步的“造词”过程:

  1. 第一步(删除): 从模板串 s 中删掉一个连续的子串 s[l...r],得到一个新的、更短的字符串 s'
  2. 第二步(提取): 从新字符串 s' 中再选出一个子串 s''

我们的目标是,找出总共有多少种不同的方案(即 l, r, l', r' 的组合),可以使得最终造出的词 s'' 正好是目标串 t

换个角度思考:
这个过程有点绕。我们可以简化一下思考方式。第一步删除操作,实际上是保留了 s 的一个前缀 s[1...l-1] 和一个后缀 s[r+1...|s|],然后把它们拼接起来。
所以,整个问题等价于:

核心问题: 我们可以从 s 中选择任意一个前缀和任意一个后缀(只要它们不重叠,即删除的中间部分至少有一个字符),将它们拼接成新串 s'。我们需要计算,在所有可能拼接出的 s' 中,目标串 t 一共出现了多少次。

例如,s = aabbaaba, t = aba
一种方案是删除 s[3...4] (即 bb),s 的前缀 aa 和后缀 aaba 拼接成 s' = aaaabat="aba"s' 中出现了 2 次。
我们要求的就是所有删除方案产生的 t 的出现次数的总和。

2. 整体思路:分类讨论

直接枚举所有删除方案 (l, r) 再去新串里找 t,效率太低。我们不妨换个思路:t 的角度出发

一个 t 的出现,在拼接后的 s' 中,有三种可能性:

  1. 完全来自前缀: ts 的某个前缀 s[1...l-1] 的一个子串。
  2. 完全来自后缀: ts 的某个后缀 s[r+1...|s|] 的一个子串。
  3. 横跨前后缀: t 的一部分来自前缀的末尾,另一部分来自后缀的开头。

我们可以分别计算这三种情况的方案数,然后加起来。

3. 算法详解

3.1. 情况一和情况二:t 未横跨拼接点

这种情况相对简单。我们改变求和的顺序:不先枚举删除的区间,而是先在原串 s 中找到一个 t 的出现。

假设 ts 中出现,起始于位置 p,结束于位置 p + |t| - 1

  • 要让这个 t 完整地保留在前缀部分,我们必须删除它右边的任意一段区间 s[l...r]。这意味着 l 必须大于 p + |t| - 1。有多少种这样的删除方案呢?
    • l 可以取 p + |t||s|。对于每个 lr 可以取 l|s|。总方案数是 (|s| - (p+|t|) + 1) + ... + 1,这是一个等差数列求和,结果是 k(k+1)/2,其中 k = |s| - (p+|t|-1)
  • 同理,要让这个 t 完整地保留在后缀部分,我们必须删除它左边的任意一段区间。这意味着 r 必须小于 p
    • r 可以取 1p-1。对于每个 rl 可以取 1r。总方案数是 1 + 2 + ... + (p-1),结果是 (p-1)p/2

所以,我们只需要在 s 中找到所有 t 的出现位置 p,对每个 p 把这两种情况的方案数累加到总答案 ans 中即可。找 t 的位置可以用字符串哈希O(|s|) 时间内完成。这部分对应了代码的最后几行。

3.2. 情况三:t 横跨拼接点(核心难点)

这是最复杂,也是本题的精髓所在。

目标串 t 是由模板串 s 的两部分拼接而成的。我们找到了这样的充要条件:

存在一对 (i, j) 满足 i < j,以及一个分割长度 p (1 <= p < |t|),使得:

  1. s 中以 i 结尾的长度为 p 的子串,等于 t 的长度为 p 的后缀。
  2. s 中以 j 开头的长度为 |t|-p 的子串,等于 t 的长度为 |t|-p 的前缀。

为了方便计算,我们预处理了两个数组(使用题解代码中的定义):

  • L[k]: s 中以 k 结尾的子串,能与 t最长后缀匹配的长度。
  • R[k]: s 中以 k 开头的子串,能与 t最长前缀匹配的长度。

对于一个固定的 (i, j) 对(i < j),它能贡献的方案数,就是满足 p <= L[i]|t|-p <= R[j]p 的数量。
将第二个不等式变形得到 p >= |t| - R[j]
所以,合法的 p 的取值范围是 [|t| - R[j], L[i]]
这个区间的长度(即 p 的取值数量)为 max(0, L[i] - (|t| - R[j]) + 1)

我们的总目标就是计算这个总和:

\[\text{Ans} = \sum_{1 \le i < j \le |s|} \max(0, L[i] - (|t| - R[j]) + 1) \]

这个公式很难直接处理,所以我们只对满足 L[i] \ge |t| - R[j](i, j) 对求和:

\[\text{Ans} = \sum_{i < j, \text{ s.t. } L[i] \ge |t| - R[j]} (L[i] - (|t| - R[j]) + 1) \]

现在,我们把括号拆开,就得到了你提到的那个公式:

\[\text{Ans} = \underbrace{\sum_{i < j, \text{ s.t. } L[i] \ge |t| - R[j]} (L[i] + 1)}_{\text{Part A}} - \underbrace{\sum_{i < j, \text{ s.t. } L[i] \ge |t| - R[j]} (|t| - R[j])}_{\text{Part B}} \]

我们的任务就是用扫描线和数据结构来分别求出 Part APart B

扫描线 + 双树状数组解法

我们将 j1 遍历到 |s|,把 j 看作是扫描线。在处理每个 j 的时候,我们想一次性算出所有 i < jj 产生的贡献。

核心思想:
当扫描线在 j 时,j 是一个查询点。我们需要查询所有已经处理过的 i(即 i < j)的信息。查询完毕后,j 就变成了一个数据点,需要将其信息加入我们的数据结构中,供后续的 j'j' > j)查询。

1. 计算 Part B

Part B 的求和项是 |t| - R[j]。对于一个固定的 j,这个值是常数。所以 Part Bj 这一步的贡献可以写作:

\[(|t| - R[j]) \times (\text{满足 } i < j \text{ 且 } L[i] \ge |t| - R[j] \text{ 的 } i \text{ 的数量}) \]

这下问题就清晰了:我们需要一个数据结构,能快速查询满足某个条件的 i数量

数据结构: 我们用一个树状数组(Fenwick Tree),命名为 BIT_count

  • 用途BIT_count 用来维护 L[i] 值的分布。它的下标范围是 [0, |t|]
  • 定义BIT_countk 处的值,表示已经处理过的 i 中,L[i] = k 的有多少个。

扫描过程(for Part B):
当我们处理到 j 时:

  1. 查询:我们需要找到满足 L[i] >= |t| - R[j]i 的数量。这等价于在 BIT_count 中查询下标在 [|t| - R[j], |t|] 这个范围内的值的总和。
    count = BIT_count.query_range(|t| - R[j], |t|)
  2. 计算贡献jPart B 的贡献就是 (|t| - R[j]) * count
  3. 累加到总答案Total_Part_B += (|t| - R[j]) * count

2. 计算 Part A

Part A 的求和项是 L[i] + 1。它依赖于 i
我们需要一个数据结构,能快速查询满足条件的 iL[i] + 1总和

数据结构: 我们再用一个树状数组,命名为 BIT_sum

  • 用途BIT_sum 用来维护 L[i] + 1 的和的分布。它的下标范围也是 [0, |t|]
  • 定义BIT_sumk 处的值,表示所有已处理的、且 L[i] = ki,它们的 L[i] + 1 的总和。

扫描过程(for Part A):
当我们处理到 j 时:

  1. 查询:我们需要找到满足 L[i] >= |t| - R[j] 的所有 iL[i] + 1 的总和。这等价于在 BIT_sum 中查询下标在 [|t| - R[j], |t|] 这个范围内的值的总和。
    sum_val = BIT_sum.query_range(|t| - R[j], |t|)
  2. 计算贡献jPart A 的贡献就是 sum_val
  3. 累加到总答案Total_Part_A += sum_val

3. 整合算法流程

现在我们把两部分合在一起。

预处理阶段:

  1. 用字符串哈希和二分,计算出 L[1...|s|]R[1...|s|] 数组。复杂度 O(|s| log |t|)

扫描线阶段:

  1. 初始化总答案 Ans = 0

  2. 初始化 BIT_countBIT_sum,所有值为 0。

  3. 循环 j1|s|
    a. 查询步骤 (计算 j 的贡献):
    i. 令 y = |t| - R[j]。这是一个阈值。
    ii. 从 BIT_sum 中查询区间 [y, |t|] 的和,得到 current_part_A
    iii. 从 BIT_count 中查询区间 [y, |t|] 的和,得到 current_count
    iv. 计算 current_part_B = y * current_count
    v. 将 j 的贡献累加到总答案:Ans += current_part_A - current_part_B

    b. 更新步骤 (将 j 作为数据点加入):
    i. 令 x = L[j]
    ii. 更新 BIT_count:在 x 位置加上 1BIT_count.add(x, 1)
    iii. 更新 BIT_sum:在 x 位置加上 x + 1BIT_sum.add(x, x + 1)

  4. 循环结束后,Ans 就是情况二的总方案数。

总复杂度: 预处理是 O(|s| log |t|)。扫描线部分,循环 |s| 次,每次在树状数组上进行常数次查询和更新,每次操作复杂度 O(log |t|)。所以扫描线部分也是 O(|s| log |t|)。总复杂度为 O(|s| log |t|)

总结

这种“双树状数组”的方法是解决这类二维计数求和问题的“标准模板”。它思路清晰,将复杂的求和式拆解成两个更简单、可以用数据结构维护的部分(一个求数量,一个求总和)。

posted @ 2025-07-16 11:00  surprise_ying  阅读(15)  评论(0)    收藏  举报