2021牛客暑期多校训练营9 E题: Eyjafjalla

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 n1 条无向边相连,第 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 tir 的祖先) v v v ,从而问题转化为求以 v v v 为根的子树中满足 t i ≥ l t_i\ge l til 的节点个数(因为 t v ≤ r t_v\le r tvr ,而 v v v 的子树中的节点都不小于 v v v 的深度,因此子树中的节点都应满足 t i ≤ r t_i\le r tir ,只需考虑 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 tix 的节点个数的前缀和 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} sumrsuml1 得到。

询问次数很多,若每次直接计算最坏复杂度为 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 ltir 的节点总数,那么我们从头开始遍历整个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 1ti109 直接建树会炸),记录下所有出现过的可能温度(包括城市温度 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;
}
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值