[CodeChef]Children Trips

98 篇文章 0 订阅
13 篇文章 0 订阅

题目

传送门 to CodeChef

传送门 to VJ

题意概要
给定带边权树 T T T 。定义 a a a b b b 的 “旅行” 为,最初位置在 a a a,每次选择 c ∈ Path ( a , b ) c\in\text{Path}(a,b) cPath(a,b) 使得 dis ( a , c ) ⩽ z \text{dis}(a,c)\leqslant z dis(a,c)z 并最大化之,然后移动到 c c c ;移动到 b b b 时结束。

q q q 个询问,每次给定 a , b , z a,b,z a,b,z,请求出 a a a b b b 的 “旅行” 所需的移动次数。

数据范围与约定
max ⁡ ( n , q ) ⩽ 1 0 5 \max(n,q)\leqslant 10^5 max(n,q)105 。边权 ∈ [ 1 , 2 ] \in[1,2] [1,2] 。移动距离限制 z ∈ [ 2 , 2 n ] z\in[2,2n] z[2,2n] 。时限 8 s \rm 8s 8s

思路

第一步转化就很关键。至少我在看论文前还没往那个方面想。

显然每一步尽量走长,等价于用最少的步数。可是这样在 l c a lca lca 那里拐弯之后,就会得到一个奇怪的问题,即自顶向下走。这是很麻烦的一件事,因为 儿子往往比父亲难找到

仔细想想, a a a 走到 b b b b b b 走到 a a a 是等价的。那么从 b b b 出发尽量走长,从 a a a 出发也尽量走长,最后 在中间相遇,也是可以的!问题终于被规约到了从底往上走。

考虑树上问题的传统算法,比如倍增、树链剖分等,都面临一个问题:如果对不同的 z z z 都存储结果,根本存不下;如果只是笼统地存距离等,则区间信息无法合并。也就是说,不同的 z z z 信息不互通。因此,我们应考虑更暴力的方法。

分块正解

由于边权十分特殊,我们会发现如下特性:

  • 路径长度最大 2 n 2n 2n
  • 每一次至少会走 ( z − 1 ) (z{-}1) (z1) 的长度。
  • 进而有 z > n z>\sqrt{n} z>n 时,最多走 O ( n ) \mathcal O(\sqrt n) O(n ) 次。

同时,第一点让我们可以离线 O ( 1 ) \mathcal O(1) O(1) 处理树上 z z z 级祖先(走长度为 z z z 的路径)的询问——自顶向下 d f s \rm dfs dfs,维护映射 f ( d e p t h ) = x f(depth)=x f(depth)=x ;查询 f ( d e p t h − z ) f(depth{-}z) f(depthz) f ( d e p t h − z + 1 ) f(depth{-}z{+}1) f(depthz+1) 即可。

于是,对于 z > n z>\sqrt{n} z>n ,我们就用上面这种方法,暴力跳祖先——在 d f s \rm dfs dfs 时先递归子树,使得标记被 “上传”,然后继续往上跳跃就好了。每个询问均摊 O ( n ) \mathcal O(\sqrt{n}) O(n )

z ⩽ n z\leqslant\sqrt{n} zn 只有 O ( n ) \mathcal O(\sqrt{n}) O(n ) 种,不妨把每一种的显式树(好像更可能是森林)建出来,即每个点下一步会走到哪里。运用上面的 “离线” 做法,每种 z z z 可以 O ( n ) \mathcal O(n) O(n) 建树。

在显式树上,只需要找到 x x x 恰好比 l c a lca lca 深的祖先。方案很多,比如倍增或树剖,或者 d f s \rm dfs dfs 时维护 f ( d e p t h ) = x f(depth)=x f(depth)=x 然后二分查找。时间复杂度 O [ ( n + q ) n + q log ⁡ n ] \mathcal O[(n{+}q)\sqrt{n}+q\log n] O[(n+q)n +qlogn]

在线询问

如果询问在线呢?对于 z > n z>\sqrt{n} z>n 的情况,需要在线的 k k k 级祖先,只需要 O ( n log ⁡ n ) − O ( 1 ) \mathcal O(n\log n)-\mathcal O(1) O(nlogn)O(1) 的 “长链剖分 + + + 倍增” 在线方法。对于 z ⩽ n z\leqslant\sqrt{n} zn ,需要定位 l c a lca lca 的位置,可以树链剖分,在最后一条链上二分。时间复杂度不变。

当然,也可以树上分块——实际上是设置关键点,用关键点将树上路径给割开。有两种方案:贪心地递归选取关键点,若子树内某个深度 ⩾ n \geqslant\sqrt{n} n 的点到当前点路径上没有关键点,则设置该点为关键点;或者,选取子树 ⩾ n \geqslant\sqrt{n} n 的、深度为 n \sqrt{n} n 的倍数的点。

可以证明点数不超过 O ( n ) \mathcal O(\sqrt{n}) O(n ),且每个点的祖先中最近的关键点的距离为 O ( n ) \mathcal O(\sqrt{n}) O(n ) 。对于每个 z ⩽ n z\leqslant\sqrt{n} zn ,预处理每个点到祖先(不含自己)中最近关键点的信息。显然跳 O ( n ) \mathcal O(\sqrt{n}) O(n ) 次就能走到 l c a lca lca 了。

但这样会带来 O ( n n ) \mathcal O(n\sqrt{n}) O(nn ) 的空间开销,不太现实。所以也只是提一提。

代码

注意:在 l c a lca lca 一步可达时,就要停止。因为此时走到 l c a lca lca 就不一定能够充分利用移动距离限制了。

