This commit is contained in:
Zengtudor 2024-11-19 18:47:13 +08:00
parent 3ab0e4c503
commit c6b7d10884
6 changed files with 784 additions and 90 deletions

View File

@ -98,7 +98,7 @@ int main(){
co<<'\n';
// for(ll i{1};i<=srtn;i++)sum+=srts[i];
// co<<nv(srtn)<<'\n';
ll min = std::max(1ll, srtn/2+1);
ll min = std::max(ll(1), srtn/2+1);
// co<<nv(min)<<'\n';
if(srtn==1){
sum=srts[1];

183
src/20241118/LCA.md Normal file
View File

@ -0,0 +1,183 @@
当然可以LCALowest Common Ancestor最近公共祖先是树形结构中一个常见的问题广泛应用于计算机算法竞赛中。以下是关于如何在C++中实现LCA的一些指导包括常见的方法和示例代码。
## 一、LCA问题简介
在一棵树中,给定两个节点,找出它们的最近公共祖先(即在这两个节点的所有公共祖先中,深度最大的一个节点)。
## 二、常见的LCA算法
1. **二叉提升法Binary Lifting/倍增法**
- 通过预处理每个节点的2^k阶祖先能在O(n log n)的时间预处理并在O(log n)的时间内回答每个查询。
2. **Tarjan离线LCA算法**
- 使用并查集Union-Find结构和DFS适用于离线查询在O(n + q α(n))时间内解决其中q是查询次数α是阿克曼函数的反函数几乎可以视为常数。
3. **Euler Tour + RMQ**
- 通过欧拉序列将LCA问题转化为范围最小查询问题预处理O(n)或O(n log n)查询O(1)或O(log n)。
其中,**二叉提升法**由于实现简单且适用于在线查询,故在此详细介绍。
## 三、二叉提升法实现LCA
### 1. 算法思路
- **预处理阶段**
- 使用DFS或BFS遍历树记录每个节点的深度和2^k阶祖先。
- 通常需要计算log₂(n)的上限,以确定需要记录多少个祖先。
- **查询阶段**
- 调整两个节点的深度,使它们位于同一深度。
- 从高到低即从最大的k到0同时提升两个节点直到找到它们的公共祖先。
### 2. C++ 示例代码
以下是一个基于二叉提升法的LCA实现示例
```cpp
#include <bits/stdc++.h>
using namespace std;
const int MAX = 100005;
const int LOG = 20; // 因为2^20 > 10^6
vector<vector<int>> adj(MAX);
int up[MAX][LOG]; // up[u][k] 是节点u的2^k祖先
int depth_node[MAX];
// 深度优先搜索预处理深度和up数组
void dfs(int u, int parent_node){
up[u][0] = parent_node;
for(int k = 1; k < LOG; ++k){
up[u][k] = up[ up[u][k-1] ][k-1];
}
for(auto &v: adj[u]){
if(v != parent_node){
depth_node[v] = depth_node[u] + 1;
dfs(v, u);
}
}
}
// 寻找两个节点的LCA
int lca(int u, int v){
if(depth_node[u] < depth_node[v]) swap(u, v);
// 将u提升到与v相同的深度
int diff = depth_node[u] - depth_node[v];
for(int k = 0; k < LOG; ++k){
if(diff & (1 << k)){
u = up[u][k];
}
}
if(u == v) return u;
// 同时提升u和v直到他们的祖先相同
for(int k = LOG-1; k >= 0; --k){
if(up[u][k] != up[v][k]){
u = up[u][k];
v = up[v][k];
}
}
return up[u][0];
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
int n; // 节点数量
cin >> n;
// 假设节点编号从1到n
for(int i = 1; i < n; ++i){
int u, v;
cin >> u >> v;
adj[u].push_back(v);
adj[v].push_back(u);
}
// 假设1为根节点
depth_node[1] = 0;
dfs(1, 1);
int q; // 查询次数
cin >> q;
while(q--){
int u, v;
cin >> u >> v;
cout << lca(u, v) << "\n";
}
return 0;
}
```
### 3. 代码解释
- **变量说明**
- `adj`:邻接表,表示树。
- `up[u][k]`节点u的第2^k个祖先。
- `depth_node[u]`节点u的深度根节点深度为0
- **DFS函数**
- 对树进行DFS遍历计算每个节点的深度和其各级祖先。
- `up[u][0]`是节点u的直接父节点。
- `up[u][k]`通过`up[ up[u][k-1] ][k-1]`计算得到即节点u的2^k祖先是节点`up[u][k-1]`的2^(k-1)祖先。
- **LCA函数**
1. **深度对齐**
- 如果节点u比节点v深交换u和v。
- 计算深度差diff将u提升diff步使u和v在同一深度。
2. **共同祖先查找**
- 从最高位k开始同时检查u和v的2^k祖先是否不同。如果不同则同时提升u和v到各自的2^k祖先。
- 最后u和v的父节点即为LCA。
- **主函数**
- 输入节点数n。
- 输入n-1条边构建树。
- 假设1为根节点进行DFS预处理。
- 处理q个LCA查询。
### 4. 使用示例
假设输入:
```
5
1 2
1 3
2 4
2 5
3
4 5
4 3
5 3
```
输出:
```
2
1
1
```
表示节点4和5的LCA是2节点4和3的LCA是1节点5和3的LCA是1。
## 四、注意事项
1. **节点编号**确保节点编号从1开始或从0开始并在代码中保持一致。
2. **树的根节点**选择一个合适的根节点通常选择1或给定的节点。
3. **LOG值的选择**根据树的规模nLOG应满足2^LOG > n。常见的取值是20因为2^20约等于10^6。
4. **边的输入形式**:确保输入的边是无向的,并正确构建邻接表。
5. **处理根节点的情况**在查询LCA时如果一个节点是另一个节点的祖先算法应能正确返回祖先节点。
## 五、优化与扩展
- **空间优化**:如果树较大,可以考虑压缩`up`数组或使用更高效的存储方式。
- **路径查询**:在二叉提升的基础上,可以集成其他信息(如路径和、最小值等)以支持更多复杂的查询。
- **其他LCA算法**根据具体问题需求选择合适的LCA算法可能更加高效。
希望以上内容对你理解和实现LCA有所帮助如果有进一步的问题欢迎继续提问。

View File

@ -1,98 +1,101 @@
#include <cctype>
#include <cstdint>
#include <iostream>
#include <utility>
#include <vector>
#include <map>
#include <queue>
#include <bitset>
#include <exception>
using ll = int64_t;
using std::cin, std::cout;
// 定义最大节点数为5e5加5以确保足够的空间
const ll maxn = 5e5 + 5;
ll n, k; // n: 树的节点数k: 暴龙的种类数
ll a[maxn]; // a[i]: 第i个节点的暴龙种类
ll ans[maxn]; // ans[i]: 第i种暴龙的红温值
std::vector<std::pair<ll, ll>> adj[maxn]; // adj[u]: 节点u的邻居及边权
std::map<ll, ll> dp[maxn]; // dp[u][j]: 节点u到子树中种类j的最大距离
// 优化输入读取提高IO速度
struct CinNum {
char c;
ll n, w;
// 重载右移运算符以自定义输入方式
CinNum &operator>>(ll &num) {
c = n = 0;
w = 1;
// 跳过非数字字符,记录负号
while (!isdigit(c = getchar())) {
if (c == '-') w = -1;
}
// 读取数字
while (isdigit(c)) {
n = n * 10 + (c - '0');
c = getchar();
}
num = n * w;
return *this;
}
} cinn;
// 使用自定义的CinNum来替代标准输入
#define cin cinn
/**
* @brief 使 (DFS)
*
* @param fth
* @param u
*/
void dfs(const ll &fth, const ll &u) noexcept {
// 初始化当前节点的dp自己到自己距离为0
dp[u].emplace(a[u], 0);
// 遍历所有邻接节点
for (const auto& [v, w] : adj[u]) {
if (v == fth) continue; // 避免回到父节点
dfs(u, v); // 递归处理子节点
// 遍历子节点v的所有暴龙种类及其对应的最大距离
for (const auto& [kk, vv] : dp[v]) {
// 如果当前节点u已经有这种暴龙类型且子节点v也有
if ((dp[u][kk] > 0 || a[u] == kk) && (dp[v][kk] > 0 || a[v] == kk)) {
// 更新ans[kk]为u和v子树中这种暴龙类型的最大距离
ans[kk] = std::max(ans[kk], dp[u][kk] + dp[v][kk] + w);
}
// 更新u节点到这种暴龙类型的最大距离
dp[u][kk] = std::max(dp[u][kk], dp[v][kk] + w);
// 如果当前节点u本身就是这种暴龙类型进一步更新ans[kk]
if (a[u] == kk) {
ans[kk] = std::max(ans[kk], dp[u][kk]);
}
}
// 清空子节点v的dp以节省内存因为已经合并到u的dp中了
dp[v].clear();
const ll maxn=5e5+5, L{20};
ll n, k, a[maxn], fa[maxn][L], dpt[maxn], w[maxn];
std::vector<ll> g[maxn];
std::vector<std::pair<ll,ll>> adj[maxn];
std::bitset<maxn> vis;
void dfs(const ll &fth,const ll &u){
fa[u][0]=fth;
for(auto& [v,ww]:adj[u]){
if(v==fth)continue;
dpt[v]=dpt[u]+1;
w[v]=w[u]+ww;
dfs(u,v);
}
}
void initfa(){
for(ll k{1};k<L;k++){
for(ll u{1};u<=n;u++){
fa[u][k]=fa[fa[u][k-1]][k-1];
}
}
}
ll lca(ll u,ll v){
if(dpt[u]<dpt[v]){
std::swap(u,v);
}
ll dif = dpt[u]-dpt[v];
for(ll j{0};dif;dif>>=1,j++){
if(dif&1){
u=fa[u][j];
}
}
if(u==v){
return u;
}
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];
}
ll dis(const ll &u,const ll &v){
return w[u]+w[v]-2*w[lca(u,v)];
}
int main(){
// 读取节点数n和暴龙种类数k
cin >> n >> k;
// 读取每个节点的暴龙种类
for(ll i = 1; i <= n; i++) cin >> a[i];
// 读取树的边信息,并构建邻接表
for(ll i = 1; i < n; i++){
ll u, v, w;
cin >> u >> v >> w;
adj[u].emplace_back(v, w);
adj[v].emplace_back(u, w);
std::iostream::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);
}
// 从节点1开始DFS遍历整棵树假设节点1为根节点父节点为0
dfs(0, 1);
// 输出每种暴龙的红温值
for(ll i = 1; i <= k; i++) cout << ans[i] << '\n';
}
for(ll i{1};i<n;i++){
ll u,v,w;
cin>>u>>v>>w;
adj[u].emplace_back(v,w);
adj[v].emplace_back(u,w);
}
dfs(0,1);
initfa();
for(ll i{1};i<=k;i++){
if(g[i].size()<2){
cout<<"0\n";
continue;
}
ll u{g[i][0]},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])},dv{dis(v,g[i][j])};
if(du>dv){
if(du>ans){
v=g[i][j];
ans=du;
}
}else{
if(dv>ans){
ans=dv;
u=g[i][j];
}
}
}
cout<<ans<<'\n';
}
}

