字典与前缀差分

共 20 题
📑 题目列表 20 题
#
★★★

1. 前缀和与差分的对比应用中前缀和 O(1) 查询区间和但修改 O(n),差分 O(1) 区间加但查询 O(n),两者如何与树状数组结合?

请对比前缀和与差分的应用:前缀和 O(1) 查询区间和但修改 O(n),差分 O(1) 区间加但查询 O(n),并说明两者如何与树状数组结合?

  • 前缀和的查询快、修改慢特性
  • 差分的区间加快、查询慢特性
  • 树状数组同时支持区间加与区间查询

前缀和 pre[i] 表示前 i 项和,可用 O(1) 求任意区间和(pre[r]-pre[l-1]),但修改一个元素需 O(n) 更新前缀和数组。差分 d[i]=a[i]-a[i-1] 使区间 [l,r] 整体加 v 只需 d[l]+=v、d[r+1]-=v,O(1) 完成,但单点查询要累加差分前缀 O(n)。两者互补:查询密集用前缀和,修改密集用差分。树状数组(BIT)用差分思想把"区间加"变成两个单点更新,同时用 BIT 维护前缀和实现 O(log n) 区间查询,从而同时支持"区间加 + 区间查询"均为 O(log n)。具体地,用两个 BIT 分别维护 d[i] 与 i·d[i] 即可实现区间加、区间和查询。

前缀和与差分是"字面相反"的两种预处理,各自牺牲一个方向换取另一个方向的 O(1)。树状数组通过维护差分及其加权前缀,把两个方向的复杂度都降到 O(log n),是两者的统一与增强。

// 两个 BIT 实现区间加 + 区间和查询
void rangeAdd(int l, int r, long v) { add(bit1, l, v); add(bit1, r+1, -v); add(bit2, l, v*(l-1)); add(bit2, r+1, -v*r); }
long prefixSum(int x) { return sum(bit1, x) * x - sum(bit2, x); }
long rangeSum(int l, int r) { return prefixSum(r) - prefixSum(l-1); }
#
★★★

2. 前缀和的差分还原中给定差分数组如何恢复原数组,区间加如何用差分优化?

请说明如何由差分数组恢复原数组,以及区间加操作如何用差分数组优化?

  • 差分数组与原数组的还原关系
  • 区间加转化为差分的两点操作
  • 复杂度优势

给定差分数组 d,原数组 a[i] = a[i-1] + d[i](即 a[i] = Σ d[1..i]),所以对差分数组做前缀和即可还原原数组。区间 [l, r] 整体加 v 对应的差分操作是 d[l] += v、d[r+1] -= v:因为原数组在 [l,r] 内每个元素都加 v,等价于差分在 l 处多 v、在 r+1 处减 v。这样多次区间加只需 O(1) 修改差分,最后一次性前缀和还原,总复杂度从 O(n·m) 降到 O(n+m)。

差分是"区间加"的延迟化:把区间操作记录在两个端点上,最后统一前缀和得到结果。这是"打标记然后统一结算"的经典思想,也是第二类差分、树状数组等的基础。

int[] d = new int[n + 2];
for (int[] q : queries) { d[q[0]] += q[2]; d[q[1] + 1] -= q[2]; } // 每个区间加
for (int i = 1; i <= n; i++) a[i] = a[i-1] + d[i]; // 前缀和还原
#
★★★

3. 二阶差分中等差数列区间加如何用两次差分后两次前缀和还原,与普通区间加在标记点上的差异

请解释二阶差分:等差数列区间加如何用两次差分后两次前缀和还原,以及它与普通区间加在标记点上的差异?

  • 等差数列区间加的含义
  • 二阶差分与两次前缀和还原
  • 标记点(三个点)与普通区间加(两个点)的差异

