E题: Eyjafjalla
原题链接:https://ac.nowcoder.com/acm/contest/11260/E
题目大意
火山国家有 n n n 个城市,编号从 1 1 1 到 n n n ,城市 1 1 1 是首都,有一座大火山。城市 i i i 的温度是 t i t_i ti 。
n n n 个城市用 n − 1 n-1 n−1 条无向边相连,第 i i i 条边连接城市 u i u_i ui 和 v i v_i vi 。如果 u i u_i ui 比 v i v_i vi 更接近首都,则 t u i > t v i t_{u_i}>t_{v_i} tui>tvi 。首都的温度是最高的。
如果在城市
x
x
x 爆发生存温度为
[
l
,
r
]
[l,r]
[l,r] 的病毒,求会感染多少城市。
若一个城市的温度位于
[
l
,
r
]
[l,r]
[l,r] 之内,且它与另一个受感染的城市相连,则它也会被感染。
题解
根据题目的描述,我们可以将这些城市看作一棵以
1
1
1 为根的树,其中若两点位于同一条从根出发的链上,则深度越大者温度越低。
同时考虑多个方向的传播并不方便,根据温度的单调性,我们可以通过倍增的方式快速找到病毒从
x
x
x 向根方向传播的最远点(即离
1
1
1 最近的
t
i
≤
r
t_i\le r
ti≤r 的祖先)
v
v
v ,从而问题转化为求以
v
v
v 为根的子树中满足
t
i
≥
l
t_i\ge l
ti≥l 的节点个数(因为
t
v
≤
r
t_v\le r
tv≤r ,而
v
v
v 的子树中的节点都不小于
v
v
v 的深度,因此子树中的节点都应满足
t
i
≤
r
t_i\le r
ti≤r ,只需考虑
l
l
l 即可)。
对于一棵子树内的查询,我们可以通过跑dfs,记录每个节点递归进入与递归返回时的时间戳,用于表示其子树的范围,并将每个点根据访问顺序(即递归进入时间戳)进行记录,从而将树转化为数组,将子树内的查询转化为区间的查询。
见下例(dfn表示递归进入时的时间戳,edn表示递归返回时的时间戳):
数组表示为:1 2 3 4 5 6 7。
若我们查询以
2
2
2 为根的子树,则区间为(dfn:2 end:6),即1 2 3 4 5 6 7。
我们可以计算满足
t
i
≥
x
t_i\ge x
ti≥x 的节点个数的前缀和
s
u
m
i
sum_i
sumi (不直接遍历区间而计算前缀和,目的是为了方便下文在离线过程中同时计算多个查询),对于查询区间
[
l
,
r
]
[l,r]
[l,r] ,我们可以通过
s
u
m
r
−
s
u
m
l
−
1
sum_r-sum_{l-1}
sumr−suml−1 得到。
询问次数很多,若每次直接计算最坏复杂度为
O
(
n
2
)
O(n^2)
O(n2) ,显然会TLE。
我们不妨建立线段树,范围为
[
l
,
r
]
[l,r]
[l,r] 的节点记录满足
l
≤
t
i
≤
r
l\le t_i\le r
l≤ti≤r 的节点总数,那么我们从头开始遍历整个dfs数组,每次对于一个节点
x
x
x 我们单点更新
t
x
t_x
tx ,然后离线化存储每个询问所需的前缀和位置(同时存储该次询问的病毒的最低生存温度
l
l
l ),扫描到需要询问的位置查询线段树即可。
实现过程中需要离散化( 1 ≤ t i ≤ 1 0 9 1\le t_i\le 10^9 1≤ti≤109 直接建树会炸),记录下所有出现过的可能温度(包括城市温度 t i t_i ti 和病毒最低生存温度 l i l_i li ,最高温度不会在线段树中使用,可以不用记录),排序后每个温度建立映射,根据可能温度总数建树即可。
参考代码
#include<bits/stdc++.h>
#define For(i,n,m) for(int i=n;i<=m;i++)
#define FOR(i,n,m) for(int i=n;i>=m;i--)
using namespace std;
void read(int &x){
int ret=0;
char c=getchar(),last=' ';
while(!isdigit(c))last=c,c=getchar();
while(isdigit(c))ret=ret*10+c-'0',c=getchar();
x=last=='-'?-ret:ret;
}
const int MAXN=1e5+5;
int n,m,t[MAXN],f[MAXN][25];
int cnt,dfn[MAXN],edn[MAXN],node[MAXN];//dfn记录递归进入时间戳(区间起始点),edn记录递归返回时间戳(区间结束点),node为树展开形成的数组
int ans[MAXN];//用于离线记录每次询问的答案
vector<int>e[MAXN];//记录树的边
struct que{//记录每次询问
int id,l;//id表示第几次询问,l表示该次的病毒最低生存温度
};
vector<que>l[MAXN],r[MAXN];//分别记录需查询的前缀和l-1和r
struct Node{
int l,r,sum;//线段树,sum表示区间和
}segtree[MAXN*2<<2];
void pushup(int x){segtree[x].sum=segtree[x<<1].sum+segtree[x<<1|1].sum;}
void build(int x,int l,int r){//建树
segtree[x].l=l,segtree[x].r=r,segtree[x].sum=0;
if(l==r)return;
int mid=l+r>>1;
build(x<<1,l,mid);
build(x<<1|1,mid+1,r);
}
void update(int x,int q){//单点更新,温度为q
if(segtree[x].l==q&&segtree[x].r==q){
segtree[x].sum++;//该温度的节点数量++
return;
}
int mid=segtree[x].l+segtree[x].r>>1;
if(q<=mid)update(x<<1,q);
else update(x<<1|1,q);
pushup(x);//更新
}
int query(int x,int l,int r){//查询
if(segtree[x].l==l&&segtree[x].r==r)return segtree[x].sum;
int mid=segtree[x].l+segtree[x].r>>1;
if(r<=mid)return query(x<<1,l,r);
if(l>mid)return query(x<<1|1,l,r);
return query(x<<1,l,mid)+query(x<<1|1,mid+1,r);
}
void dfs(int x,int fa){
cnt++;
dfn[x]=cnt;//记录递归进入时间戳
node[cnt]=x;//根据dfs序更新数组
f[x][0]=fa;//记录初步倍增父亲
For(i,1,20)f[x][i]=f[f[x][i-1]][i-1];//重复更新倍增的父亲
int son;
For(i,0,e[x].size()-1){//遍历子节点
son=e[x][i];
if(son==fa)continue;
dfs(son,x);
}
edn[x]=cnt;//记录递归返回时间戳
}
int main()
{
read(n);
int u,v;
For(i,1,n-1){
read(u),read(v);
e[u].push_back(v);
e[v].push_back(u);
}
set<int>st;//记录出现过的温度
map<int,int>to;//用于建立映射,实现离散化
For(i,1,n){
read(t[i]);
st.insert(t[i]);//记录温度
}
dfs(1,0);//跑dfs
int q,x,xl,xr;
read(q);
For(i,1,q){
read(x),read(xl),read(xr);
st.insert(xl);//记录xl温度,xr记录不是必要的
if(t[x]<xl||t[x]>xr)continue;//若初始城市病毒无法生存则直接跳过,默认ans为0即可
FOR(j,20,0)if(t[f[x][j]]<=xr&&f[x][j])x=f[x][j];//寻找最靠近根的合法祖先,注意判断f[x][j]非0(1没有父亲,以0作为无该点的标记)
l[dfn[x]-1].push_back({i,xl}),r[edn[x]].push_back({i,xl});//存入离线询问的记录数组中,注意因为是前缀和所以l是dfn[x]-1
}
m=st.size();//记录总温度数便于建树和查询
int pos=1;
set<int>::iterator it=st.begin();
while(it!=st.end()){//从头到尾遍历整个集合
to[*it]=pos++;//将该数值的点映射为一个新的pos
it++;
}
build(1,1,m);//建树,叶节点数为m
For(i,1,n){
update(1,to[t[node[i]]]);//将该点的温度的映射更新到线段树中
if(l[i].size()){//若有起始点询问
For(j,0,l[i].size()-1){
ans[l[i][j].id]=query(1,to[l[i][j].l],m);//范围是温度的映射到m(m可以换为读入离线询问时的xr,但是只有符合区间的值在前缀和中才可能满足t[i]<=xr,所以不用记录直接将上界定为m可以节省内存(其实是懒得整))
}
}
if(r[i].size()){//若有结束点询问
For(j,0,r[i].size()-1){
ans[r[i][j].id]=query(1,to[r[i][j].l],m)-ans[r[i][j].id];//查询此时的温度的映射到m,减去l时的前缀和即是区间满足生存温度的节点数量
}
}
}
For(i,1,q)printf("%d\n",ans[i]);//根据输入顺序输出答案
return 0;
}