惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

酷 壳 – CoolShell
酷 壳 – CoolShell
P
Privacy & Cybersecurity Law Blog
G
GRAHAM CLULEY
T
The Exploit Database - CXSecurity.com
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Project Zero
Project Zero
S
Security @ Cisco Blogs
TaoSecurity Blog
TaoSecurity Blog
A
Arctic Wolf
Webroot Blog
Webroot Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
Security Latest
Security Latest
H
Heimdal Security Blog
N
News and Events Feed by Topic
N
News | PayPal Newsroom
T
Tor Project blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
GbyAI
GbyAI
The Last Watchdog
The Last Watchdog
Y
Y Combinator Blog
宝玉的分享
宝玉的分享
Scott Helme
Scott Helme
A
About on SuperTechFans
M
MIT News - Artificial intelligence
V
V2EX
V
Visual Studio Blog
Recorded Future
Recorded Future
博客园 - 叶小钗
F
Fortinet All Blogs
L
Lohrmann on Cybersecurity
The GitHub Blog
The GitHub Blog
博客园 - Franky
P
Proofpoint News Feed
MyScale Blog
MyScale Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
S
Secure Thoughts
D
DataBreaches.Net
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
博客园 - 三生石上(FineUI控件)
I
InfoQ
SecWiki News
SecWiki News
Blog — PlanetScale
Blog — PlanetScale
Engineering at Meta
Engineering at Meta
J
Java Code Geeks
B
Blog RSS Feed
AWS News Blog
AWS News Blog
Know Your Adversary
Know Your Adversary
V
Vulnerabilities – Threatpost
H
Help Net Security

博客园_首页