若区间 [l,r] 内每个元素加上一个首项为 a、公差为 d 的等差数列,即 a[i] += a + (i-l)·d。普通差分对"等差数列"无能为力,因为一次差分后每个元素新增的值仍不同。做法是二阶差分:对原数组做一次差分得到 d1,再做一次差分得到 d2。对区间 [l,r] 加等差数列,等价于在一阶差分上区间 [l,r] 加常数 d(在 d1[l]+=d、d1[r+1]-=d)、在 d1[l] 处多加首项(a - d·l 之类),最终在二阶差分 d2 上只需在 l、l+1、r+1、r+2 四个(或 l、r+1 等)点打标记。还原时先对 d2 做一次前缀和得到 d1,再做一次前缀和得到原数组。与普通区间加(两个标记点)相比,二阶差分的标记点更多(通常 4 个),还原需要两次前缀和。

一阶差分解决"常数区间加",二阶差分解决"线性(等差数列)区间加"。每多一阶,就能处理高一阶的多项式,标记点增多、还原多一次前缀和。这是"差分阶数对应多项式次数"的规律。

#
★★★

4. 二维差分中矩形区域整体加 v 如何在四个角做 O(1) 标记、再经二维前缀和还原出完整矩阵

请解释二维差分:矩形区域整体加 v 如何在四个角做 O(1) 标记,再经二维前缀和还原出完整矩阵?

  • 二维差分数组的定义
  • 四个角的 O(1) 标记
  • 二维前缀和还原

二维差分数组 d 满足原数组 a[i][j] = Σ_{x≤i,y≤j} d[x][y],即 a 是 d 的二维前缀和。对矩形 (x1,y1)-(x2,y2) 整体加 v,只需在二维差分数组的四个角打标记:d[x1][y1]+=v、d[x2+1][y1]-=v、d[x1][y2+1]-=v、d[x2+1][y2+1]+=v。这样所有矩形内的点会累积 v,而矩形外的点因正负抵消为 0。最后对二维差分数组做二维前缀和(先按行、再按列累加)即可还原出完整矩阵。每个矩形操作 O(1),多次矩形加后一次性还原,总时间 O(n·m + k)(k 为矩形数)。

二维差分是"一维差分在高维的推广",核心仍是"在端点打标记、前缀和结算"。四个角分别对应矩形的四个边界,正负号由容斥原理决定(左上加、右上减、左下减、右下加)。

int[][] d = new int[n + 2][m + 2];
for (int[] rect : rects) {
    int x1=rect[0], y1=rect[1], x2=rect[2], y2=rect[3], v=rect[4];
    d[x1][y1] += v; d[x2+1][y1] -= v; d[x1][y2+1] -= v; d[x2+1][y2+1] += v;
}
// 二维前缀和还原
for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++)
    d[i][j] += d[i-1][j] + d[i][j-1] - d[i-1][j-1];
#
★★

5. 树上差分的标记规则中为什么路径点差分在 u、v 处 +1、LCA 处 -1、LCA 的父节点再 -1,边差分与点差分有何不同?

请解释树上差分的标记规则:为什么路径点差分在 u、v 处 +1、LCA 处 -1、LCA 的父节点再 -1,以及边差分与点差分的不同?

  • 路径点差分的标记规则
  • LCA 与差值累计的推导
  • 边差分与点差分的差异

树上点差分:对路径 u-v,每个节点被访问次数可用差分标记后 DFS 求和得到。标记为 cnt[u]++、cnt[v]++、cnt[lca]--、cnt[parent(lca)]--。推导:以根为基准做"子树贡献"求和,每个节点 node 的最终值 = 其子树内所有点 +1 贡献之和。这样 u 和 v 分别贡献到它们到根的路径,lca 被加了两次(u 和 v 各一次)需减一次,parent(lca) 之上会多算一次,因此再减一次。DFS 自底向上累加子树和即可得到每个节点的真实访问次数。边差分(统计每条边被覆盖次数):标记 cnt[u]++、cnt[v]++、cnt[lca]-=2,然后 DFS 求和,每条边(子节点方向)的真实值 = 子树和。与点差分不同,边差分只减 lca 一次(-2),不需要动 parent(lca),因为边不包含 lca 自身。

点差分的本质是"让 u、v 到根路径上的点都 +1,再在 lca 处修正重复"。边差分把"点"换成"边"后,lca 自身不参与,因此修正点在 lca 处一次性 -2。根是"子树求和"的结算基准。