View File

@ -0,0 +1,98 @@
#include <cctype>
#include <cstdint>
#include <iostream>
#include <utility>
#include <vector>
#include <map>
using ll = int64_t;
using std::cin, std::cout;
// 定义最大节点数为5e5加5以确保足够的空间
const ll maxn = 5e5 + 5;
ll n, k; // n: 树的节点数k: 暴龙的种类数
ll a[maxn]; // a[i]: 第i个节点的暴龙种类
ll ans[maxn]; // ans[i]: 第i种暴龙的红温值
std::vector<std::pair<ll, ll>> adj[maxn]; // adj[u]: 节点u的邻居及边权
std::map<ll, ll> dp[maxn]; // dp[u][j]: 节点u到子树中种类j的最大距离
// 优化输入读取提高IO速度
struct CinNum {
char c;
ll n, w;
// 重载右移运算符以自定义输入方式
CinNum &operator>>(ll &num) {
c = n = 0;
w = 1;
// 跳过非数字字符,记录负号
while (!isdigit(c = getchar())) {
if (c == '-') w = -1;
}
// 读取数字
while (isdigit(c)) {
n = n * 10 + (c - '0');
c = getchar();
}
num = n * w;
return *this;
}
} cinn;
// 使用自定义的CinNum来替代标准输入
#define cin cinn
/**
* @brief 使 (DFS)
*
* @param fth
* @param u
*/
void dfs(const ll &fth, const ll &u) noexcept {
// 初始化当前节点的dp自己到自己距离为0
dp[u].emplace(a[u], 0);
// 遍历所有邻接节点
for (const auto& [v, w] : adj[u]) {
if (v == fth) continue; // 避免回到父节点
dfs(u, v); // 递归处理子节点
// 遍历子节点v的所有暴龙种类及其对应的最大距离
for (const auto& [kk, vv] : dp[v]) {
// 如果当前节点u已经有这种暴龙类型且子节点v也有
if ((dp[u][kk] > 0 || a[u] == kk) && (dp[v][kk] > 0 || a[v] == kk)) {
// 更新ans[kk]为u和v子树中这种暴龙类型的最大距离
ans[kk] = std::max(ans[kk], dp[u][kk] + dp[v][kk] + w);
}
// 更新u节点到这种暴龙类型的最大距离
dp[u][kk] = std::max(dp[u][kk], dp[v][kk] + w);
// 如果当前节点u本身就是这种暴龙类型进一步更新ans[kk]
if (a[u] == kk) {
ans[kk] = std::max(ans[kk], dp[u][kk]);
}
}
// 清空子节点v的dp以节省内存因为已经合并到u的dp中了
dp[v].clear();
}
}
int main(){
// 读取节点数n和暴龙种类数k
cin >> n >> k;
// 读取每个节点的暴龙种类
for(ll i = 1; i <= n; i++) cin >> a[i];
// 读取树的边信息,并构建邻接表
for(ll i = 1; i < n; i++){
ll u, v, w;
cin >> u >> v >> w;
adj[u].emplace_back(v, w);
adj[v].emplace_back(u, w);
}
// 从节点1开始DFS遍历整棵树假设节点1为根节点父节点为0
dfs(0, 1);
// 输出每种暴龙的红温值
for(ll i = 1; i <= k; i++) cout << ans[i] << '\n';
}