Plist 二进制格式 Milvus 和 PGVector,哪个更好? OpenClaw 已过时?在 VS Code 中运行 Hermes Agent! 第30篇文章:一个大三计科生的自白 Manim如何在数学公式中完美显示中文? Docker 部署 RocketMQ 5 并发编程核心概念辨析 C#事务处理最佳实践:别再让“主表存了、明细丢了”的破事发生 CLI 是什么?为什么大厂突然集体卷命令行? 【从0到1构建一个ClaudeAgent】协作-自主Agent UIImageView 设置图片不生效的原因排查 最小二乘问题详解20:无先验约束下的增量式SFM自由网平差 痞子衡嵌入式:大话双核i.MXRT1180之XIP应用里借助MU实现可靠Flash IAP的方法 AI Chat 封装, SemanticKerne.AiProvider.Unified 已发布 Windows下右键编辑js文件无法打开记事本——在注册表中使用环境变量 在后台服务中使用 Scoped 服务,为什么总是报错? H200 安装驱动并使用sglang启动模型 wireshark 抓包Trap上报告警内容 我用 AI 辅助开发了一系列小工具(2):图片压缩工具 [A Primer On MC and CC] 2.1 Memory Consistency 1 - 指令重排序和 SC 模型 Oracle数据库SCN推进技术详解与实践指南 玩转控件:封装个带图片的Label控件 Claude Code 4.7 真正该升级的不是模型,而是你的工作流 前端小白一句话,AI 帮我做了个颜值拉满的桌面媒体播放器。当代码不再是门槛,一句话编程就是现实。 5. WorkBuddy: 小龙虾的灵魂三件套,让你的小龙虾不只是工具 SQLite 分片方案实战:三种分片策略的深度对比 告别简陋 UI!一款基于 Fluent Design 和基于 WinUI 的开源免费、现代化的 Avalonia UI 控件库 关于二进制排列组合枚举的总结 AI开发-python-LangGraph框架(3-27-LangGraph从零实现大模型智能决策工作流) ElasticSearch主分片和副本分片概念详解 【002】HTTPS 粗解:证书、TLS 握手与对后端配置的影响 Hermes Agent 一周暴涨五万 Star,但我劝你别急着追 明明连接的是Redis的DB0,为什么能查到DB3的数据? 【从0到1构建一个ClaudeAgent】协作-Agent团队 熟悉电子元器件之后,电子小白下一步该怎么走? MAF快速入门(23)通过C#类定义Skills .NET 高级开发 | 手写一个对象映射框架 FastAPI数据库ORM怎么选?我肝了三个Demo后,终于不再纠结了 mysqldump 参数拾遗:在遗忘与铭记之间 C# .NET 周刊|2026年3月5期 Claude code入门 - 陈彦斌 一文学习入门 ThingsBoard 开源物联网平台 GitHub 热门项目 | 2026年04月16日 如何为GIT设置全局勾子,为每次提交追加信息 Number.isFinite和isFinite与isNaN()和Number.isNaN的区别 PortSwigger SQL注入LAB2 推荐一个测试人必备的Skills,从功能到性能全搞定(附详细实操和安装下载方式) 筑基期:掌握Odoo基础核心知识点02(Odoo XML 开发方式详解) GLM模型这么火,咱们用vllm也咧一个呗! 深入理解 AbortController:从底层原理到跨语言设计哲学 字符串学习笔记 多租户系统框架的基础模块设计和分析设计 Apache SeaTunnel Zeta 为什么能做到“又快又稳”? AI开发-python-LangGraph框架(3-26-LangGraph基本概念及第一个简单样例) Vue 3 组件通信,别只会用 Props 和 Emits 了,这几个狠活儿你得看看 ElasticSearch7.X版本配置密码 用Manim实现动态交点计算--从一个动点问题说起 团结引擎+Addressable+Instant Game打包抖音小游戏 function call 实战:让 LLM 自动判断 pod 异常、调用日志工具并完成故障分析 bubseek —— 让 Agent 的足迹,变成团队的洞察 通过 C# 读取并导出 PDF 书签 如何用 GitHub Actions 实现 Steam 自动化发布 【从0到1构建一个ClaudeAgent】并发-后台任务 .NET 高级开发 | 定制 ASP.NET Core 框架 电子小白:什么是运算放大器(运放) zero2Agent:面向大厂面试的 Agent 工程教程,从概念到生产的完整学习路线 堆上的ORW HC32F460 USB CDC通信异常:非对齐访问异常排查 20260413-Hyperbridge 攻击事件:发生在默克尔山上的验证绕过 那些喊着AI 要淘汰你的人,正在靠你的焦虑赚大钱! 深度学习进阶(八)Swin Transformer 最小二乘问题详解19:带先验约束的增量式SFM优化与实现 SnapTranslate 3.0 正式发布:全局划词翻译 + 完整英语学习闭环,一站式搞定查词、记词、复习 工作的意义、工作的困难认知再思考 .NET + AI 进阶实战:基于类的技能开发 - 打造可治理的 Agent 能力模块 【从0到1构建一个ClaudeAgent】规划与协调-技能 上周热点回顾(4.6-4.12) 电子小白的工具三件套:面包板、杜邦线、万能板 单表五亿数据的查询优化 | Mysql、StarRocks 2. WorkBuddy:从“我是谁”到“帮我干活” C# 如何减少代码运行时间:7 个实战技巧 基于HelixToolkit.SharpDX 渲染3D模型 - 笺上知微 从零开始的双臂具身VLA起源及现阶段发展综述 - SkyXZ 记对 xonsh shell 的使用, 脚本编写, 迁移及调优 - pluvium27 受够了Vibe Coding的失控?换个起点,让AI事半功倍 从开始配置漏洞环境到漏洞复现流程 - 難しい 关于10年工作经验的程序员对OpenClaw的实战经验分享以及看法 - 虚无境 Any metadata 的内存布局 C# .NET 周刊|2026年3月2期 - InCerry 我帮你测过了,测试圈排名第二的 Skill 依然很牛逼 Skill Discovery | 无监督技能发现的经典工作总结 - MoonOut 上下文工程是什么?过时了么?一文讲明白! - 一枫说码 开了 TUN 模式还是直连?90% 的人都踩过这个坑 AScript扩展多种脚本语言 - rockey627 AI 学习笔记:Agent 的记忆机制 你能被装进一个文件里吗?——7 万人把同事"蒸馏"成了 AI - 我没有三颗心脏 Claude Code 通关手册(七):给 AI 装上技能包——Skills 完全指南 - 暮色之狐 在浏览器中快速编辑代码:VSCode Web 集成实践 - Newbe36524 蒸馏自己 skill?基于 Deepseek 的蒸馏器,丐版蒸馏方式,简单便捷 - To_Carpe_Diem Spring AI Aliababa和AgentScope,哪个更好? - 苏三说技术
LCA问题的高效解法:欧拉序 + RMQ 详解
LiQirui · 2026-06-19 · via 博客园_首页

