模拟赛树上询问(query)超细题解+学习笔记!
树上询问(query)题解
题目描述
你有一棵 \(n\) 节点的树 \(T\),你要回答 \(m\) 个询问,每次询问给定两个整数 \(l,r\),
问存在多少个整数 \(k\) 使得从 \(l\) 沿着 \(l \to r\) 的简单路径走 \(k\) 步恰好到达 \(k\)。
输入
第一行,两个整数 \(n,m\) 表示节点数和询问数。
下面 \(n−1\) (注意是树所以 \(n-1\) )行,每行两个整数 \(u,v\) 表示一条边。
之后 \(m\) 行,每行两个整数 \(l,r\) 表示一个询问,题意同题目描述。
输出
共 \(m\) 行,对于每个询问单独输出一行表示你的答案。
样例
输入
9 3
5 4
4 3
3 7
4 1
1 6
1 8
1 9
5 2
6 7
2 3
3 2
输出
2
1
0

限制条件
数据保证合法且为一棵树。对于 \(100 \%\) 的数据,有 \(2\leqslant n,m \leqslant 3 \times 10^5.\)
人话解释:
在树上指定 路径 \(l \to r\) 中, 走多少步数该点编号就是多少, 求这样的点的个数
(好像还是不够简洁)
正片开始!
瞎扯开始
思考
首先,一看题我们就知道,要想求解必须算出树上路径是谁————
——叫你呢LCA!
有了它我们就能方便快速的求路径啦
\(Tips\) : LCA指求两点树上最近公共祖先的一种算法,路径为\(l \to lca(l,r) \to r\)
LCA你肯定学过叭qwq其实能做这题的应该都学过吧...
什么,你没学过OAO?
orz (教练的blog)LCA教程
下面只给出标准代码模板
#include<bits/stdc++.h>
using namespace std;
int N;
int dep[N]//dep[i]在树上点i的深度
int f[N][20];//f[i][j]是一个类似st表的东西
//表示树上节点i的第2^j辈父节点是f[i][j]
int LCA(int x, int y){
if(dep[x]>dep[y])swap(x,y);//让y始终是深度较深(辈分较低)的那个
for(int i=20; i>=0; i--){//之所以要从大到小是为了防止跳过头
if(dep[f[y][i]]>=dep[x]) y=f[y][i];
}//使x与y在同一深度再一起向上找公共祖先
if(x==y)return x;//<--若x就是y的直接祖先,显然x就是LCA
//一起跳但是始终不跳到祖先(也是防止跳过头)
for(int i=20; i>=0; i--){
if(f[x][i]!=f[y][i]){
x=f[x][i];
y=f[y][i];
}
}
return f[x][0];//刚刚没到祖先,再跳一级就行
}
这样我们既得到了一个相当接近正解的解法:先找每个询问的两点LCA,一步一步找路径即可。部分代码如下
Code
#include<bits/stdc++.h>
using namespace std;
void dfs(int s, int fa)//标准版子不多说,可以看看教程
int Lca(int x, int y)//标准版子*2
int getans(int ls, int r, int lca){
int ans=0;
int l=ls;
while(l!=lca){//求l-->LCA
l=f[l][0];//思考:可能存在l是满足要求的点吗?
if(l==dep[ls]-dep[l])ans++;
}
while(r!=lca){//求r-->LCA(这里把路径颠倒没有问题,只要注意步数怎么求就行)
if(r==dep[r]+dep[ls]-2*dep[lca])ans++;//思考:为什么这句反而要写在前面?
r=f[r][0];
}
//思考:为什么不用单独算点LCA,l和r?
//建议你可以琢磨一下这一段是怎么实现的
return ans;
}
//其他定义与读入读出不过多赘述,输出如下
cout<<getans(l,r,Lca(l,r))<<endl;
win题来了:
为什么说是接近正解呢?因为你交上去就会发现——
TLE 70pts...?
啊哈, (盲生你发现了华点)
看看复杂度:\(O(nm)\) (极端情况下每次询问要把整个树(链)遍历一遍)
再看数据范围: \(n,m \leqslant 3 \times 10^{5}\)
也就是说——
???!!!这么阴的吗...
那么,我们就不得不考虑如何优化时间了。
正解
优化思路
如何优化时间复杂度呢?
似乎在查找路径(LCA)方面没有什么更好的算法了,但是我们可以尝试把算法的运用优化一下。
注意到这题其实 没有要修改的点,也就是说可以 离线 解决问题
离线:指不实时回答问题,将问题全部记录以后再回答
e.g. 正常:问->处理->答->问->处理->答; 离线:问->问->集中处理->答->答
想离线就必须合理存储,可是我们怎么存、存什么呢?
推导过程
敲黑板 这是重点!
每个问题的答案 都可以看做求树上路径 \(l \to lca(l,r) \to r\) 中满足
的点数,也就是满足
的所有点的个数.
注意到,这里 等式右边的量(我们令他为 K) 在每一个问题中 都可以直接确定为定值 且只需一次计算,只有等式左边的 \(i \plusmn dep_i\) 需要特殊逐点 记录 (记住是 记录 ,不会修改,后面会考)
然后我们就能把问题拆成离线的,将每一个问题的答案储存在 \(ans_i\) 里,同时把询问 "挂" 在树上即可 (学长的比喻,很巧妙) ,所以我们需要一个结构体来进行挂点:
struct asks{
int id;//表示该问题是询问的第几个问题
bool kind;//表示先前 i+/-dep[i]的符号,正1负0
bool flag;//这里卖个关子,后面会讲
int K;//表示判定式的值 (等式右边)
};
在问询时把每一个问题的\(kind\)和\(X\)挂到树上,就可以通过一次dfs离线解决所有点的问题啦
需要注意的是,由于一个点不一定只有一个问题,所以直接定义一维数组挂点会出问题,最简单的解决方法就是改用vector存储,既能避免MLE又保证了将所有问题记录完整,所以在dfs时会多一层循环遍历所有问题。
wait a minute,
这时就会有 着急 聪明的同学就要问了:怎么dfs?
这谁不会,你都挂好点了直接跑不就行了?不兑谁问你了?
哎,问题就出在这里,如果你直接莽的话,打到一半就会发现:
不兑啊我不是要优化复杂度吗怎么还是\(O(nm)\)啊甚至耗时很长了你这题解有问题吧
咳咳,说话要加标点不然会呛到 题解是没有问题的,可能你来的太早我还没写下文
上面的离线只是一个铺垫, 重点在下面
真·正解
我们用vector已经将所有问题挂到树上了,但是要怎么集中解决他们还是个问题。也许你会想到要 把问题标记打到 \(lca\)上,dfs遇到标记就记录问题继续向下找 l 和 r,但这其实仍然行不通,并没有什么实质性优化,实际上还是要每问一遍遍历一遍。
于是我们要引出本题的最大\(Boss\)——
打表(bush)
逐点记录&回退处理
\(\scriptsize \xcancel{其实这是我瞎起的名我也不知道我在说什么总之很玄妙就是了}\)
后来知道这题其实是 $ 树上差分+Lca+桶存储 $ awa
我们额外引入两个数组 \(s1_x \ s2[x]\),作为类似于 "树上前缀和" 的东西,使得
\(s1_x\) 表示 从根节点到 某一结点 满足 {\(i+dep_i\) 的值是 \(x\)} 的点有多少个;
\(s2_x\) 表示 从根节点到 某一结点 满足 {\(i-dep_i\) 的值是 \(x\)} 的点有多少个.
这个 某一结点 指的是在dfs的过程中所处的位置,每次进入或退出一个点都会变化
以防你不知道树上前缀和原理:
指定链总和=左链前缀和+右链前缀和-2*{lca的父节点}前缀和
然后我们在每一次进入节点时访问一下这个节点的信息,把相对应的信息(X和Y)利用s1和s2记录下来,这样在遇到 有问题的节点 时就可以根据现在的 前缀和 (s1和s2) 求出该问题所对应的答案。
不得不注意的是,因为我们实际上用的是答案的前缀和,所以在把问题挂到树上的过程中,要注意应该在lca节点挂两次,并且让答案 减去 这个子问题的结果 (这个加减标记就是上面结构体中\(flag\)的用处) (在下图中我们称之为负问题),这样才能保证结果是正确的。
图示:
(以样例中第二个询问为例)

注意到上图中记录的X和Y可能是负的,所以要开大概 \(s1[2*N],s2[2*N]\) (N是i+dep[i]的极大值)用来映射负数
以上就是本体的大致思路,希望对你有用 (>w<)/💗
Accepted Code
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
自己打awa
绝对不是我打完题解代码还没打~(我是不会告诉你倒数第六个点其实是能点的!>wO)
结语
非常抱歉前几篇文章全都烂尾了,而且有不少(像这篇)都还没校对就发出来了,给大佬们带来诸多误导,还请谅解orz
建议神犇们可以自己试一试打一打,有帮助的话能不能给个推荐(OwO)👍?
到这里.早/午/晚安!
这个题的思路还可以用于这个题.

浙公网安备 33010602011771号