#include <cstdio> // megalomaniac JZM yydJUNK!!!
#include <iostream> // Almighty XJX yyds!!!
#include <algorithm> // decent XYX yydLONELY!!!
#include <cstring> // Pet DDG yydOLDGOD!!!
#include <cctype> // oracle: ZXY yydBUSlut!!!
#include <vector>
typedef long long llong;
# define rep(i,a,b) for(int i=(a); i<=(b); ++i)
# define drep(i,a,b) for(int i=(a); i>=(b); --i)
# define rep0(i,a,b) for(int i=(a); i!=(b); ++i)
inline int readint(){
	int a = 0, c = getchar(), f = 1;
	for(; !isdigit(c); c=getchar()) if(c == '-') f = -f;
	for(; isdigit(c); c=getchar()) a = a*10+(c^48);
	return a*f;
}

const int MAXN = 100005;
struct Edge { int to, nxt, val; };
Edge e[MAXN<<1]; int head[MAXN], cntEdge;
inline void addEdge(int a, int b, int c){
	e[cntEdge] = Edge{b,head[a],c}, head[a] = cntEdge ++;
}
# define _go(i,x) for(int i=head[x]; ~i; i=e[i].nxt)

int dep[MAXN], siz[MAXN], son[MAXN], fa[MAXN];
void scan(int x, int pre){
	siz[x] = 1, son[x] = 0;
	_go(i,x) if((i^1) != pre){
		dep[e[i].to] = dep[x]+e[i].val, fa[e[i].to] = x;
		scan(e[i].to,i), siz[x] += siz[e[i].to];
		if(siz[e[i].to] > siz[son[x]]) son[x] = e[i].to;
	}
}
int top[MAXN];
void chainSplit(int x, int bel){
	top[x] = bel; if(son[x]) chainSplit(son[x],bel);
	_go(i,x) if(e[i].to != fa[x] && e[i].to != son[x])
		chainSplit(e[i].to,e[i].to);
}
int getLca(int a, int b){
	while(top[a] != top[b])
		if(dep[top[a]] < dep[top[b]]) b = fa[top[b]];
		else a = fa[top[a]]; // deeper one go up
	return dep[a] < dep[b] ? a : b;
}

struct Query{ int a, b, z, lca, ans; };
Query xw[MAXN]; ///< inquiries

int dep_to_node[MAXN<<1];
std::vector<int> ask[MAXN];
void dfs_big(int x, int pre){
	dep_to_node[dep[x]] = x;
	_go(i,x) if((i^1) != pre) dfs_big(e[i].to,i);
	for(const int &id : ask[x]){
		if(dep[xw[id].lca] > dep[x]-xw[id].z) continue;
		int nxt = dep_to_node[dep[x]-xw[id].z];
		if(!nxt) nxt = dep_to_node[dep[x]-xw[id].z+1];
		(xw[id].a == x ? xw[id].a : xw[id].b) = nxt;
		ask[nxt].push_back(id), ++ xw[id].ans;
	}
	ask[x].clear(), dep_to_node[dep[x]] = 0;
}

std::vector<int> gra[MAXN];
void build_small(const int &z, int x, int pre){
	dep_to_node[dep[x]] = x;
	if(dep[x] > z){ // find father first
		int nxt = dep_to_node[dep[x]-z];
		if(!nxt) nxt = dep_to_node[dep[x]-z+1];
		gra[nxt].push_back(x);
	}
	_go(i,x) if((i^1) != pre) build_small(z,e[i].to,i);
	dep_to_node[dep[x]] = 0; // clear is needed
}
typedef std::pair<int,int> PII;
std::vector<PII> sta; ///< first = dep, second = id
void dfs_small(const int &x){
	sta.push_back(PII{dep[x],x});
	for(const int &id : ask[x]){
		if(xw[id].lca == x) continue; // here it is
		int nxt = int(std::lower_bound(sta.begin(),
			sta.end(),PII{dep[xw[id].lca]+1,0})-sta.begin());
		xw[id].ans += int(sta.size())-nxt-1;
		(xw[id].a == x ? xw[id].a : xw[id].b) = sta[nxt].second;
	}
	for(const int &ch : gra[x]) dfs_small(ch);
	gra[x].clear(), ask[x].clear(), sta.pop_back();
	dep_to_node[dep[x]] = 0;
}

const int SQRTN = 316;
int main(){
	int n = readint();
	memset(head+1,-1,n<<2);
	for(int i=1,a,b,c; i!=n; ++i){
		a = readint(), b = readint(), c = readint();
		addEdge(a,b,c), addEdge(b,a,c);
	}
	dep[1] = 1, scan(1,-1), chainSplit(1,0);
	int q = readint();
	rep(i,1,q){
		xw[i].a = readint(), xw[i].b = readint(), xw[i].z = readint();
		xw[i].lca = getLca(xw[i].a,xw[i].b);
		if(xw[i].z > SQRTN){
			ask[xw[i].a].push_back(i);
			ask[xw[i].b].push_back(i);
		}
	}
	dfs_big(1,-1); // solve all
	rep(z,2,SQRTN){
		rep(i,1,q) if(xw[i].z == z){
			ask[xw[i].a].push_back(i);
			ask[xw[i].b].push_back(i);
		}
		build_small(z,1,-1);
		rep(i,1,n) if(dep[i] <= z) dfs_small(i);
	}
	rep(i,1,q){
		int lst = dep[xw[i].a]+dep[xw[i].b]
			-(dep[xw[i].lca]<<1); // remains
		if(xw[i].z < lst) xw[i].ans += 2;
		else if(lst) ++ xw[i].ans;
		printf("%d\n",xw[i].ans);
	}
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值