341
src/P3379/LCA.md Normal file
View File

@ -0,0 +1,341 @@
>
# 最近公共祖先Lowest Common Ancestor, LCA算法详解
本文将详细解释您提供的 C++ 代码该代码用于解决计算机算法竞赛中关于求解树上两个节点的最近公共祖先LCA的问题。通过这篇文章您将深入理解 LCA 算法的原理、实现细节以及如何在高效地处理大规模数据时避免暴力方法带来的性能瓶颈。
## 1. 问题概述
**题目**:给定一棵有根多叉树,回答多个询问,每个询问要求出指定两个节点的最近公共祖先。
**输入**
- 第一行:三个整数 $N,M,S$,分别表示树的节点数、询问次数和根节点的编号。
- 接下来的 $N-1$ 行:每行两个整数 $x, y$,表示节点 $x$ 和节点 $y$ 之间有一条边。
- 接下来的 $M$ 行:每行两个整数 $a, b$,表示询问节点 $a$ 和节点 $b$ 的最近公共祖先。
**输出**
- 对每个询问输出一个整数,表示两个节点的最近公共祖先的编号。
**样例解释**
通过样例可以看出,最近公共祖先是位于两个节点之间的最近的共同祖先节点。
## 2. 算法选择二进制跳跃Binary Lifting
为了高效地回答多次 LCA 查询,我们采用 **二进制跳跃** 技术。该方法预处理树的信息,使得每次查询的时间复杂度降至 $O(\log N)$。
### 2.1 原理
**二进制跳跃** 通过预计算每个节点在祖先链上的 2 的幂次方祖先,允许在查询过程中迅速地跳跃到高位的祖先,从而高效地找到两个节点的最近公共祖先。
具体步骤如下:
1. **深度优先搜索DFS**:遍历树,记录每个节点的深度和直接父节点。
2. **预处理祖先表**:构建一个表 `fa[u][k]`,表示节点 $u$ 的第 $2^k$ 个祖先。
3. **LCA 查询**
- **调整深度**:将较深的节点提升到与较浅节点相同的深度。
- **同时跳跃**:从最高位开始,同时跳跃两个节点,直到找到最近的不同祖先。
- **返回结果**:最终的最近公共祖先即为两节点的共同祖先。
## 3. 代码详解
以下是您提供的 C++ 代码,并附有详细注释和解释:
```cpp
#include <cmath>
#include <cstdint>
#include <iostream>
#include <utility>
#include <vector>
using ll = int64_t; // 定义 ll 为长整型
using std::cin, std::cout;
// 常量定义
const ll maxn = 500000 + 5; // 节点最大数
constexpr ll L = 20; // 二进制跳跃的最大深度2^20 > 500,000
ll n, m, s; // n: 节点数, m: 询问数, s: 根节点编号
ll fa[maxn][L]; // fa[u][k]: 节点 u 的第 2^k 个祖先
ll dpt[maxn]; // dpt[u]: 节点 u 的深度
std::vector<ll> adj[maxn]; // 邻接表表示的树
/**
* @brief 深度优先搜索,计算每个节点的深度和直接父节点
*
* @param fth 当前节点 u 的父节点
* @param u 当前遍历的节点
*/
void dfs(const ll &fth, const ll &u) {
fa[u][0] = fth; // 记录当前节点的直接父节点
for(auto &v : adj[u]) { // 遍历当前节点的所有邻居
if(v != fth) { // 避免回到父节点
dpt[v] = dpt[u] + 1; // 子节点的深度为父节点深度加一
dfs(u, v); // 递归遍历子节点
}
}
}
/**
* @brief 预处理祖先表,填充 fa[u][k] 表
*/
void initat() {
for(ll k = 1; k < L; ++k) { // 对每一个 2^k 的倍数
for(ll u = 1; u <= n; ++u) { // 对每个节点
if(fa[u][k-1] != 0) { // 如果 u 有 2^(k-1) 个祖先
fa[u][k] = fa[fa[u][k-1]][k-1]; // u 的 2^k 个祖先是 u 的 2^(k-1) 个祖先的 2^(k-1) 个祖先
}
// 如果 fa[u][k-1] == 0表示 u 的 2^(k-1) 个祖先不存在,则 fa[u][k] 保持为 0
}
}
}
/**
* @brief 计算两个节点的最近公共祖先
*
* @param u 节点 u
* @param v 节点 v
* @return 最近公共祖先的节点编号
*/
ll lca(ll u, ll v) {
if(dpt[u] < dpt[v]) std::swap(u, v); // 确保 u 是更深的节点
ll tmp = dpt[u] - dpt[v]; // 深度差
for(ll j = 0; tmp; j++, tmp >>= 1) { // 将 u 提升到与 v 同样的深度
if(tmp & 1) { // 如果当前位为 1则提升 u
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 和 v 的 2^j 祖先不同
u = fa[u][j]; // 同时提升 u 和 v
v = fa[v][j];
}
}
return fa[u][0]; // 返回 u 和 v 的直接父节点,即最近公共祖先
}
int main(){
// 加速读写
std::iostream::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
cin >> n >> m >> s; // 读取节点数、询问数和根节点
for(ll i = 1; i < n; i++){ // 读取 n-1 条边
ll u, v;
cin >> u >> v;
adj[u].emplace_back(v); // 添加到邻接表
adj[v].emplace_back(u);
}
dfs(0, s); // 从根节点 s 开始 DFS假设根节点的父节点为 0
initat(); // 预处理祖先表
while(m--){ // 处理每一个询问
ll u, v;
cin >> u >> v;
cout << lca(u, v) << '\n'; // 输出 LCA 的结果
}
}
```
### 3.1 代码结构解析
- **数据结构**
- `fa[maxn][L]`:二维数组,存储每个节点的 $2^k$ 个祖先。
- `dpt[maxn]`:记录每个节点的深度。
- `adj[maxn]`:邻接表表示的树结构。
- **函数**
- `dfs(fth, u)`:深度优先搜索,计算每个节点的深度和直接父节点。
- `initat()`:预处理祖先表,填充 `fa[u][k]`
- `lca(u, v)`:计算两个节点的最近公共祖先。
### 3.2 关键步骤详解
#### 3.2.1 深度优先搜索DFS
```cpp
void dfs(const ll &fth, const ll &u){
fa[u][0] = fth; // 记录直接父节点
for(auto &v: adj[u]){
if(v != fth){
dpt[v] = dpt[u] + 1; // 设置子节点的深度
dfs(u, v); // 递归遍历子节点
}
}
}
```
- **作用**:通过 DFS 遍历,计算每个节点的深度和直接父节点。
- **参数**
- `fth`:当前节点 `u` 的父节点。
- `u`:当前遍历的节点。
- **逻辑**
1. 设置当前节点 `u` 的直接父节点为 `fth`
2. 遍历 `u` 的所有邻居节点 `v`
3. 如果 `v` 不是父节点,则更新其深度为 `u` 的深度加一,并递归遍历 `v`
#### 3.2.2 预处理祖先表
```cpp
void initat(){
for(ll k = 1; k < L; ++k){
for(ll u = 1; u <= n; ++u){
if(fa[u][k-1] != 0){
fa[u][k] = fa[fa[u][k-1]][k-1];
}
}
}
}
```
- **作用**:通过动态规划填充 `fa[u][k]` 表,使得 `fa[u][k]` 存储节点 `u` 的第 $2^k$ 个祖先。
- **逻辑**
1. 对于每一个可能的 $k$(从 $1$ 到 $L-1$),计算每个节点的第 $2^k$ 个祖先。
2. 如果节点 `u` 的第 $2^{k-1}$ 个祖先存在(即 `fa[u][k-1] != 0`),则 `fa[u][k]` 是节点 `u` 的第 $2^{k-1}$ 个祖先的第 $2^{k-1}$ 个祖先。
3. 如果 `fa[u][k-1] == 0`,表示 `u` 没有第 $2^{k}$ 个祖先,保持 `fa[u][k] = 0`
#### 3.2.3 最近公共祖先查询
```cpp
ll lca(ll u, ll v){
if(dpt[u] < dpt[v]){
std::swap(u, v); // 确保 u 是更深的节点
}
ll tmp = dpt[u] - dpt[v]; // 计算深度差
for(ll j = 0; tmp; j++, tmp >>= 1){
if(tmp & 1){
u = fa[u][j]; // 将 u 提升到与 v 同样的深度
}
}
if(u == v) return u; // 如果 u 和 v 已经相同,返回 u
for(ll j = L - 1; j >= 0; j--){
if(fa[u][j] != fa[v][j]){ // 找到 u 和 v 的第一个不同的祖先
u = fa[u][j];
v = fa[v][j];
}
}
return fa[u][0]; // 返回最近公共祖先
}
```
- **作用**:计算两个节点 `u``v` 的最近公共祖先。
- **逻辑**
1. **调整深度**:如果 `u``v` 更浅,则交换 `u``v`,确保 `u` 是更深的节点。
2. **提升深度**:计算 `u``v` 的深度差,通过二进制跳跃将 `u` 提升到与 `v` 相同的深度。
3. **寻找 LCA**
- 如果提升后 `u``v` 相同,则 LCA 即为 `u`
- 否则,从高位开始,同时提升 `u``v`,直到找到两个节点的祖先不同的位置。
- 最终LCA 为 `u``v` 的直接父节点 `fa[u][0]`
### 3.3 主函数流程
```cpp
int main(){
// 加速输入输出
std::iostream::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
cin >> n >> m >> s; // 输入节点数、询问数和根节点
for(ll i = 1; i < n; i++){ // 输入 n-1 条边
ll u, v;
cin >> u >> v;
adj[u].emplace_back(v);
adj[v].emplace_back(u);
}
dfs(0, s); // 从根节点 s 开始 DFS假设根节点的父节点为 0
initat(); // 预处理祖先表
while(m--){ // 处理每一个询问
ll u, v;
cin >> u >> v;
cout << lca(u, v) << '\n'; // 输出最近公共祖先
}
}
```
- **流程**
1. **输入处理**
- 读取节点数 `n`、询问数 `m` 和根节点 `s`
- 读取 `n-1` 条边,将树结构存储在邻接表 `adj` 中。
2. **预处理**
- 通过 DFS 遍历树,计算每个节点的深度和直接父节点。
- 预处理祖先表 `fa[u][k]`
3. **处理询问**
- 对于每一个询问,读取节点 `u``v`,计算并输出它们的 LCA。
## 4. 重要概念总结
### 4.1 二进制跳跃Binary Lifting
二进制跳跃是一种预处理技术,用于在树上高效地回答 LCA 及其他相关查询。其核心思想是通过预计算每个节点的 $2^k$ 个祖先,使得在查询过程中可以通过二进制表示逐步跳跃到目标位置,从而降低查询时间复杂度。
**优点**
- **高效性**:预处理时间为 $O(N \log N)$,每次 LCA 查询时间为 $O(\log N)$。
- **适用性广**:适用于静态树(树结构不变的情况下)的多次查询。
**关键点**
- 需要记录每个节点在不同层次的祖先。
- 利用二进制表示,快速提升节点到目标深度。
### 4.2 最近公共祖先Lowest Common Ancestor, LCA
**定义**:在树中,两个节点的最近公共祖先是距离这两个节点最近的一个共同祖先。
**应用场景**
- 计算树上两个节点之间的距离。
- 解决家族关系、层级关系等问题。
### 4.3 深度优先搜索DFS
**作用**:遍历树结构,计算每个节点的深度和直接父节点。
**特点**
- **递归或迭代实现**
- **时间复杂度**$O(N)$,适用于大规模树的遍历。
## 5. 复杂度分析
- **预处理**
- **DFS**$O(N)$。
- **祖先表填充**$O(N \log N)$。
- **每次查询**$O(\log N)$。
- **总体**
- **总预处理时间**$O(N \log N)$。
- **所有查询时间**$O(M \log N)$。
对于本题的约束条件 ($N, M \leq 500,000$),二进制跳跃确保了程序的高效性,避免了 $M \times N$ 的暴力查询带来的性能问题。
## 6. 代码优化与注意事项
- **输入输出优化**
- 使用 `std::ios::sync_with_stdio(false);``cin.tie(nullptr);` 加速输入输出,避免使用 `std::printf``std::scanf`
- **常量设置**
- 确保 `L` 足够大,使得 $2^L \geq N$。对于 $N \leq 5 \times 10^5$$L = 20$ 足以,因为 $2^{20} = 1,048,576 > 500,000$。
- **避免递归深度过大**
- 对于 $N = 500,000$,递归深度可能达到 $500,000$。在一些编译器中,这可能导致栈溢出。可以通过优化编译器选项或使用非递归 DFS 实现来避免。
## 7. 扩展阅读与学习建议
- **Tarjan 算法**:另一种用于解决 LCA 问题的算法,基于并查集,适合离线查询。
- **线性时间 LCA**:通过 Euler Tour 和 RMQ可以在 $O(N)$ 的预处理时间下,实现 $O(1)$ 的查询时间。
- **HLD重链剖分**:将树分解为重链和轻链,适用于多种树上的查询问题,包括 LCA、路径查询等。
建议深入理解二进制跳跃原理,并尝试实现相关算法。同时,通过大量的实践题目来巩固知识,增强对不同 LCA 算法的应用能力。
## 8. 总结
通过以上的详细解析,您已经了解了二进制跳跃法在 LCA 问题中的应用,并掌握了对应代码的具体实现方式。关键在于:
1. **预处理**
- 使用 DFS 计算每个节点的深度和直接父节点。
- 使用动态规划填充祖先表 `fa[u][k]`
2. **查询**
- 调整节点深度,使得两节点处于相同的层次。
- 通过二进制跳跃同时上升,找到最近的共同祖先。
这种方法在大规模数据下表现出色,避免了暴力方法带来的性能瓶颈,非常适合竞赛环境中的实时查询需求。