#
★★

6. LZW 压缩的最坏退化中重复性差的输入会使码本增长失控、压缩率劣于固定编码,字典满时的清空/固定策略如何处理?

请解释 LZW 压缩的最坏退化:为什么重复性差的输入会使码本增长失控、压缩率劣于固定编码,以及字典满时的清空/固定策略如何处理?

  • LZW 字典增长的机制
  • 重复性差输入的退化
  • 字典满时的处理策略

LZW 动态构建字典,把已出现的字符串组合编码成新码。当输入重复性好时,字典中的长串被频繁复用,压缩率高;但当输入重复性差(几乎无重复)时,每个新字符都产生新码,码本快速增长,而每个码字长度固定(如 12 位),输出码流长度接近甚至超过输入,压缩率劣于固定编码(甚至可能膨胀)。字典满时(码字位数到上限),常见策略:1)清空字典重建(reset),重新开始动态编码,适合重复性变化的输入;2)固定字典(停止增长),保留已有码字继续用,适合字典已捕获足够规律的输入;3)动态调整码字位数。选型取决于数据分布。

LZW 的前提是"输入有自相关/重复性",字典能捕获重复模式。重复性差时字典贡献的压缩收益为负,清空或固定字典是应对字典满的两种主要工程策略。

#
★★

7. 逆 BWT 的 LF-mapping 原理中为什么用 Last 列的字符计数与 rank 查询就能逐字符还原原串,与前缀和/计数排序思想的关系?

请解释逆 BWT 的 LF-mapping 原理:为什么用 Last 列的字符计数与 rank 查询就能逐字符还原原串,并说明它与前缀和/计数排序思想的关系?

  • BWT 的 Last 列与字符计数
  • LF-mapping 的定义
  • 与前缀和/计数排序的关系

BWT 把原串的所有循环移位排序,Last 列是每个移位末字符。逆 BWT 的关键是 LF-mapping:原串中位置 i 的字符,在 Last 列中的位置映射回 First 列(排序后的字符列)的对应位置。若知道 Last 列中每个字符的前缀计数(C 数组)及每个位置的 rank(该位置在 Last 列中该字符出现的第几次),则 LF(i) = C[Last[i]] + rank(Last[i], i)。从任意位置(如 '$' 所在行)出发,反复应用 LF 即可逐字符还原原串。这本质上就是计数排序/前缀和思想:C 数组是前缀和(每个字符的起始位置),rank 是在该字符段内的偏移,二者结合确定字符在排序列中的位置。逆 BWT 实际在"按字符做桶排序"。

LF-mapping 是逆 BWT 的数学核心,它把"循环移位排序"这个结构翻译成"前缀和 + rank"的索引运算。C 数组(前缀和)与 rank(桶内偏移)正是计数排序的两个要素,因此逆 BWT 与计数排序、前缀和思想一脉相承。

#
★★

8. 前缀异或与区间查询中如何用前缀异或数组 O(1) 求任意区间异或?

请说明如何用前缀异或数组 O(1) 求任意区间的异或值?

  • 前缀异或数组的定义
  • 区间异或的推导(自反性)
  • 常见应用

设前缀异或 pre[i] = a[0] ^ a[1] ^ ... ^ a[i-1](或 pre[i] = a[1]^...^a[i])。任意区间 [l, r] 的异或 = pre[r] ^ pre[l-1]。因为异或满足自反性(x^x=0),pre[r] 包含 [0..r-1] 的异或,pre[l-1] 包含 [0..l-2] 的异或,两者异或后 [0..l-2] 的部分被自己抵消,只剩 [l-1..r-1] 的异或,即区间 [l,r] 的异或。预处理 O(n)、查询 O(1)。应用包括连续数组异或、子数组异或查询、"只出现一次的数字"等。

前缀异或是前缀和思想在异或(非数性代数)下的推广。异或的"自反性"使得"减去前缀"变成"异或前缀",从而 O(1) 求区间异或。它比前缀和更简洁,因为无需考虑减法与进位。

