记录129
#include<bits/stdc++.h>
using namespace std;
int q;//定义全局变量q,表示询问的组数
//自定义函数:求节点x的父节点(即x除以它的最小质因数)
int get_par(int x){
if(x==1)return 0;//特判:根节点1没有父节点,返回0
for(int i=2;i*i<=x;i++){//从2开始遍历到根号x,寻找x的最小质因数
if(x%i==0)return x/i;//如果i能整除x,说明i是最小质因数,返回x/i即为最大真因数(父节点)
}
return 1;//如果循环结束都没找到因数,说明x是质数,父节点直接返回1
}
//自定义函数:计算两个节点x和y在树上的距离
int get_dis(int x,int y){
int steps=0;//定义步数计数器,初始化为0
while(x!=y){//当两个节点还没有相遇(编号不相同)时,持续循环
if(x>y)x=get_par(x);//如果x的编号更大,让x向上跳到它的父节点
else y=get_par(y);//如果y的编号更大(或相等但没相遇的情况已被排除),让y向上跳到它的父节点
steps++;//每跳一次,路径步数加一
}
return steps;//当x和y相遇时,返回累计的总步数
}
int main(){//主函数入口
ios::sync_with_stdio(false);
cin.tie(0);
cin>>q;//读入询问的组数q
while(q--){//循环处理每一组询问
int x,y;//定义变量x和y,表示当前询问的两个节点编号
cin>>x>>y;//读入这两个节点的编号
cout<<get_dis(x,y)<<"\n";//调用距离计算函数,输出结果并换行
}
return 0;//程序正常结束
}
题目传送门
https://www.luogu.com.cn/problem/P13016
前言
我是一名专注信奥赛(CSP-J/S、NOIP)的教练。
- 如果你觉得这篇题解对你有帮助,欢迎点击关注我的CSDN账号,我会持续更新高质量算法解析。
- 我深知算法思维的构建远比单纯通过题目更重要,本系列题解不局限于AC代码的堆砌,而是致力于拆解题目背后的逻辑链条与核心知识点
- 备赛路上若遇瓶颈,欢迎随时评论或私信,我将甄选典型疑难问题,通过视频讲解或撰写专项文章的形式,为你提供深度答疑。
核心解题思路
这道题表面上是一个关于树和图论的问题,但由于节点数量高达 10^9,传统的建树和最近公共祖先(LCA)算法(如倍增法)会因为空间和时间复杂度过高而直接崩溃。因此,我们需要深入挖掘这棵特殊树的数学性质,将其转化为一个数论与模拟问题。
- 树的特殊构建规则:
题目规定,节点 k 的父节点是“除 kk 以外最大的因数”。在数论中,一个数的最大真因数等于该数除以它的最小质因数。例如,60 的最小质因数是 2 ,所以它的父节点是 60/2=30 ;15 的最小质因数是 3 ,父节点是 15/3=5 。 - 向上跳跃的单调性:
因为父节点总是等于“原数除以最小质因数”,而最小质因数必定 ≥2 ,所以父节点的值必定严格小于原数。这意味着,沿着父节点向上走,节点的编号是严格单调递减的。 - 双指针模拟 LCA:
既然编号是单调递减的,我们在寻找两个节点 x 和 y 的最近公共祖先(LCA)时,根本不需要复杂的倍增数组。只需要比较 x 和 y 的大小,让较大的那个数不断向上跳跃(寻找父节点),直到 x 和 y 相遇即可。相遇的点必然是它们的 LCA,跳跃的总次数就是它们之间的距离。 - 时间复杂度分析:
- 寻找一个数的最小质因数(即求父节点)的时间复杂度为 O(
) 。
- 每次向上跳跃,节点的值至少减半(因为最小质因数 ≥2 ),所以从 109109 走到根节点 1 ,最多只需要跳跃 log2(10^9)≈30 次。
- 综合来看,单次查询的时间复杂度极低,完美契合 10^9 的数据规模。
- 寻找一个数的最小质因数(即求父节点)的时间复杂度为 O(
代码分块详细解释
1. 头文件与全局变量定义
#include<bits/stdc++.h>
using namespace std;
int q; // 定义全局变量 q,表示询问的组数
- 详细分析:引入万能头文件以使用 STL 库和数学函数。定义全局变量
q用于控制后续的循环次数。
2. 核心数论函数:求父节点
// 自定义函数:求节点 x 的父节点(即 x 除以它的最小质因数)
int get_par(int x){
if(x==1) return 0; // 特判:根节点 1 没有父节点,返回 0 作为终止标志
for(int i=2; i*i<=x; i++){ // 从 2 开始遍历到 sqrt(x),寻找 x 的最小质因数
if(x%i==0) return x/i; // 如果 i 能整除 x,说明 i 就是最小质因数。
// 根据数论性质,x/i 即为除 x 以外的最大真因数(父节点),直接返回
}
return 1; // 如果循环结束都没找到因数,说明 x 本身就是一个质数。
// 质数的因数只有 1 和自身,所以它的最大真因数必定是 1,直接返回 1
}
- 详细分析:这是整个算法的基石。代码巧妙地利用了“最大真因数 = 原数 / 最小质因数”这一数学等价转换。通过 O(
)的试除法,我们避免了去枚举所有因数带来的巨大开销。同时,对根节点 1 和质数进行了完美的边界处理,保证了函数的鲁棒性(稳健性)。
3. 距离计算函数:模拟 LCA 过程
// 自定义函数:计算两个节点 x 和 y 在树上的距离
int get_dis(int x, int y){
int steps = 0; // 定义步数计数器,初始化为 0
while(x != y){ // 当两个节点还没有相遇(编号不相同)时,持续循环
if(x > y) x = get_par(x); // 核心贪心策略:如果 x 的编号更大,说明 x 在树上更深,让 x 向上跳到它的父节点
else y = get_par(y); // 反之,如果 y 的编号更大,让 y 向上跳
steps++; // 每发生一次向上跳跃,路径上的边数(步数)加一
}
return steps; // 当 x 和 y 相遇时,说明到达了最近公共祖先,返回累计的总步数
}
- 详细分析:这个函数完美替代了传统 LCA 的复杂模板。由于树的节点编号具有“越往上编号越小”的单调性,
if(x > y)的判断实际上就是在比较两个节点的深度。谁更深,谁就先往上爬。这种“双指针”式的模拟不仅代码极其简洁,而且由于每次跳跃至少使数值减半,循环次数被严格限制在对数级别,绝不会超时。
4. 主函数与 IO 处理
int main(){ // 主函数入口
ios::sync_with_stdio(false);
cin.tie(0); // 关闭 C++ 标准流同步,大幅提升大量数据输入输出的效率
cin >> q; // 读入询问的组数 q
while(q--){ // 循环处理每一组询问
int x, y; // 定义变量 x 和 y,表示当前询问的两个节点编号
cin >> x >> y; // 读入这两个节点的编号
cout << get_dis(x, y) << "\n"; // 调用距离计算函数,输出结果并换行
}
return 0; // 程序正常结束
}
- 详细分析:主函数逻辑清晰,专注于数据的读取与结果的输出。使用
"\n"代替endl避免了不必要的缓冲区刷新,配合 IO 优化,确保了在 q≤1000的询问下,程序能够以极快的速度响应。
核心逻辑总结表
| 代码模块 | 核心变量/操作 | 精炼作用 | 解决的痛点 |
|---|---|---|---|
| IO 优化 | ios::sync_with_stdio(false) | 提升输入输出效率 | 防止在多次循环查询下因 IO 瓶颈导致超时 |
| 数论转换 | for(int i=2; i*i<=x; i++) | 寻找最小质因数 | 将“求最大真因数”的 O(N) 暴力枚举降维打击至 O( |
| 边界处理 | if(x==1) 与 return 1 | 处理根节点与质数 | 保证向上跳跃逻辑在任何极端数据下都不会死循环或报错 |
| 单调性利用 | if(x > y) x = get_par(x) | 模拟双指针向上爬树 | 巧妙利用节点编号单调递减的特性,替代复杂的 LCA 倍增算法 |
| 步数累加 | steps++ | 记录路径边数 | 将抽象的树上距离转化为具体的模拟操作次数,直观且高效 |
643

被折叠的 条评论
为什么被折叠?