\(\text{0x01}\). 问题定义 \(\text{Problem}\space \text{Definition}\).

最近公共祖先简称\(\text{LCA}\)。两个节点的最近公共祖先,就是这两个点的公共祖先里面,离根最远的那个。
为了方便,我们记某点集 \(S=\{v_1,v_2,\ldots,v_n\}\) 的最近公共祖先为 \(\text{LCA}(v_1,v_2,\ldots,v_n)\)\(\text{LCA}(S)\)

\(\text{0x02}\). 解法 \(\text{Solutions}\).

\(\text{0x02.1}\) 前置知识 \(\text{Prerequisite}.\)

欧拉序

在树上做一次深度优先遍历(DFS),每当到达一个节点(无论是首次进入,还是处理完所有子节点后回溯回来),就把该节点记录下来,由此得到的节点序列称为欧拉序(Euler tour)。
一种较为形式化描述是:
从根节点 root 开始执行以下递归过程:

%%{init: {'themeCSS': 'svg { transform: scale(0.6); transform-origin: top left; }'}}%% flowchart TD Start([开始: dfs u ]) --> RecordEntry[记录节点 u 到欧拉序] RecordEntry --> CheckChild{还有未处理的子节点?} CheckChild -- 是 --> NextChild[取下一个子节点 v] NextChild --> Recurse[[递归调用 dfs v]] Recurse --> RecordBack[记录节点 u 到欧拉序] RecordBack --> CheckChild CheckChild -- 否 --> End([结束])

RMQ

RMQ 问题指区间最值查询(Range Minimum/Maximum Query)。

\(\text{0x02.2}\) 欧拉序与RMQ问题 \(\text{Euler Tour and RMQ}.\)

好了,了解完前面的知识,我们就要引出一个重要性质:

性质1

\(pos[u]\) 为节点 u 第一次在欧拉序中出现的位置。
对于任意两个节点 u,v,设 \(l=min(pos[u],pos[v])\),$ r=max(pos[u],pos[v])$
则:

\[\text{LCA}(u,v)=arg\space min_{i \in [l,r]}\space dep[euler[i]] \]

区间 [l,r] 中深度最小的节点就是 u 与 v 的最近公共祖先。

\(\text{Proof}.\)

\(w = \text{LCA}(u, v)\)。节点 \(u\)\(v\) 分别位于 \(w\) 的不同子树中,或者其中一个就是 \(w\) 本身。
考察从首次进入 \(u\) 到首次进入 \(v\) 这段欧拉序对应的遍历过程:当 DFS 首次到达 \(u\)(位置 \(pos[u]\))后,算法会回溯到 \(w\),再向下进入 \(v\)(到达位置 \(pos[v]\))。因此 \(w\) 必然在区间 \([l, r]\) 内至少出现一次。
对于该区间内的任意一个节点 \(x\),它出现在 \(pos[u]\)\(pos[v]\) 之间,意味着在从 \(u\) 回溯到 \(w\) 并前往 \(v\) 的过程中访问了 \(x\)。由 DFS 的性质,这期间访问的所有节点都位于 \(w\) 的子树中(包括 \(w\) 本身)。故有:

\[dep[x] \ge dep[w] \]

当且仅当 \(x = w\) 时等号成立。因此,区间 \([l, r]\) 内深度最小的节点恰好就是 \(w\),即:

\[\text{LCA}(u, v) = \arg\min_{i \in [l, r]} dep[\text{euler}[i]] \]

证毕。

\(\text{0x02.2}\) RMQ求法 \(\text{RMQ Calculation Method}.\)

你该不会认为这章最简单吧
你该不会直接套ST表吧,很显然,还是太不优秀了
当然不会直接套 ST 表。注意到我们的欧拉序有一个极其特殊的性质:相邻两个元素的深度差恰好为 \(\pm 1\)。这引出了解决一般 RMQ 问题最优秀的算法之一——\(\pm 1\) RMQ,它可以将预处理复杂度压到真正的 \(O(n)\),同时保持 \(O(1)\) 查询。

