【问题描述】
就要放暑假了。忙碌了一个学期的Sharon 准备前往W 国进行为期长达一个
月的旅行。然而旅行往往是疲惫的,因此她需要来规划一下自己的旅行路线。
W 国由n 个城市组成,我们不妨将每个城市看成是一个二维平面上的点。
对于第i 个城市,我们设它所处的位置为( , ) i i x y 。W 国的人做事一向循规蹈矩,
因此在从城市i 向城市j 移动的过程中,只能沿水平或竖直方向移动。换句话说,
城市i 和城市 j 的最短距离为 i, j i j i j d x x y y 。
当Sharon 为自己设计一条旅行路线时,她会选好起点城市S 和终点城市T。
然后考虑从城市S 通过一些中转城市最终到达城市T。
我们不妨设城市S 到城市T 的中转城市依次为1 2 , ,..., k a a a 。则Sharon 首先从
城市S 出发到达城市1a ,对于任意的1 ik ,她将从城市ia 出发到达城市1 i a 。
最后从城市k a 出发到达城市T。显然对于中途的k 个中转城市,她将行走1 k 段
路。我们定义
1 S,a1 D d ,对于任意的2 i k,
1 , i i i a a D d
, k 1 ak ,T D d 。
既然已经放假了,Sharon 当然不希望这次旅行过于疲惫,因此我们定义一条
旅行路线的疲劳值为
1 1
max i
i k
R D
。现在Sharon 找到善于编程的你,她将告诉
你W 国城市的个数,每个城市的坐标,以及她选定的起点城市S 和终点城市T。
她想由你来帮她安排旅行的中转城市,使得R 的值最小。
特别注意的是,我们允许从城市S 不经过任何中转城市到达城市T,也就是
说可以从城市S 直接前往城市T,此时的疲劳值为T S T S R x x y y 。当ST
时,我们规定0 R 。
【输入格式】
第一行包含一个正整数n 表示W 国城市的个数。
接下来n 行每行包含两个正整数, i x 和i y ,表示第i 个城市的坐标。
接下来一行包含一个正整数m,表示Sharon 旅行的次数。
接下来m 行每行包含两个正整数,S 和T,表示起点城市和终点城市。
【输出格式】
输出共包含m 行,其中第i 行包含一个正整数Ri,表示第i 次旅行的最小疲
劳值。
NOI 2016 模拟训练 旅行计划
第6 页 共8 页
【样例输入】
3
1 1
2 1
4 1
2
1 2
1 3
【样例输出】
1
2
【样例说明】
第一次旅行时直接从城市1 前往城市2,疲劳值为1。
第二次旅行时从城市1 经过中转城市2 再到达城市3,疲劳值为2。
刚拿到这道题,以为很简单,但只是想到了暴力的算法,就是把每个点之间的距离算出来再进行加和比较。
但是到后面却发现这道题没有想象中的那么简单,你不只要算出每个的距离,你还得比较加上他之后的距离的最大值来判断取舍,显然这种枚举的方法复杂度是很大的。
后来我又想到的是DFS,思路是:从你出发的点开始寻找,只能走上下左右四个方向,且每次都是一步,这样就保证了与单位长度是一致的。然后只要找到一个点,就比较这个点到起点的距离与这个点到终点的距离的最大值与你记录的上一个点的数据的最小值来决定这个点到底走不走(如果要走的点是除了起点外的第一个点的话,就可以直接走上去),所以一直搜索下去,就可以保证到达终点的距离是最短的了。
这里有个题解,是每种方法的得分情况:
算法一 :
使用类似 Floyd的算法预来处理任意两点间答案,记 ans[i][j]表 示从点 i走到点 走到点 j最小的大距离值。每次用 最小的大距离值。每次用 最小的大距离值。每次用 max(ans[i][k],ans[k][j])来更新 来更新 ans[i][j]。 预处理复杂度 O(n3),单次询问复杂度 O(1)。期望得分 。期望得分 20分。
算法二 :
注意到测试点 :注意到测试点 3只有一次询问, 测试点 1和 2的点数及询问都很 小。 因此可以二分答案后从 S使用 BFS来扩展看能否走到 来扩展看能否走到 T,然后调整二分边 ,然后调整二分边 界。期望得分 30分。
算法三 :
易知我们的行走过程一定要在原图最小生成树上,而 :易知我们的行走过程一定要在原图最小生成树上,而 :易知我们的行走过程一定要在原图最小生成树上,而 n个点 的图共有约 n2/2条无向边,求出最小生成 树问题转换条无向边,求出最小生成 树问题转换m次询问树上两点间 最大边权。预处理复杂度 O(n2logn)或 O(n2),单次询问复杂度 ,单次询问复杂度 O(n)。期望得分 40分。
算法四 :
注意到测试点 5和 6所有的点都满足 xi = 1,因此所有的点将构成 ,因此所有的点将构成 一条链。每次的行走过程定是从个点到它上面或者下,将所 一条链。每次的行走过程定是从个点到它上面或者下,将所 一条链。每次的行走过程定是从个点到它上面或者下,将所 有的点按照纵坐标排序,这样每次询问就变成求给定区间 有的点按照纵坐标排序,这样每次询问就变成求给定区间 有的点按照纵坐标排序,这样每次询问就变成求给定区间 有的点按照纵坐标排序,这样每次询问就变成求给定区间 有的点按照纵坐标排序,这样每次询问就变成求给定区间 有的点按照纵坐标排序,这样每次询问就变成求给定区间 有的点按照纵坐标排序,这样每次询问就变成求给定区间 有的点按照纵坐标排序,这样每次询问就变成求给定区间 有的点按照纵坐标排序,这样每次询问就变成求给定区间 有的点按照纵坐标排序,这样每次询问就变成求给定区间 有的点按照纵坐标排序,这样每次询问就变成求给定区间 有的点按照纵坐标排序,这样每次询问就变成求给定区间 有的点按照纵坐标排序,这样每次询问就变成求给定区间 有的点按照纵坐标排序,这样每次询问就变成求给定区间 有的点按照纵坐标排序,这样每次询问就变成求给定区间 有的点按照纵坐标排序,这样每次询问就变成求给定区间 有的点按照纵坐标排序,这样每次询问就变成求给定区间 有的点按照纵坐标排序,这样每次询问就变成求给定区间 有的点按照纵坐标排序,这样每次询问就变成求给定区间 有的点按照纵坐标排序,这样每次询问就变成求给定区间 有的点按照纵坐标排序,这样每次询问就变成求给定区间 有的点按照纵坐标排序,这样每次询问就变成求给定区间 有的点按照纵坐标排序,这样每次询问就变成求给定区间 有的点按照纵坐标排序,这样每次询问就变成求给定区间 有的点按照纵坐标排序,这样每次询问就变成求给定区间 [L,R]的最小值,使用 的最小值,使用 的最小值,使用 的最小值,使用 的最小值,使用 的最小值,使用 的最小值,使用 ST表或线段树等数据结构进行处理。期望得分 20分。结合算法三 。期望得分 60分。
算法五 :
我们发现算法的瓶颈主要出在预处理最小生成树上。对于平面点 :我们发现算法的瓶颈主要出在预处理最小生成树上。对于平面点 :我们发现算法的瓶颈主要出在预处理最小生成树上。对于平面点 曼哈顿距离最小生成树有一些性质可以利用。对于个点 i,我们以它为原点建 立平面直角坐标系,并画出线 y = x和 y = x。这 样整个平面就被坐标轴和两条直线分成了 8个部分,可以证明的是点 个部分,可以证明的是点 i只需要向每个部分离它最近的点连 边然后求最小生成树即可。这样数就变了 8n条,可以快速求出最小生成树 条,可以快速求出最小生成树 了。接下来我们考虑如何构造这 8n条边,如果对每个点都去寻找这 条边,如果对每个点都去寻找这 8个部分离 它最近的点,建边复杂度将再次退化为 O(n2)。一个可行的做法是将所有 的点按 照横坐标为第一关键字纵二排序,然后使用线段树或 状数组照横坐标为第一关键字纵二排序,然后使用线段树或 状数组平衡树等数据结构维护点之间的关系即可做到 O(nlogn)的预处理复杂度。接下来 的预处理复杂度。接下来 的预处理复杂度。接下来 我们考虑每次询问,设树的高度为 我们考虑每次询问,设树的高度为 h,则单次询问的复杂度就为 ,则单次询问的复杂度就为 O(h)。对于前 。对于前 4个测试点 n都较小,对于第 都较小,对于第 5和第 6个测试点可以通过算法四解决,对于第 个测试点可以通过算法四解决,对于第 7和第 8个测试点由于数据随机生成,这样树的高度不会特别。因此结合算法四 个测试点由于数据随机生成,这样树的高度不会特别。因此结合算法四 个测试点由于数据随机生成,这样树的高度不会特别。因此结合算法四 以后期望得分 80分。
算法六 :
使用算法五预处理出最小生成树,我们考虑优化的询问部分。 :使用算法五预处理出最小生成树,我们考虑优化的询问部分。 :使用算法五预处理出最小生成树,我们考虑优化的询问部分。 每次询问树上两点间的最大边权,使用链剖分预处理复杂度 每次询问树上两点间的最大边权,使用链剖分预处理复杂度 每次询问树上两点间的最大边权,使用链剖分预处理复杂度 O(nlogn),单次 ,单次 询问复杂度 O(log2n)。期望得分 。期望得分 80~90分。使用动态树,预处理复杂度 分。使用动态树,预处理复杂度 分。使用动态树,预处理复杂度 分。使用动态树,预处理复杂度 O(nlogn), 单次询问复杂度 O(logn)。期望得分 80~90分。以上两种算法虽然可高效解决 分。以上两种算法虽然可高效解决 树上的很多询问题,但由于常数过大等原因无法通所有测试据。我们考虑 树上的很多询问题,但由于常数过大等原因无法通所有测试据。我们考虑 树上的很多询问题,但由于常数过大等原因无法通所有测试据。我们考虑 到树的形态固定,边权因此我们可以使用倍增方式预处理点 到树的形态固定,边权因此我们可以使用倍增方式预处理点 到树的形态固定,边权因此我们可以使用倍增方式预处理点 到树的形态固定,边权因此我们可以使用倍增方式预处理点 到树的形态固定,边权因此我们可以使用倍增方式预处理点 到树的形态固定,边权因此我们可以使用倍增方式预处理点 到树的形态固定,边权因此我们可以使用倍增方式预处理点 到树的形态固定,边权因此我们可以使用倍增方式预处理点 到树的形态固定,边权因此我们可以使用倍增方式预处理点 到树的形态固定,边权因此我们可以使用倍增方式预处理点 到树的形态固定,边权因此我们可以使用倍增方式预处理点 到树的形态固定,边权因此我们可以使用倍增方式预处理点 i往上走 往上走 2j条边 的最大边权,这样预处理复杂度 O(nlogn),单次询问复杂度 O(logn),常数 很小。期望得分 100分
代码:
#include <cmath> #include <iostream> #include <cstdio> #include <cstring> #include <string> #include <cstdlib> #include <vector> #include <algorithm> #define mp make_pair #define fi first #define se second using namespace std; typedef pair<int,int> PII; const int MAXN=100005; const int INF=1000000000; struct Point{ int x,y,num; friend bool operator < (const Point &a,const Point &b) { if (a.x!=b.x) return a.x<b.x; return a.y<b.y; } }pnt[MAXN]; struct E{ int a,b,w; E(void){} E(int _a,int _b,int _w):a(_a),b(_b),w(_w){} friend bool operator < (const E &a,const E &b) { return a.w<b.w; } }e[MAXN*4]; int en; struct G{ int to,next,d; }g[MAXN*2]; int gn,start[MAXN]; inline void Add(int a,int b,int d) { gn++,g[gn].to=b,g[gn].d=d,g[gn].next=start[a],start[a]=gn; } int n,m; int fa[MAXN]; int ffa[20][MAXN]; int dis[20][MAXN]; int a[MAXN],b[MAXN]; PII c[MAXN]; int Log[MAXN]; int dep[MAXN]; inline void Update(int x,PII v) { for (int i=x;i<=n;i+=i&(-i)) c[i]=min(c[i],v); } inline PII Query(int x) { PII res=mp(INF,-1); for (int i=x;i;i-=i&(-i)) res=min(res,c[i]); return res; } int getfa(int x) { if (fa[x]!=x) fa[x]=getfa(fa[x]); return fa[x]; } void merge(int x,int y) { fa[getfa(x)]=getfa(y); } inline int LCA(int p,int q) { if (dep[p]<dep[q]) swap(p,q); int x=dep[p]-dep[q]; for (int i=0;i<=Log[x];i++) if (x&(1<<i)) p=ffa[i][p]; for (int i=Log[n];i>=0;i--) if (ffa[i][p]!=ffa[i][q]) p=ffa[i][p],q=ffa[i][q]; if (p==q) return p; return ffa[0][p]; } inline int Query(int p,int q) { int x=dep[p]-dep[q]; int res=0; for (int i=0;i<=Log[x];i++) if (x&(1<<i)) res=max(res,dis[i][p]),p=ffa[i][p]; return res; } void Init() { scanf("%d",&n); for (int i=1;i<=n;i++) { scanf("%d%d",&pnt[i].x,&pnt[i].y); pnt[i].num=i; } } void BuildMST() { for (int dir=1;dir<=4;dir++) { if (dir==2||dir==4) for (int i=1;i<=n;i++) swap(pnt[i].x,pnt[i].y); if (dir==3) for (int i=1;i<=n;i++) pnt[i].x=-pnt[i].x; sort(pnt+1,pnt+n+1); for (int i=1;i<=n;i++) a[i]=b[i]=pnt[i].x-pnt[i].y; sort(b+1,b+n+1); int nq=unique(b+1,b+n+1)-(b+1); for (int i=1;i<=n;i++) { a[i]=lower_bound(b+1,b+nq+1,a[i])-b; c[i]=mp(INF,-1); } for (int i=n;i;i--) { int now=Query(a[i]).se; if (now!=-1) e[++en]=E(pnt[i].num,pnt[now].num,pnt[now].x+pnt[now].y-pnt[i].x-pnt[i].y); Update(a[i],mp(pnt[i].x+pnt[i].y,i)); } } for (int i=1;i<=n;i++) fa[i]=i; sort(e+1,e+en+1); for (int i=1;i<=en;i++) { if (getfa(e[i].a)==getfa(e[i].b)) continue; merge(e[i].a,e[i].b); Add(e[i].a,e[i].b,e[i].w); Add(e[i].b,e[i].a,e[i].w); } } void BuildTree() { static int qu[MAXN]; static bool vis[MAXN]; int head=0,tail=1; qu[0]=1; vis[1]=true; while (head!=tail) { int p=qu[head++]; for (int i=start[p];i;i=g[i].next) { int v=g[i].to,d=g[i].d; if (vis[v]) continue; vis[v]=true; dep[v]=dep[p]+1; dis[0][v]=d; ffa[0][v]=p; qu[tail++]=v; } } Log[0]=-1; for (int i=1;i<=n;i++) Log[i]=Log[i>>1]+1; for (int i=1;i<=Log[n];i++) for (int j=1;j<=n;j++) { ffa[i][j]=ffa[i-1][ffa[i-1][j]]; dis[i][j]=max(dis[i-1][j],dis[i-1][ffa[i-1][j]]); } } void Solve() { scanf("%d",&m); while (m--) { int p,q,lca; scanf("%d%d",&p,&q); lca=LCA(p,q); printf("%d\n",max(Query(p,lca),Query(q,lca))); } } int main() { freopen ("plan.in","r",stdin); freopen ("plan.out","w",stdout); Init(); BuildMST(); BuildTree(); Solve(); return 0; }