bitset 解决高维偏序连边的 DAG 点权最短路问题

当点权非负时对于任意 DAG 都可以用 bitset 优化 Dijkstra 做到平凡的 \(\mathcal{O}(\frac{n^2}{w}+n\log n\)

乐零老师今天教我了一种对于高维偏序图可以无论正负点权都能做到 \(O(\frac{n^2}{w}+n^{1.5})\) 的算法/崇拜

特此记录下,懒得写题解了,写了这个题,以下是 Gemini 3 Pro 对代码生成的题解:

这份代码使用了 分块 (Square Root Decomposition) 结合 bitset 优化 的非传统做法来解决四维偏序(最长不下降子序列)问题。

相比于传统的 CDQ 分治或 KD-Tree,这种做法在 \(N=50000\) 的数据规模下,利用计算机位运算的并行特性,往往能跑出非常优秀的常数,且代码逻辑相对线性,不易写挂(虽然逻辑比较绕)。

以下是为你生成的题解。


P3769 [CH弱省胡策R2] TATT 题解

算法核心:分块 + Bitset 优化 DP

1. 问题转化

题目要求求四维空间中的最长不下降路径。令 \(dp[i]\) 表示以点 \(i\) 结尾的最长路径长度。
状态转移方程为:

\[dp[i] = \max(\{dp[j] \mid \forall k \in \{0,1,2,3\}, coord_k(j) \le coord_k(i)\} \cup \{0\}) + 1 \]

这是一个典型的四维偏序优化 DP 问题。

2. 核心思路

通常这类问题使用 CDQ 分治套树状数组(\(O(N \log^3 N)\))或者 KD-Tree。但本题解采用了一种根号分块结合 Bitset 的“黑科技”做法。

算法主要分为两部分:

  1. 拓扑序排序:首先将所有点按照四维坐标进行字典序排序。这保证了在计算 \(dp[i]\) 时,所有潜在的前驱 \(j\) 一定在 \(i\) 之前被遍历到(或者在同层循环中)。
  2. 分块维护值域
    由于维数高达 4 维,难以建立树形结构。我们利用 bitset 维护DP 值的存在性
    • 我们将时间(处理顺序)分块,块大小设为 \(B \approx \sqrt{N}\)
    • 每处理完 \(B\) 个点,我们重构一次辅助数据结构。

3. 数据结构设计

为了快速查询“在所有四个维度都小于当前点 \(u\) 的点中,\(dp\) 值的最大值是多少”,我们维护以下信息:

  • 离散化与置换数组 (p[4]):
    对每一维的坐标进行离散化。p[o][v] 存储的是第 \(o\) 维坐标排名为 \(v\) 的点的原始编号。这允许我们快速访问“第 \(o\) 维最小的 \(k\) 个点”。

  • 动态排名的 DP 值 (rank, g, pre):
    这是本代码最精妙的地方。因为我们需要求 max,而 bitset 只能处理 bool(存在性)。

    • 在每轮重构时,我们将当前所有已计算出的 \(dp\) 值收集起来,从大到小排序去重得到数组 g
    • 我们用 bitset 的第 \(k\) 位为 \(1\),表示“存在一个点的 \(dp\) 值等于 g[k]”。
    • pre[o][id]:这是一个 bitset。它表示在第 \(o\) 维的坐标排序中,前 \(id\) 个块(每个块大小 \(B\))所包含的所有点中,拥有的 \(dp\) 值集合。

4. 查询过程 (bitset 求交集)

在计算点 \(u\)\(dp[u]\) 时,我们将候选点分为三类:

  1. “整块”内的点
    对于每一维 \(o\),点 \(u\) 的坐标 \(a[o][u]\) 覆盖了第 \(o\) 维排序数组的前若干个完整块。
    我们取出这四个维度对应的预处理 bitset: pre[o][a[o][u]/B]
    将这 4 个 bitset 进行 按位与 (&) 操作。结果 S 中如果第 \(k\) 位是 \(1\),说明存在一个点 \(v\),它在四个维度上都在 \(u\) 的“整块”范围内,且 \(dp[v] = g[k]\)
    由于 g 是降序排列的,我们只需要找到 S 中第一个为 \(1\) 的位 (S._Find_first()),对应的就是最大的 \(dp\) 值。

  2. “散块”内的点
    对于每一维,坐标 \(a[o][u]\) 可能落在某个块的中间。该块开头到 \(a[o][u]\) 之间的点没有被包含在 pre 中。我们暴力遍历这部分点,如果它们满足四维偏序关系 (leq),则尝试更新 \(dp[u]\)

  3. 当前块内刚处理过的点
    自从上次重构以来,新处理的 \(B\) 个点还没有进入 pre 数组。我们在拓扑序数组 q 中暴力遍历从上一个整块结束位置到 \(u\) 之间的点,进行检查和更新。

5. 复杂度分析

  • 时间复杂度

    • 重构部分:共进行 \(N/B\) 次。每次重构需要遍历所有点填充 bitset,以及对 DP 值排序。由于 Bitset 操作是 \(O(N/w)\) 的(\(w=64\)),重构总耗时约为 \(O(\frac{N^2}{B \cdot w})\)
    • 查询部分:
      • Bitset 操作:\(O(1)\) 次位运算,单次 \(O(N/w)\)。总共 \(O(N^2/w)\)
      • 散块暴力:每一维最多遍历 \(B\) 个点,共 \(4B\)
      • 当前块暴力:最多遍历 \(B\) 个点。
      • 查询总耗时:\(O(N \cdot B)\)
    • \(B = \sqrt{N}\)\(B = \sqrt{\frac{N}{w}} \cdot N\) (调整块大小以平衡)。代码中 \(B=224 \approx \sqrt{50000}\)
    • 由于 \(w=64\),常数极小,且 pre 数组通过前缀和方式构建,实际运行效率非常高。
  • 空间复杂度
    需要存储 pre 数组,大小为 \(4 \times \frac{N}{B} \times \frac{N}{8}\) 字节。
    \(4 \times 224 \times 6250 \approx 5.6 \text{ MB}\),完全在限制范围内。

代码实现细节注解

// 核心查询逻辑解析
bitset<N> S; S.set(); // 初始化全 1
for(int o = 0; o < 4; ++o) {
    // 1. 利用 bitset 快速获取四个维度“整块”部分的交集
    S &= pre[o][a[o][u] / B]; 
    
    // 2. 暴力扫描每一维“散块”部分(当前块的起始位置到当前坐标位置)
    // p[o] 是按第 o 维坐标排序后的点的索引
    for(int j = a[o][u] / B * B; j <= a[o][u]; ++j)
        if(leq(A[p[o][j]], A[u])) // 必须严格检查四维偏序,因为只满足了第 o 维
            f[u] = max(f[u], f[p[o][j]]);
}
// 3. 从交集中提取最大值
if(S.any()) f[u] = max(f[u], g[S._Find_first()]); // g 是降序的,first 就是最大值

总结

这份代码是典型的“时间换空间、位运算换常数”的高维偏序解法。虽然理论复杂度看似接近 \(O(N^2)\),但由于 \(1/64\) 的常数因子以及 \(N=50000\) 的数据规模,它比常数巨大的 \(O(N \log^3 N)\) CDQ 分治往往跑得更快,且不需要通过复杂的递归处理边界条件。

posted @ 2025-12-11 21:05  山田リョウ  阅读(2)  评论(0)    收藏  举报