This commit is contained in:
Zengtudor 2024-11-19 20:46:03 +08:00
parent c6b7d10884
commit bdb8bea73f
2 changed files with 444 additions and 3 deletions

View File

@ -1,9 +1,7 @@
#include <cstdint>
#include <iostream>
#include <vector>
#include <queue>
#include <bitset>
#include <exception>
using ll = int64_t;
using std::cin, std::cout;

View File

@ -226,3 +226,446 @@ int main(){
- 对于更大规模的数据需要优化数据结构和算法例如使用更高效的方式存储DP信息如并查集、最低公共祖先LCA等技术以降低时间和空间复杂度。
希望这些解释和注释对您理解和优化代码有所帮助!如果有进一步的问题,欢迎继续交流。
# 100pts解
### 题目概述
本题涉及在一棵带权树上计算不同类型的暴龙(类似于节点类型)的红温值(即同类型暴龙任意两点之间的最大距离)。具体来说,给定一棵有 $n$ 个节点和边权的树,树上的每个节点都有 $k$ 种暴龙中的一种。对于每一种暴龙类型定义其红温值为同类型暴龙之间的最大距离如果该类型的暴龙数量小于2则红温值为0。
### 解题思路
1. **树的预处理**
- **深度优先搜索 (DFS)**:计算每个节点的深度 (`dpt`),以及从根节点到该节点的距离 (`w`)。
- **二进制跳表 (Binary Lifting)**:预处理每个节点的祖先节点,以便快速计算任意两个节点的最近公共祖先 (LCA)。
- **距离计算**:利用 LCA计算任意两个节点之间的距离。
2. **计算每种暴龙的红温值**
- 对于每种暴龙类型,首先选取其中任意两个节点,计算它们之间的距离作为初始的最大距离 (`ans`)。
- 逐一遍历该类型的所有节点,对于每个新的节点,计算它与当前两个端点的距离,更新端点和 `ans` 的值以确保 `ans` 始终为当前类型的最大距离。
### 代码详解与注释
```cpp
#include <cstdint>
#include <iostream>
#include <vector>
#include <bitset>
// 使用类型别名提高代码可读性
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<ll> g[maxn]; // g[i] 存储所有类型为 i 的节点
std::vector<std::pair<ll, ll>> 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 和每个 j0 ≤ j ≤ log₂nfa[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 = 6dif & 1 = 0跳过
- j=2: dif >> 2 = 3dif & 1 = 1提升 2^2 = 4 步u = fa[u][2]
- j=3: dif >> 3 = 1dif & 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=2fa[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 = 2v = 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。这种方法的核心在于利用二进制位对应的跳跃步数实现快速的多级提升从而优化查询效率。理解并掌握二进制提升对于解决大规模树问题如本题具有重要意义。