diff --git a/src/20241118/T541156.cpp b/src/20241118/T541156.cpp index a1190bf..b95463e 100644 --- a/src/20241118/T541156.cpp +++ b/src/20241118/T541156.cpp @@ -1,9 +1,7 @@ #include #include #include -#include #include -#include using ll = int64_t; using std::cin, std::cout; diff --git a/src/20241118/T541156.md b/src/20241118/T541156.md index 12f8c93..f7711cb 100644 --- a/src/20241118/T541156.md +++ b/src/20241118/T541156.md @@ -225,4 +225,447 @@ int main(){ - 该代码适用于子任务1,规模较小(n ≤ 3000)的情况,能够在有限的时间内完成计算。 - 对于更大规模的数据,需要优化数据结构和算法,例如使用更高效的方式存储DP信息(如并查集、最低公共祖先LCA等技术),以降低时间和空间复杂度。 -希望这些解释和注释对您理解和优化代码有所帮助!如果有进一步的问题,欢迎继续交流。 \ No newline at end of file +希望这些解释和注释对您理解和优化代码有所帮助!如果有进一步的问题,欢迎继续交流。 + +# 100pts解 +### 题目概述 + +本题涉及在一棵带权树上计算不同类型的暴龙(类似于节点类型)的红温值(即同类型暴龙任意两点之间的最大距离)。具体来说,给定一棵有 $n$ 个节点和边权的树,树上的每个节点都有 $k$ 种暴龙中的一种。对于每一种暴龙类型,定义其红温值为同类型暴龙之间的最大距离;如果该类型的暴龙数量小于2,则红温值为0。 + +### 解题思路 + +1. **树的预处理**: + - **深度优先搜索 (DFS)**:计算每个节点的深度 (`dpt`),以及从根节点到该节点的距离 (`w`)。 + - **二进制跳表 (Binary Lifting)**:预处理每个节点的祖先节点,以便快速计算任意两个节点的最近公共祖先 (LCA)。 + - **距离计算**:利用 LCA,计算任意两个节点之间的距离。 + +2. **计算每种暴龙的红温值**: + - 对于每种暴龙类型,首先选取其中任意两个节点,计算它们之间的距离作为初始的最大距离 (`ans`)。 + - 逐一遍历该类型的所有节点,对于每个新的节点,计算它与当前两个端点的距离,更新端点和 `ans` 的值以确保 `ans` 始终为当前类型的最大距离。 + +### 代码详解与注释 + +```cpp +#include +#include +#include +#include + +// 使用类型别名提高代码可读性 +using ll = int64_t; +using std::cin, std::cout; + +// 常量定义 +const ll maxn = 5e5 + 5; // 最大节点数 +const ll L = 20; // 二进制跳表的层数,满足 n <= 5e5 时 2^20 > 5e5 + +// 全局变量 +ll n, k; // 节点数和暴龙种类数 +ll a[maxn]; // a[i] 表示节点 i 的暴龙类型 +ll fa[maxn][L]; // fa[u][j] 表示节点 u 的 2^j 祖先节点 +ll dpt[maxn]; // dpt[u] 表示节点 u 的深度 +ll w[maxn]; // w[u] 表示节点 u 与根节点的距离 +std::vector g[maxn]; // g[i] 存储所有类型为 i 的节点 +std::vector> adj[maxn]; // adj[u] 存储节点 u 的所有邻接点及对应边权 + +// DFS 预处理,计算深度和从根到每个节点的距离 +void dfs(const ll &fth, const ll &u){ + fa[u][0] = fth; // 记录节点 u 的直接父节点 + for(auto& [v, ww] : adj[u]){ + if(v == fth) continue; // 避免回到父节点 + dpt[v] = dpt[u] + 1; // 更新子节点的深度 + w[v] = w[u] + ww; // 更新子节点到根的距离 + dfs(u, v); // 递归遍历子节点 + } +} + +// 初始化二进制跳表,用于快速查询 LCA +void initfa(){ + for(ll j = 1; j < L; j++){ // 对于每一层 + for(ll u = 1; u <= n; u++){ // 对于每个节点 + fa[u][j] = fa[fa[u][j-1]][j-1]; // 2^j 祖先等于 2^(j-1) 的祖先的 2^(j-1) 祖先 + } + } +} + +// 计算两个节点的最近公共祖先 (LCA) +ll lca(ll u, ll v){ + if(dpt[u] < dpt[v]) std::swap(u, v); // 确保 u 的深度不小于 v + ll dif = dpt[u] - dpt[v]; + // 将 u 提升到与 v 在同一深度 + for(ll j = 0; dif > 0; j++, dif >>= 1){ + if(dif & 1){ + u = fa[u][j]; + } + } + if(u == v) return u; // 如果 v 是 u 的祖先,直接返回 + // 从高位到低位同时提升 u 和 v,找到 LCA + for(ll j = L-1; j >= 0; j--){ + if(fa[u][j] != fa[v][j]){ + u = fa[u][j]; + v = fa[v][j]; + } + } + return fa[u][0]; // 返回 LCA +} + +// 计算两个节点之间的距离 +ll dis(const ll &u, const ll &v){ + return w[u] + w[v] - 2 * w[lca(u, v)]; +} + +int main(){ + // 关闭同步,提高输入输出速度 + std::ios::sync_with_stdio(false); + std::cin.tie(nullptr); + std::cout.tie(nullptr); + + // 输入节点数和暴龙种类数 + cin >> n >> k; + // 输入每个节点的暴龙类型,并将节点编号加入对应类型的列表 + for(ll i = 1; i <= n; i++){ + cin >> a[i]; + g[a[i]].push_back(i); + } + // 输入树的边 + for(ll i = 1; i < n; i++){ + ll u, v, c; + cin >> u >> v >> c; + adj[u].emplace_back(v, c); + adj[v].emplace_back(u, c); + } + // 从根节点 1 开始进行 DFS 预处理 + dfs(0, 1); + // 初始化二进制跳表 + initfa(); + + // 对于每一种暴龙类型,计算其红温值 + for(ll i = 1; i <= k; i++){ + // 如果该类型的暴龙数量小于2,红温值为0 + if(g[i].size() < 2){ + cout << "0\n"; + continue; + } + // 初始化第一个端点 u 和第二个端点 v + ll u = g[i][0]; + ll v = g[i][1]; + // 计算初始的最大距离 + ll ans = dis(u, v); + // 遍历该类型的剩余节点,更新端点和最大距离 + for(ll j = 2; j < (ll)g[i].size(); j++){ + ll du = dis(u, g[i][j]); // 当前节点与端点 u 的距离 + ll dv = dis(v, g[i][j]); // 当前节点与端点 v 的距离 + if(du > dv){ + if(du > ans){ + v = g[i][j]; // 更新 v 为当前节点 + ans = du; // 更新最大距离 + } + } + else{ + if(dv > ans){ + u = g[i][j]; // 更新 u 为当前节点 + ans = dv; // 更新最大距离 + } + } + } + // 输出该类型的红温值 + cout << ans << '\n'; + } +} +``` + +### 详细解释 + +1. **输入与预处理**: + - **节点信息**:首先读取节点数 `n` 和暴龙种类数 `k`。然后读取每个节点的暴龙类型,存储在数组 `a` 中,并将每个节点编号加入对应类型的列表 `g[i]`。 + - **树的构建**:接下来读取 `n-1` 条边,每条边连接两个节点并有一个边权,将这些信息存储在邻接表 `adj` 中。 + +2. **深度优先搜索 (DFS) 和二进制跳表初始化**: + - **DFS**:从根节点(这里假设为节点1)开始遍历整棵树,计算每个节点的深度 `dpt`,以及从根节点到该节点的距离 `w`。同时,记录每个节点的直接父节点 `fa[u][0]`。 + - **二进制跳表 (`initfa` 函数)**:构建一个 `fa` 数组,其中 `fa[u][j]` 表示节点 `u` 的 `2^j` 祖先节点。通过动态规划的方式,`fa[u][j] = fa[fa[u][j-1]][j-1]`,从而可以在对数时间内找到任意节点的祖先,用于高效计算 LCA。 + +3. **最近公共祖先 (LCA) 和距离计算**: + - **LCA (`lca` 函数)**:利用二进制跳表,首先将深度较深的节点提升到与另一个节点同一深度,然后同时提升两者直到找到 LCA。 + - **距离 (`dis` 函数)**:利用 LCA 计算两个节点之间的距离,即 `w[u] + w[v] - 2*w[lca(u, v)]`。 + +4. **计算每种暴龙的红温值**: + - 对于每种暴龙类型 `i`: + - 如果该类型的节点数少于2,输出0。 + - 否则,选择该类型中的任意两个节点作为初始的两个端点 `u` 和 `v`,计算它们之间的距离作为初始的红温值 `ans`。 + - 遍历该类型的剩余节点,每遇到一个新节点,计算它与当前两个端点的距离 `du` 和 `dv`。 + - 如果 `du > dv`: + - 如果 `du > ans`,则将端点 `v` 更新为当前节点,`ans` 更新为 `du`。 + - 否则: + - 如果 `dv > ans`,则将端点 `u` 更新为当前节点,`ans` 更新为 `dv`。 + - 这种方法类似于在树上寻找直径的双 BFS 方法,通过两次遍历找到最大距离。 + +5. **输出结果**: + - 对每一种暴龙类型,输出其对应的红温值。 + +### 算法复杂度分析 + +- **预处理阶段**: + - **DFS**:$O(n)$。 + - **二进制跳表初始化**:$O(n \log n)$。 + +- **每种类型的红温值计算**: + - 对于每种类型,遍历其所有节点,计算距离。 + - 总体复杂度为 $O(k \cdot m \cdot \log n)$,其中 $m$ 是每种类型的节点数。 + +- **整体复杂度**:由于 $k \leq n$ 且每种类型的节点总和为 $n$,整体复杂度为 $O(n \log n + n \log n) = O(n \log n)$,满足题目约束。 + +### 代码优化技巧 + +1. **高效输入输出**:使用 `std::ios::sync_with_stdio(false)` 和 `std::cin.tie(nullptr)` 关闭同步和绑定,提高输入输出速度,避免因大量数据导致超时。 +2. **使用静态数组**:利用预定义大小的静态数组(如 `a[maxn]`, `fa[maxn][L]` 等)提高访问速度和内存利用效率。 +3. **按类型存储节点**:将相同类型的节点集中存储在 `g[i]` 中,便于后续遍历和计算。 + +### 总结 + +本题通过有效地预处理树的深度、距离和 LCA,结合类似于双 BFS 的方法,能够在较低的时间复杂度内求解每一种暴龙类型的红温值。关键在于利用二进制跳表实现高效的 LCA 查询,以及聪明地遍历节点以找到最大距离。这种方法在处理大规模树型数据时尤为有效,适用于类似的树上最远点对问题。 +### 详细解释 LCA(最近公共祖先)与二进制位的关系 + +在树结构中,**最近公共祖先(Lowest Common Ancestor, LCA)** 是指两个节点在树中共同的最深的祖先节点。高效地计算 LCA 对于许多树相关的问题至关重要,例如本题中用于计算节点间距离。 + +本文将详细解释如何利用 **二进制提升(Binary Lifting)** 技术来高效计算 LCA,特别是如何通过二进制位的概念实现这一过程。 + +#### 二进制提升概述 + +**二进制提升** 是一种预处理技术,用于在树上快速查询任意两个节点的 LCA。其核心思想是为每个节点预计算并存储其 2^j 祖先节点,其中 j 从 0 到 log₂n。这允许我们以对数时间复杂度(O(log n))跳跃树的高度,从而快速找到 LCA。 + +#### 关键概念 + +1. **2^j 祖先节点(fa[u][j])**: + - 对于树中的每个节点 u 和每个 j(0 ≤ j ≤ log₂n),fa[u][j] 表示节点 u 的第 2^j 级祖先节点。 + - 例如,fa[u][0] 是 u 的直接父节点,fa[u][1] 是 u 的祖父节点(2^1 = 2 跳),fa[u][2] 是 u 的曾祖父节点(2^2 = 4 跳),依此类推。 + +2. **节点深度(dpt)**: + - dpt[u] 表示节点 u 相对于根节点的深度(根节点深度为 0)。 + +3. **距离表示与二进制位**: + - 任意整数可以用二进制表示为若干个 2^j 的和。例如,13 = 8 + 4 + 1 = 2^3 + 2^2 + 2^0,对应二进制 1101。 + - 这种表示方式允许我们通过二进制位选择性地跳跃若干步,从而高效地移动节点。 + +#### 计算 LCA 的步骤与二进制位的应用 + +以下为计算两个节点 u 和 v 的 LCA 的主要步骤,详细解释了二进制位的应用: + +1. **确保 u 在较深的位置**: + ```cpp + if(dpt[u] < dpt[v]) std::swap(u, v); + ``` + 确保节点 u 比节点 v 深,这样我们只需处理 u 的提升。 + +2. **计算深度差并用二进制表示**: + ```cpp + ll dif = dpt[u] - dpt[v]; + ``` + 计算节点 u 和节点 v 之间的深度差 dif。 + +3. **将 u 提升到与 v 同一深度**: + ```cpp + for(ll j = 0; dif > 0; j++, dif >>= 1){ + if(dif & 1){ + u = fa[u][j]; + } + } + ``` + - **二进制位检查**:通过检查 dif 的每一位(从低到高),确定需要提升的步数。 + - **位的意义**:每一位 j(从 0 开始)对应着 2^j 步的提升。 + - **示例**: + - 若 dif = 13(即二进制 1101),则表示需要提升 2^0 + 2^2 + 2^3 = 1 + 4 + 8 = 13 步。 + - 循环过程中: + - j=0: dif & 1 = 1,提升 2^0 = 1 步,u = fa[u][0] + - j=1: dif >> 1 = 6,dif & 1 = 0,跳过 + - j=2: dif >> 2 = 3,dif & 1 = 1,提升 2^2 = 4 步,u = fa[u][2] + - j=3: dif >> 3 = 1,dif & 1 = 1,提升 2^3 = 8 步,u = fa[u][3] + - 最终,u 被提升 13 步,达到与 v 同一深度。 + +4. **检查是否已经找到 LCA**: + ```cpp + if(u == v) return u; + ``` + 如果提升后 u 和 v 重合,则 LCA 为 u(或 v)。 + +5. **同步提升 u 和 v 以找到 LCA**: + ```cpp + for(ll j = L - 1; j >= 0; j--){ + if(fa[u][j] != fa[v][j]){ + u = fa[u][j]; + v = fa[v][j]; + } + } + return fa[u][0]; + ``` + - **从高位到低位**:从最大的 j 开始,逐步检查并提升 u 和 v。 + - **条件判断**:若 fa[u][j] != fa[v][j],则同时提升 u 和 v 到它们各自的第 2^j 祖先。 + - **找到 LCA**:最终,u 和 v 的父节点(fa[u][0])为 LCA。 + + **二进制位的应用**: + - 通过从高到低的二进制位检查,我们确保每一步尽可能多地提升 u 和 v,快速缩小它们的差距。 + - 这种方式最大限度地利用了二进制表示中的高位,从而在对数步内完成提升,达到高效计算的目的。 + +#### 示例说明 + +让我们通过一个具体示例来说明二进制位在 LCA 计算中的应用。 + +**示例树结构**: + +``` + 1 + / \ + 2 3 + / / \ + 4 5 6 + / \ + 7 8 +``` + +**节点深度(dpt)与 fa 表**: + +| 节点 | dpt | fa[u][0] | fa[u][1] | fa[u][2] | fa[u][3] | +|------|-----|----------|----------|----------|----------| +| 1 | 0 | 0 | 0 | 0 | 0 | +| 2 | 1 | 1 | 0 | 0 | 0 | +| 3 | 1 | 1 | 0 | 0 | 0 | +| 4 | 2 | 2 | 1 | 0 | 0 | +| 5 | 2 | 3 | 1 | 0 | 0 | +| 6 | 2 | 3 | 1 | 0 | 0 | +| 7 | 3 | 4 | 2 | 1 | 0 | +| 8 | 3 | 4 | 2 | 1 | 0 | + +**查询 LCA(7, 5)**: + +1. **节点深度**: + - dpt[7] = 3 + - dpt[5] = 2 + +2. **保证 u 深度 >= v 深度**: + - u = 7, v = 5 + +3. **计算深度差 dif = 3 - 2 = 1(二进制 0001)**: + - 0001 表示需要提升 2^0 = 1 步。 + +4. **提升 u 7 一步**: + - fa[7][0] = 4 → u = 4 + +5. **比较 u 和 v**: + - u = 4, v = 5 + - u ≠ v,因此需要继续提升。 + +6. **同步提升 u 和 v**: + - 从 j = L-1(假设 L=4)到 j=0: + - j=3 到 j=2:fa[4][3] = 0, fa[5][3] = 0 → 相等,跳过 + - j=1: + - fa[4][1] = 1 + - fa[5][1] = 1 + - 相等,跳过 + - j=0: + - fa[4][0] = 2 + - fa[5][0] = 3 + - 不相等,提升: + - u = fa[4][0] = 2 + - v = fa[5][0] = 3 + +7. **找到 LCA**: + - 最终,u = 2,v = 3 + - fa[2][0] = 1 + - fa[3][0] = 1 + - LCA = 1 + +**总结**:LCA(7,5) = 1 + +#### 二进制位在代码中的应用 + +让我们回顾代码中 LCA 相关部分,并结合上述示例进一步理解。 + +```cpp +ll lca(ll u, ll v){ + if(dpt[u] < dpt[v]){ + std::swap(u, v); + } + ll dif = dpt[u] - dpt[v]; + // 将 u 提升到与 v 同一深度 + for(ll j = 0; dif > 0; j++, dif >>= 1){ + if(dif & 1){ + u = fa[u][j]; + } + } + if(u == v){ + return u; + } + // 同时提升 u 和 v 直到它们的父节点相同 + for(ll j = L-1; j >= 0; j--){ + if(fa[u][j] != fa[v][j]){ + u = fa[u][j]; + v = fa[v][j]; + } + } + return fa[u][0]; +} +``` + +**详细解释**: + +1. **提升 u 到 v 的深度**: + - 通过检查 dif 的每一位(即二进制位),决定是否需要进行 2^j 次提升。 + - 如果 dif 的某一位为 1,则将 u 提升 2^j 步。 + +2. **同步提升 u 和 v**: + - 从最高的二进制位(L-1)开始,逐步检查并提升 u 和 v。 + - 若 fa[u][j] != fa[v][j],则将 u 和 v 分别提升 2^j 步,直到它们接近 LCA。 + +3. **返回 LCA**: + - 最终,fa[u][0] 即为 LCA。 + +#### 二进制位与跳跃步数的对应关系 + +- 每一位 j(从 0 开始)对应着 2^j 的跳跃步数。 +- 通过位操作,可以将任意步数的提升分解为若干个 2^j 的跳跃,从而高效实现多级提升。 + +**具体对应关系**: + +| j(位数) | 2^j(跳跃步数) | +|----------|----------------| +| 0 | 1 | +| 1 | 2 | +| 2 | 4 | +| 3 | 8 | +| ... | ... | +| L-1 | 2^(L-1) | + +**举例说明**: + +假设需要提升一个节点 u 13 步: + +- 13 的二进制表示为 1101。 +- 这意味着需要: + - 2^0 = 1 步(j=0) + - 2^2 = 4 步(j=2) + - 2^3 = 8 步(j=3) +- 通过分别提升 u 一步、四步和八步,总共提升13步。 + +在代码中,这通过以下操作实现: + +```cpp +for(ll j = 0; dif > 0; j++, dif >>= 1){ + if(dif & 1){ + u = fa[u][j]; + } +} +``` + +- 每次循环检查 dif 的最低位(dif & 1)。 +- 若为1,则将 u 提升 2^j 步,即 u = fa[u][j]。 +- 然后,将 dif 右移一位(dif >>= 1),准备检查下一位。 + +#### 为什么二进制提升高效? + +- **时间复杂度**:计算 LCA 的时间复杂度为 O(log n),因为提升和同步提升均涉及对数级别的操作。 +- **空间复杂度**:需要 O(n log n) 的空间来存储 fa 表,但这对于 n ≤ 5×10^5 是可接受的。 + +相比于传统的每次向上遍历节点的方式(可能达到 O(n) 的时间复杂度),二进制提升显著提高了效率,特别是在处理大规模数据时。 + +### 总结 + +在本题中,**二进制提升** 技术通过预计算每个节点的 2^j 祖先,结合二进制位的表示,能够在对数时间内高效地计算树中任意两个节点的 LCA。这种方法的核心在于利用二进制位对应的跳跃步数,实现快速的多级提升,从而优化查询效率。理解并掌握二进制提升对于解决大规模树问题(如本题)具有重要意义。