模拟赛树上询问(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}\)

也就是说——

\[O(nm)\approx O(10^{10}) \]

???!!!这么阴的吗...
那么,我们就不得不考虑如何优化时间了。

正解

优化思路

如何优化时间复杂度呢?
似乎在查找路径(LCA)方面没有什么更好的算法了,但是我们可以尝试把算法的运用优化一下。
注意到这题其实 没有要修改的点,也就是说可以 离线 解决问题

离线:指不实时回答问题,将问题全部记录以后再回答
e.g. 正常:问->处理->答->问->处理->答; 离线:问->问->集中处理->答->答

想离线就必须合理存储,可是我们怎么存、存什么呢?

推导过程

敲黑板 这是重点!
每个问题的答案 都可以看做求树上路径 \(l \to lca(l,r) \to r\) 中满足

\[ i=\begin{cases} dep_l-(dep_i-dep_{lca}) & {(i 位于 l \text{→} lca路径)}\\ dep_i-dep_{lca}+dep_l-dep_{fa_{lca}} & {(i 位于 r \text{→} lca路径)} \end{cases} \]

的点数,也就是满足

\[\begin{cases} i+dep_i=dep_l+dep_{lca} &{(i 位于 l \text{→} lca路径)}\\ i-dep_i=dep_l-dep_{lca}-dep_{fa_{lca}} &{(i 位于 r \text{→} lca路径)} \end{cases} \]

的所有点的个数.

注意到,这里 等式右边的量(我们令他为 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)👍?
到这里.早/午/晚安!

这个题的思路还可以用于这个题.




posted @ 2026-06-04 22:54  听凭风引adj  阅读(22)  评论(0)    收藏  举报