69
src/P3379/P3379.cpp Normal file
View File

@ -0,0 +1,69 @@
#include <cmath>
#include <cstdint>
#include <iostream>
#include <utility>
#include <vector>
using ll = int64_t;
using std::cin,std::cout;
const ll maxn=500000+5;
constexpr ll L = 20;
ll n,m,s, fa[maxn][L],dpt[maxn];
std::vector<ll> adj[maxn];
void dfs(const ll &fth,const ll &u){
fa[u][0]=fth;
for(auto &v:adj[u]){
if(v!=fth){
dpt[v]=dpt[u]+1;
dfs(u,v);
}
}
}
void initat(){
for(ll k{1};k<L;++k){
for(ll u{1};u<=n;++u){
fa[u][k]=fa[fa[u][k-1]][k-1];
}
}
}
ll lca(ll u,ll v){
if(dpt[u]<dpt[v]){
std::swap(u,v);
}
ll tmp{dpt[u]-dpt[v]};
for(ll j{0};tmp;j++,tmp>>=1){
if(tmp&1){
u=fa[u][j];
}
}
if(u==v)return u;
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];
}
int main(){
std::iostream::sync_with_stdio(false),std::cin.tie(nullptr),std::cout.tie(nullptr);
cin>>n>>m>>s;
for(ll i{1};i<n;i++){
ll u,v;
cin>>u>>v;
adj[u].emplace_back(v);
adj[v].emplace_back(u);
}
dfs(0,s);
initat();
while(m--){
ll u,v;
cin>>u>>v;
cout<<lca(u,v)<<'\n';
}
}