\(\pm 1\) RMQ 算法

我们面对的序列 \(A[0..m-1]\)(此处 \(m = 2n-1\),即欧拉序长度)满足:

\[|A[i] - A[i-1]| = 1, \quad \forall i \in [1, m-1] \]

我们要在线回答 \(\min_{i \in [l, r]} A[i]\) 的位置。
算法的核心思想是分块 + 状态压缩
第一步:分块与块间稀疏表
令块大小 \(B = \lfloor \frac{\log_2 m}{2} \rfloor\)(在 \(n=10^5\)\(B \approx 8\))。将序列划分为 \(\lceil m/B \rceil\) 个块。
块的最小值建立稀疏表(Sparse Table)。具体地,令 \(b_i = \min\{\text{第 } i \text{ 块中的 } A \text{ 值}\}\),在数组 \(b\) 上运行标准 ST 表预处理。这一步时空复杂度均为 \(O(\frac{m}{B} \log \frac{m}{B})\),由于 \(B = \Theta(\log n)\),这其实是 \(O(n)\) 的。
第二步:块内本质类的数量
对于块内查询,若对整个数组的每一块都暴力跑 ST 表或全预处,空间会爆炸。但注意到 \(\pm 1\) 性质把块内的“形状”限制在极小的范围内。
考虑任意一个长度为 \(B\) 的块,对其内部元素作差分:

\[\Delta_k = A[\text{start}+k] - A[\text{start}+k-1] \in \{+1, -1\}, \quad k=1..B-1 \]

因此,给定该块的首元素值,整个块由这个长度为 \(B-1\)\(\pm 1\) 差分序列唯一决定其相对形态。差分序列总共只有 \(2^{B-1}\) 种。由 \(B \approx \frac{1}{2}\log_2 m\),得种类数约为:

\[2^{B-1} \le 2^{(\log_2 m)/2} = \sqrt{m} \]

这是一个远远小于 \(m\) 的数量级(\(n=10^5\) 时约 \(300\) 多种)。
第三步:块内查询的预打表
对于这至多 \(\sqrt{m}\) 种差分类型,我们完全可以预处理出每一种类型在任意区间 \([l', r']\)\(0 \le l' \le r' < B\))内的最小值位置。
第四步:回答查询
对于一个查询 \([l, r]\)

  • \(l, r\) 在同一块内,直接用块内预打表结果,\(O(1)\)
  • 若跨块,则区间被分为:左端块的后缀 + 中间若干整块 + 右端块的前缀。
    • 左端块后缀、右端块前缀:分别用块内预打表 \(O(1)\) 得到最小值位置。
    • 中间整块:在第一步的块稀疏表上做一次 \(O(1)\) RMQ。
    • 在得到的至多三个候选位置中,比较它们在原序列 \(A\) 中的深度,选取最小的那个。\(O(1)\)

\(\text{0x03}\). 时间复杂度分析 \(\text{Time Complexity Analysis}\).

\(\text{0x03.1}\) 符号约定与基础规模

设树有 \(n\) 个节点。进行一次深度优先遍历(DFS)得到欧拉序,欧拉序的长度 \(m\) 具有如下性质:

性质2(欧拉序长度)
对于 \(n \ge 1\),欧拉序的长度 \(m = 2n - 1\)
证明. 在DFS过程中,每个非根节点被记录两次:一次进入,一次回溯至父节点;根节点被记录一次进入,且最后一次回溯不产生新的记录。总计记录次数为 \(2(n-1) + 1 = 2n-1\)
\(m = \Theta(n)\),在后继复杂度分析中可直接视 \(m\)\(n\) 同阶。

我们的目标是在序列 \(A[0..m-1]\)(此处 \(A[i] = \text{dep}[\text{euler}[i]]\))上支持区间最小值位置查询,并且序列满足 \(\pm 1\) 性质:

\[|A[i] - A[i-1]| = 1,\quad \forall i\in[1,m-1] \]

\(\text{0x03.2}\) 分块与块间稀疏表

令块大小 \(B = \left\lfloor \dfrac{\log_2 m}{2} \right\rfloor\)。将 \(A\) 划分为 \(K = \left\lceil \dfrac{m}{B} \right\rceil\) 个块,记第 \(j\) 个块的最小值(按深度)为 \(b_j\)

