[CTSC2016] 时空旅行 题解

好久没有做过这么巧妙的题目了,开心捏O(∩_∩)O~


由于传送的 \(y,z\) 坐标自选,所以实际上对于一个点给出的坐标 \((a_i,b_i,c_i)\) 以及这个点调查的花费 \(cs_i\),我们只需要维护 \(a_i,cs_i\) 即可。那么设规定的 \(x\) 值为 \(x_0\),那么到该点调查的花费即为 \((x_0-a_i)^2+c_i=x_0^2-2x_0a_i+a_i^2+c_i\)

那么根据凸壳经典推式子方法,我们得到(下设 \(a_i>a_j,d_i=a_i^2+c_i\)):

\[x_0^2-2x_0a_i+a_i^2+c_i<x_0^2-2x_0a_j+a_j^2+c_j \]

\[-2x_0a_i+d_i<-2x_0a_j+d_j \]

\[\dfrac{d_i-d_j}{a_i-a_j}<2x \]

经典下凸壳公式。这样我们就可以通过维护下凸壳后二分来得到最优解。

那么问题来了:我们该如何维护删除操作呢?

显然,所有时空可以构成一棵树,每一条边 \(u\to v\) 表示 \(v\) 是由 \(u\) 转化而来的。我们容易证明,这棵树假如我们按照 \(dfs\) 序进行排序,那么连续的操作段一定只有 \(n\) 段。证明是显然的,当我们进入一棵子树并加入一个点的时候,我们添加了一个操作段;当我们进入一棵子树并删除一个点的时候,我们断开了一个操作段,操作段数量 \(+1\)

多条操作段,极其容易想到线段树分治。我们可以给线段树上每个区间都建立一个下凸壳,查询时对经过的所有区间都求一遍最小值即可。假如事先不进行排序,那么时间复杂度就是 \(O(n\log^2n)\) 的,考虑优化。

插入时,时间复杂度之所以是 \(\log^2n\),是因为我们还要对所有点进行一次排序。假如我们在插入前就进行排序,就可以省掉线段树内部的排序。

查询时,时间复杂度之所以是 \(\log^2n\),是因为我们要用二分找答案。那假如我们根据 \(x_0\) 的值事先对询问进行排序,我们就可以用均摊 \(O(\log n)\) 的算法解决。我们可以记录每一个区间上一次被访问时的答案位置。根据单调性,这一次的答案一定在上一次的答案之后。由于所有的区间的凸包总共只有 \(n\log n\) 级别个点,所以可以保证时间复杂度正确性。

这样,我们就将时间复杂度优化到了 \(O(n\log n)\)

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e5+5;
struct adm{int l,r,id;}ad[N];
struct que{int s,x,id;}qu[N];
int n,m,dfn[N];vector<int>g[N];
int id,a[N],d[N],chg[N],ans[N];
vector<int>lf[N],rt[N];int sum;
int cmp(adm x,adm y){
	return a[x.id]<a[y.id];
}int cmq(que x,que y){
	return x.x<y.x;
}void dfs(int x){
	dfn[x]=++id;
	if(chg[x]>0) lf[chg[x]].push_back(id);
	else rt[-chg[x]].push_back(id-1);
	for(auto y:g[x]) dfs(y);
	if(chg[x]>0) rt[chg[x]].push_back(id);
	else lf[-chg[x]].push_back(id+1); 
}namespace SGT{
	vector<int>st[N*4];int tp[N*4];
	int check(int x,int y,int z){
		return (d[z]-d[y])*(a[y]-a[x])<=(d[y]-d[x])*(a[z]-a[y]);
	}int gta(int id,int x){
		return d[id]-2*x*a[id]+x*x;
	}void add(int x,int l,int r,int L,int R,int id){
		if(L>R) return;
		if(L<=l&&r<=R){
			int t=st[x].size()-1;
			while(t>0&&check(st[x][t-1],st[x][t],id)) t--,st[x].pop_back();
			return st[x].push_back(id);
		}int mid=(l+r)/2;
		if(L<=mid) add(x*2,l,mid,L,R,id);
		if(R>mid) add(x*2+1,mid+1,r,L,R,id);
	}int ask(int x,int l,int r,int k,int xa){
		int &nw=tp[x],t=st[x].size()-1,mid=(l+r)/2,as=1e18;
		while(nw<t&&gta(st[x][nw],xa)>gta(st[x][nw+1],xa)) nw++;
		if(t>-1) as=gta(st[x][nw],xa);
		if(l==r) return as;
		if(k<=mid) return min(as,ask(x*2,l,mid,k,xa));
		return min(as,ask(x*2+1,mid+1,r,k,xa));
	}
}signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n>>m>>d[1],chg[1]=1;
	for(int i=2;i<=n;i++){
		int opt,s,id;cin>>opt>>s>>id,id++;
		g[++s].push_back(i),chg[i]=-id;
		if(opt) continue;
		cin>>a[id]>>s>>s>>d[id];
		d[id]+=a[id]*a[id],chg[i]=id;
	}dfs(1);
	for(int i=1;i<=n;i++)
		for(int j=0;j<lf[i].size();j++)
			ad[++sum]={lf[i][j],rt[i][j],i};
	sort(ad+1,ad+sum+1,cmp);
	for(int i=1;i<=m;i++)
		cin>>qu[i].s>>qu[i].x,qu[i].s++,qu[i].id=i;
	sort(qu+1,qu+m+1,cmq);
	for(int i=1;i<=sum;i++)
		SGT::add(1,1,n,ad[i].l,ad[i].r,ad[i].id);
	for(int i=1;i<=m;i++)
		ans[qu[i].id]=SGT::ask(1,1,n,dfn[qu[i].s],qu[i].x);
	for(int i=1;i<=m;i++) cout<<ans[i]<<"\n";
	return 0;
}
posted @ 2025-06-28 11:11  长安一片月_22  阅读(13)  评论(0)    收藏  举报