int[] pre = new int[n + 1];
for (int i = 1; i <= n; i++) pre[i] = pre[i - 1] ^ a[i];
int rangeXor(int l, int r) { return pre[r] ^ pre[l - 1]; } // O(1)
#
★★

9. 前缀异或的应用中给定数组求最大异或子段,如何用前缀异或加 01-Trie 在 O(n log A) 内完成?

请说明前缀异或的应用:给定数组求最大异或子段,如何用前缀异或加 01-Trie 在 O(n log A) 内完成?

  • 最大异或子段转化为前缀异或对
  • 01-Trie 的逐位贪心
  • 复杂度 O(n log A)

求最大异或子段等价于求两个前缀异或 pre[i] 与 pre[j](i<j)异或的最大值,因为子段 [i+1..j] 的异或 = pre[j] ^ pre[i]。于是问题转化为"在一组数中找两个数异或最大"。用 01-Trie(按二进制位建树)实现:把每个前缀异或插入 Trie,查询时对每个 pre[i] 沿位从高到低贪心选择相反位(若存在)以最大化异或,从而 O(log A) 查询。整体:遍历每个前缀异或,先查询再插入,总复杂度 O(n log A),A 为数值范围(位数)。

关键两步:1)用前缀异或把"子段"转化为"前缀对",消除区间维度;2)用 01-Trie 的逐位贪心在 O(log A) 内求最大异或对。这是"前缀思想 + 树形结构"的经典组合。

int ans = 0, pre = 0;
Trie trie = new Trie(); trie.insert(0);
for (int x : a) {
    pre ^= x;
    ans = Math.max(ans, trie.maxXor(pre));
    trie.insert(pre);
}
#
★★

10. 前缀差分/前缀和的扩展中二维前缀和、树上差分在区间统计问题中的应用?

请说明前缀差分/前缀和的扩展:二维前缀和、树上差分在区间统计问题中的应用?

  • 二维前缀和求矩形和
  • 树上差分求路径覆盖
  • 扩展的应用场景

二维前缀和预处理后可 O(1) 求任意矩形区域的和(四个角组合),用于图像区域统计、矩阵矩形和查询、区域计数等。树上差分把"路径上的计数/覆盖"转化为"点或边的差分标记 + DFS 求和",用于统计每条路径被哪些操作覆盖、路径问题(如点权增量、边权增量)的批量处理。二维前缀和的扩展还包括二维差分(矩形加)、三维前缀和等。这些扩展把一维的"前缀和/差分"思想推广到高维与树形结构,解决"区间/区域/路径"的批量统计与查询。

前缀和/差分的灵魂是"预处理 + 递推求和",高维与树形只是把"区间"换成"矩形"或"路径",用容斥(二维)或 LCA 修正(树上)实现 O(1)/O(子树) 的批量处理。

#
★★

11. LZ77 滑窗与 LZ78/LZW 字典的对比中指针引用 vs 字典码的编解码方式与流式特性差异

请对比 LZ77 滑窗与 LZ78/LZW 字典的编解码方式与流式特性差异?

  • LZ77 的滑窗指针引用
  • LZ78/LZW 的字典码
  • 流式编码与解码特性

LZ77 用"滑窗"记录最近一段文本,把重复内容编码为"距离+长度"指针引用(指向窗口内某位置的一段),解码时依赖窗口内已解码内容,因此需要窗口缓冲,编码解码都需维护窗口。LZ78/LZW 用动态字典,把已出现的字符串编成字典码字,编码时查字典、解码时反向重建字典,由于字典是"从已有前缀重建",LZW 需特殊处理(解码每步补全字典)。流式特性:LZ77 的窗口限制使引用距离受限,解码是流式的(只需窗口内数据);LZ78/LZW 的字典无限增长,解码也流式但依赖不断增长的字典。LZ77 适合文本流式(越近的重复越相关),LZ78/LZW 字典可在任意上下文复用。

两者都是"用历史信息压缩重复"的 LZ 家族,区别在引用方式:LZ77 用"位置+长度"(局域滑窗),LZ78/LZW 用"字典码"(全局字典)。流式与内存占用也随之不同。

#