对数组 \(b[0..K-1]\) 建立稀疏表(Sparse Table),以支持在 \(O(1)\) 时间内查询任意连续块区间的最小值位置。稀疏表的预处理时间复杂度为 \(O(K \log K)\),空间复杂度也为 \(O(K \log K)\)

块间稀疏表复杂度计算:
已知 \(B = \left\lfloor \frac{1}{2}\log_2 m \right\rfloor = \Theta(\log n)\),于是块数

\[K = \left\lceil \frac{m}{B} \right\rceil = O\!\left(\frac{m}{\log m}\right) = O\!\left(\frac{n}{\log n}\right). \]

代入稀疏表开销:

\[O(K \log K) = O\!\left( \frac{m}{\log m} \cdot \log\!\left(\frac{m}{\log m}\right) \right) = O\!\left( \frac{m}{\log m} \cdot (\log m - \log\log m) \right) = O(m) = O(n). \]

因此块间稀疏表的预处理时间与空间均为 \(O(n)\)

\(\text{0x03.3}\) 块内本质类的数量与预打表

对于任意一个长度为 \(B\) 的块,给定其首元素的值,该块的相对形态完全由长度为 \(B-1\) 的差分序列

\[\Delta_k = A[\text{start}+k] - A[\text{start}+k-1] \in \{+1,-1\},\quad k=1..B-1 \]

决定。因此可能的差分序列种类数为

\[C = 2^{B-1}. \]

\(B\) 的定义可得上界:

\[B-1 < \frac{\log_2 m}{2} \quad\Rightarrow\quad C = 2^{B-1} < 2^{\frac{1}{2}\log_2 m} = \sqrt{m}. \]

\(C = O(\sqrt{m}) = O(\sqrt{n})\),种类数量远小于 \(n\)

对每一种差分类型,我们都可以预处理出一个 \(B \times B\) 的查询表,存放该块内所有区间 \([l',r']\)\(0 \le l' \le r' < B\))上最小值的位置(相对于块起始的偏移)。构建该表的时间为 \(O(B^2)\),空间也为 \(O(B^2)\)

块内预打表复杂度计算:
类型数 \(C = O(\sqrt{m})\),每类耗时 \(O(B^2)\),因此总预处理时间为

\[O(C \cdot B^2) = O\!\left( \sqrt{m} \cdot \left(\frac{\log m}{2}\right)^2 \right) = O\!\left( \sqrt{n} \cdot \log^2 n \right) = o(n). \]

同样,块内打表所需额外空间为 \(O(\sqrt{n} \cdot \log^2 n) = o(n)\)

\(\text{0x03.4}\) 单次查询的时间复杂度

对于一次区间查询 \([l, r]\),分为两种情况:

  1. \(l\)\(r\) 位于同一块内:直接使用该块对应的块内预打表,在 \(O(1)\) 时间内返回最小值位置。
  2. \(l\)\(r\) 跨越多个块
    • 左端块的后缀查询:使用块内预打表,\(O(1)\)
    • 中间连续整块的查询:使用块间稀疏表进行一次 RMQ,\(O(1)\)
    • 右端块的前缀查询:使用块内预打表,\(O(1)\)
    • 比较上述至多三个候选位置的深度值,取深度最小者,\(O(1)\)

因此无论何种情形,单次查询均可在严格 \(O(1)\) 时间内完成。

\(\text{0x03.5}\) 总体复杂度

将欧拉序生成、深度数组计算、\(\pm 1\) RMQ 预处理与单次查询的开销汇总如下:

阶段 时间复杂度 空间复杂度
DFS 生成欧拉序、深度及首次出现位置 \(O(n)\) \(O(n)\)
块间稀疏表预处理 \(O(n)\) \(O(n)\)
块内本质类预打表 \(o(n)\)(亚线性) \(o(n)\)(亚线性)
总预处理 \(O(n)\) \(O(n)\)
单次 LCA 查询 \(O(1)\)

\(\text{0x04}\). 代码实现 \(\text{Code implementation}\).

自己写,不要总想抄。这是信息学竞赛,不是文科。
以P3379为题

#include<bits/stdc++.h>
using namespace std;