12. AC 自动机上的模式计数中为什么文本扫描只需在节点上计数,再用 fail 树子树求和(差分)即可得到每个模式的出现次数?

请解释 AC 自动机上的模式计数:为什么文本扫描只需在节点上计数,再用 fail 树子树求和(差分)即可得到每个模式的出现次数?

  • 扫描时节点的计数
  • fail 树子树求和的含义
  • 差分思想的复用

扫描文本时,每到达一个节点就对该节点计数 +1(表示该状态被访问一次)。某模式 p 的出现次数,等于"所有以 p 为后缀的访问状态"的计数之和。由于"p 是某状态的后缀"等价于"p 节点在该状态节点沿 fail 链的祖先上",即 p 节点在该状态节点的 fail 树祖先上;反过来,状态节点属于 p 节点在 fail 树上的子树。因此 p 的出现次数 = p 节点在 fail 树上的子树内所有节点计数之和。用 DFS 序把子树映射为区间,做子树求和(或差分)即可得到每个模式的出现次数,无需逐模式重新扫描。

这是把"匹配过程中的状态访问"与"fail 树的结构"结合:扫描时只对访问节点计数,最后用 fail 树子树求和(本质是树上的差分/前缀和)汇总出每个模式的出现次数。一次扫描 O(n),汇总 O(总长度)。

#

13. 集合覆盖贪心 ln n 近似比的证明中每次选覆盖最多未覆盖元素的集合,用调和级数 H(n) 界定近似比

请说明集合覆盖贪心 ln n 近似比的证明思路:每次选覆盖最多未覆盖元素的集合,如何用调和级数 H(n) 界定近似比?

  • 集合覆盖贪心策略
  • 近似比 ln n 的证明
  • 调和级数 H(n) 的界定

集合覆盖贪心每一步选择能覆盖最多"尚未覆盖元素"的集合。证明近似比为 ln n:设最优解用 OPT 个集合覆盖全部 n 个元素。观察贪心过程中,当还存在 k 个未覆盖元素时,最优解中参与覆盖这 k 个元素(至少一个)的集合平均每个能覆盖至少 k/OPT 个未覆盖元素(因为最优解用 OPT 个集合覆盖全部,其中覆盖这 k 个元素的集合的覆盖量之和 ≥ k)。因此贪心一步至少覆盖 k/OPT 个元素,即未覆盖数从 k 降到 k(1-1/OPT)。反复执行,未覆盖数呈指数衰减,最多经过 OPT·ln n 步能覆盖全部。因此贪心解的集合数 ≤ OPT·(1 + 1/2 + ... + 1/n) = OPT·H(n) ≈ OPT·ln n,即近似比 ≤ H(n) ≈ ln n。

证明的关键是"每步至少覆盖剩余未覆盖数的 1/OPT 比例",从而未覆盖数指数衰减,步数上界为 OPT·ln n。调和级数 H(n) = Σ 1..n 1/i ≈ ln n,提供了最终近似比上界。

#

14. 字典树的应用中自动补全、最大异或对(插入+逐位贪心)与 AC 自动机的关系?

请说明字典树(Trie)的应用:自动补全、最大异或对(插入+逐位贪心)与 AC 自动机的关系?

  • Trie 的自动补全
  • 01-Trie 求最大异或对
  • Trie 与 AC 自动机的关系

Trie 树(字典树)按字符路径组织字符串集合,支持前缀匹配。应用:1)自动补全:从输入前缀沿 Trie 走到节点,遍历该节点子树即可得到所有候选词;2)最大异或对:把数字按二进制位插入 01-Trie,查询时逐位贪心选择相反位以最大化异或;3)AC 自动机:在 Trie 基础上加 fail 指针,使失配时沿 fail 跳转继续匹配,实现多模式匹配,是"Trie + fail 指针"的组合。因此 Trie 是自动补全、01-Trie 异或、AC 自动机的基础数据结构。

Trie 的核心价值是"共享前缀、按前缀检索"。自动补全利用前缀遍历,01-Trie 利用逐位贪心,AC 自动机利用前缀结构加失配跳转,都是对 Trie 不同维度能力的利用。

#

15. 字典树的工程实现中为什么竞赛与生产常用静态数组(next[N][26])代替指针节点,空间与缓存上的差异?

请说明字典树的工程实现:为什么竞赛与生产常用静态数组(next[N][26])代替指针节点,以及空间与缓存上的差异?

  • 静态数组 vs 指针节点的实现
  • 空间与缓存局部性
  • 内存管理与性能

静态数组实现用 next[N][26](N 为节点数上限,26 为字符集),每个节点一行,用整数索引代替指针。相比指针节点(每个节点含多个指针/对象),静态数组的优势:1)无指针/对象开销,直接连续内存,缓存友好、访问局部性好;2)避免频繁 new 对象与 GC 压力;3)索引运算比指针解引用快,遍历与插入更高效。缺点是需要预分配 N·26 空间,字符集大时浪费。竞赛与生产在字符集固定且较小(如小写字母)时,静态数组是首选;字符集大(如 Unicode)时退化为稀疏表或哈希实现。

静态数组用"连续内存 + 整数索引"换取缓存局部性与零 GC 开销,代价是预分配空间。这是"空间换时间/可预测性"在工程中的典型。

#

16. 迭代加深(IDA*)的复杂度中为什么每轮加深会重复搜索前几层,但在分支因子 b 较大时总代价仍是首层搜索的约 b/(b-1) 倍?

请解释迭代加深(IDA*)的复杂度:为什么每轮加深会重复搜索前几层,但在分支因子 b 较大时总代价仍是首层搜索的约 b/(b-1) 倍?

  • IDA* 的逐层加深
  • 重复搜索的代价
  • 分支因子 b 下的总代价上界

IDA* 逐步增加深度限制,每轮都从根重新搜索,因此前几层会被重复搜索多次。但每层搜索的节点数按分支因子 b 指数增长:第 d 层有约 b^d 个节点。若最终在第 D 层找到解,则总搜索代价 ≈ b^0 + b^1 + ... + b^D = (b^{D+1}-1)/(b-1)。而单独做一次深度 D 的搜索代价是 b^D。两者之比 ≈ b/(b-1)。当 b 较大时,b/(b-1) 接近 1,即重复搜索的额外开销很小(前几层相对浅层占比可忽略),因此 IDA* 总代价约为首层搜索(深度 D 层)的 b/(b-1) 倍,复杂度仅比 DFS 多常数因子。

关键是指数增长使"浅层重复"被"深层主导"淹没:浅层节点数相对深层可忽略。b/(b-1) 倍是几何级数求和的直接结果,解释了 IDA* 在分支因子大时"重复搜索不贵"的直觉。

#

17. 字典树(Trie)的插入/查询/前缀统计,与哈希表相比在"前缀匹配"场景的优势?

请说明字典树(Trie)的插入、查询、前缀统计操作,以及它与哈希表相比在"前缀匹配"场景的优势?

  • Trie 的插入/查询/前缀统计
  • 前缀匹配的天然支持
  • 与哈希表的对比

Trie 插入 O(L)(L 为串长),查询某个串是否完全存在 O(L),前缀统计(统计有多少串以某前缀开头)可在每个节点维护 count,O(L) 完成。哈希表支持 O(L) 的插入与精确查询,但无法高效支持"前缀匹配"(如找所有以 "ab" 开头的串),因为哈希表按整个串哈希,无法按前缀组织。Trie 按字符共享路径,天然支持前缀遍历与前缀计数,牺牲一定空间(每节点多子指针)换取前缀能力。因此"前缀匹配/自动补全/前缀统计"场景 Trie 明显优于哈希表。

Trie 的"共享前缀"结构正是前缀匹配的对应物。哈希表擅长精确查找,Trie 擅长前缀检索,两者能力互补,选型取决于是否有前缀需求。

#

18. 近似比下界与 PCP 定理中 MAX-3SAT 不存在 (7/8+ε)-近似除非 P=NP,PCP 定理如何推出不可近似性

请解释近似比下界与 PCP 定理:为什么 MAX-3SAT 不存在 (7/8+ε)-近似除非 P=NP,以及 PCP 定理如何推出这类不可近似性?

  • MAX-3SAT 与随机赋值 7/8 近似
  • PCP 定理在近似算法下界中的作用
  • 不可近似性的推导逻辑