int block_size;
vector<int> eul,dep,fst;
vector<vector<int>> tree;
vector<int> block,belong,lookup;

void dfs(int now,int pre,int step){
    dep[now]=step;
    eul.push_back(now);
    fst[now]=min(fst[now],(int)eul.size()-1);
    for(int i=0;i<tree[now].size();i++){
        if(tree[now][i]==pre){
            continue;
        }
        int nxt=tree[now][i];
        dfs(nxt,now,step+1);
        eul.push_back(now);
    }
    return ;
}

int main(){
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
    int n,m,s;
    cin >> n >> m >> s;
    fst.resize(2*n+1,INT_MAX);
    dep.resize(n+1);
    tree.resize(n+1);
    for(int i=0;i<n-1;i++){
        int x,y;
        cin >> x >> y;
        tree[x].push_back(y);
        tree[y].push_back(x);
    }
    eul.reserve(2*n-1);
    dfs(s,-1,0);
    int len=eul.size();
    vector<int> dep_eul(len);
    for(int i=0;i<len;++i) dep_eul[i]=dep[eul[i]];
    int B=1;
    if(len>2){
        int log_len=31-__builtin_clz(len);
        B=max(1,log_len/2);
    }
    int nb=(len+B-1)/B;
    block.resize(len);
    vector<int> type(nb,0);
    for(int i=0;i<len;++i) block[i]=i/B;
    for(int b=0;b<nb;++b){
        int L=b*B,R=min(len,L+B);
        int mask=0;
        for(int i=L+1;i<R;++i) if(dep_eul[i]>dep_eul[i-1]) mask|=(1<<(i-L-1));
        type[b]=mask;
    }
    int tot=(B>1)?(1<<(B-1)):1;
    lookup.assign(tot*B*B,0);
    for(int mask=0;mask<tot;++mask){
        vector<int> rel(B,0);
        for(int i=1;i<B;++i){
            if((mask>>(i-1))&1) rel[i]=rel[i-1]+1;
            else rel[i]=rel[i-1]-1;
        }
        int base=mask*B*B;
        for(int l=0;l<B;++l){
            int p=l;
            for(int r=l;r<B;++r){
                if(rel[r]<rel[p]) p=r;
                lookup[base+l*B+r]=p;
            }
        }
    }
    vector<int> bmin(nb);
    for(int b=0;b<nb;++b){
        int L=b*B,R=min(len,L+B);
        int best=L;
        for(int i=L+1;i<R;++i) if(dep_eul[i]<dep_eul[best]) best=i;
        bmin[b]=best;
    }
    int K=0;
    while((1<<K)<=nb) ++K;
    vector<vector<int>> st(nb,vector<int>(K));
    for(int i=0;i<nb;++i) st[i][0]=bmin[i];
    for(int j=1;j<K;++j){
        for(int i=0;i+(1<<j)-1<nb;++i){
            int a=st[i][j-1],b=st[i+(1<<(j-1))][j-1];
            st[i][j]=dep_eul[a]<dep_eul[b]?a:b;
        }
    }
    auto qblock=[&](int b,int l,int r)->int{
        int idx=type[b]*B*B+l*B+r;
        return b*B+lookup[idx];
    };
    auto rmq=[&](int l,int r)->int{
        if(l>r) swap(l,r);
        int bl=block[l],br=block[r];
        if(bl==br) return qblock(bl,l-bl*B,r-bl*B);
        int a1=qblock(bl,l-bl*B,B-1);
        int a2=qblock(br,0,r-br*B);
        int best=dep_eul[a1]<dep_eul[a2]?a1:a2;
        if(bl+1<=br-1){
            int midlen=br-bl-1;
            int k=31-__builtin_clz(midlen);
            int m1=st[bl+1][k];
            int m2=st[br-(1<<k)][k];
            int amid=dep_eul[m1]<dep_eul[m2]?m1:m2;
            if(dep_eul[amid]<dep_eul[best]) best=amid;
        }
        return best;
    };
    while(m--){
        int u,v;
        cin>>u>>v;
        int l=fst[u],r=fst[v];
        if(l>r) swap(l,r);
        int pos=rmq(l,r);
        cout<<eul[pos]<<'\n';
    }
    return 0;
}