MAX-3SAT 是"最大化被满足子句数"的问题。随机赋值的期望满足 7/8 的子句,因此存在 7/8-近似算法(随机化)。但研究表明,除非 P=NP,不存在 (7/8+ε)-近似算法(对任意 ε>0)。这个下界由 PCP 定理(概率可检验证明)推出:PCP 定理把 NP 问题转化为"可被随机读取 O(log n) 个位置验证的证明",而 Håstad 的 PCP 构造使 MAX-3SAT 的近似判定与 NP 完全性挂钩,从而推出满足超过 7/8 比例的判定已经 NP-hard。直观上,PCP 定理能把"某个解是否满足"的验证与"随机少量采样"结合,精确刻画近似界限,从而证明某些近似比(如 7/8)是紧的。

PCP 定理是近似算法下界(不可近似性)的基石。它把 NP 完全性的判定以"高概率随机验证"的方式编码,从而能证明"若存在超过某阈值的近似算法,则 NP=P"。7/8 是 MAX-3SAT 的近似上限。

#

19. 字典树的变体中 01-Trie 求最大异或对与可持续化 Trie 的历史版本?

请说明字典树的变体:01-Trie 求最大异或对,以及可持续化(可持久化)Trie 如何支持历史版本?

  • 01-Trie 的构建与逐位贪心
  • 可持久化 Trie 的版本共享
  • 历史版本查询

01-Trie 是字典树在二进制位上的变体:把每个数的二进制位从高位到低位作为路径,插入所有数,查询某个数时从高位逐位贪心选择相反位(若存在)以最大化异或,O(log A) 完成,可用于求最大异或对、最大异或子段等。可持久化 Trie 在每次插入时创建新版本,但新版本与旧版本共享未修改的子树(路径复制),只复制被修改的路径节点,从而空间 O(n log A),可查询"某个历史版本"的状态,或支持"区间内查询"(如 [l,r] 内最大异或对,用两个版本作差)。它把"版本历史"叠加到 Trie 结构上。

01-Trie 把"数值"编码为"位路径",用逐位贪心求异或极值;可持久化 Trie 用"路径复制共享"实现版本化,使前缀/区间查询成为可能。两者是 Trie 在"位"与"时间"两个维度的扩展。

#

20. 字符频次前缀数组中预处理每种字符出现次数前缀后如何 O(1) 判断任意子串的字母构成与异位词关系

请说明字符频次前缀数组:预处理每种字符出现次数前缀后,如何 O(1) 判断任意子串的字母构成与异位词关系?

  • 字符频次前缀数组的构建
  • 任意子串频次向量的 O(1) 获取
  • 异位词判断

对字符串预计算频次前缀数组 cnt[c][i] = 字符 c 在前 i 个字符中出现的次数。任意子串 [l, r] 中字符 c 的出现次数 = cnt[c][r] - cnt[c][l-1],O(1) 得到该子串的完整频次向量。两个子串是异位词(字母构成相同)当且仅当它们的频次向量逐字符相等。因此可 O(1) 判断任意两个子串是否异位词(或判断某子串的字母构成是否满足某要求)。空间 O(26·n)(对小写字母),预处理 O(26·n),查询 O(26)(或 O(1) 若用哈希)。

这是"前缀和"思想在字符频次上的应用:把"字母构成"编码为频次向量,用前缀差 O(1) 提取任意子串的频次,从而判断异位词与构成关系。适合"子串字母构成"类问题。

int[][] cnt = new int[26][n + 1];
for (int i = 1; i <= n; i++)
    for (int c = 0; c < 26; c++) cnt[c][i] = cnt[c][i-1] + (s.charAt(i-1)-'a' == c ? 1 : 0);
boolean isAnagram(int l, int r, int l2, int r2) {
    for (int c = 0; c < 26; c++) if (cnt[c][r]-cnt[c][l-1] != cnt[c][r2]-cnt[c][l2-1]) return false;
    return true;
}