这道题有点东西,正解调了近\(2\)个小时才解决,NOIP\(CSP\)考这种题肯定\(GG\)了
\(Description\)
题面
题目大意是有\(n\)个城市,位置是\(1-n\),每个城市有一定的海拔,定义每两个城市之间的距离是海拔差的绝对值,\(A、B\)轮流开车,\(A\)先开(话说这个好别扭,为啥非要让次近的先开),只能向右开,\(A\)的策略是去次近的城市,\(B\)的策略是去最近城市。最近城市判断的第一关键字是海拔差绝对值最小,若相同则海拔越低越近。
有两个问题:
第一个问题是给你开车的距离\(x_0\),要求你找到一个起始点\(s_0\)使得到不能开位置\(\frac{dis2}{dis1}\)最大(若\(dis1=0\)则视为无穷大),输出\(s_0\),不能开的位置指找不到次小值或者最小值或者距离不足。
第二个问题是给你\(m\)个起始位置和距离\(s_i、x_i\),要求输出在给定条件下\(A、B\)分别行使的路程。
\(Solution\)
这道题在\(qbxt\)讲过,只知道是倍增也不会怎么做,于是先写暴力。
对于\(sub1\)的情况,需要枚举所有位置,然后再一步一步往后走,每走一步需要往后扫描找到最小值次小值,明显\(O(n^3)\),发现有\(40pts\)的暴力分。
考虑到每次向后找最小值次小值这个过程太累赘了,而且同一个位置会被寻找多次,于是放到外面\(O(n^2)\)预处理每个位置后面最小值次小值的位置,\(sub1\)直接枚举所有位置一个个跳就行,\(sub2\)也被同时优化了。
整个程序的复杂度降到了\(O(mn)\)或者\(O(n^2)\)左右,发现竟然可以通过\(70pts!\)
赶紧交上去发现\(65pts\),然后发现忘记开\(long\ long\)还有\(double\)设的最大值不够,考试可一定要注意啊!
\(70pts\ Code\)
#include<cstdio> #include<iostream> #include<cstring> #include<algorithm> #include<cmath> #define re register #define maxn 200100 #define MIN 2147483647 #define ll long long using namespace std; inline int read() { int x=0,f=1; char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();} while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();} return x*f; } int s[maxn],x[maxn],x0,s0,n,m,tmp1,tmp2,tmp3,tmp4,pos1,pos2,Ans; int nxt1[maxn],nxt2[maxn]; ll ans1[maxn],ans2[maxn],h[maxn]; double ans,dis1,dis2; bool CMP(int a,int b,int c,int d) { if(a<c) return true; else if(a==c&&b<d) return true; return false; } void pre() { for(re int i=1;i<=n;++i) { tmp1=tmp2=MIN; pos1=pos2=0; tmp3=tmp4=MIN; for(re int j=i+1;j<=n;++j) { if(CMP(abs(h[i]-h[j]),h[j],tmp1,tmp3)) { tmp2=tmp1,tmp4=tmp3,pos2=pos1; tmp1=abs(h[i]-h[j]),tmp3=h[j],pos1=j; } else if(CMP(abs(h[i]-h[j]),h[j],tmp2,tmp4)) { tmp2=abs(h[i]-h[j]),tmp4=h[j],pos2=j; } } nxt1[i]=pos1,nxt2[i]=pos2; } } void work1() { ans=MIN; for(re int i=1;i<=n;++i)//枚举起点 { dis1=dis2=0; int now=i; for(re int j=1;j<=n;++j) { if(j&1)//是奇数 找次小 { if(dis1+dis2+abs(h[nxt2[now]]-h[now])>x0) break; if(!nxt2[now]) break; dis2+=abs(h[nxt2[now]]-h[now]); now=nxt2[now]; } else//找最小 { if(dis1+dis2+abs(h[nxt1[now]]-h[now])>x0) break; if(!nxt1[now]) break; dis1+=abs(h[nxt1[now]]-h[now]); now=nxt1[now]; } } double t; if(dis1==0) t=MIN; else t=dis2/dis1*1.0; if(t<ans) { ans=t,s0=i; } } } void work2() { ll d1,d2; for(re int i=1;i<=m;++i) { int now=s[i]; d1=d2=0; for(re int j=1;j<=n;++j) { if(j&1)//是奇数 找次小 { if(d1+d2+abs(h[nxt2[now]]-h[now])>x[i]) break; if(!nxt2[now]) break; d2+=(ll)abs(h[nxt2[now]]-h[now]); now=nxt2[now]; } else//找最小 { if(d1+d2+abs(h[nxt1[now]]-h[now])>x[i]) break; if(!nxt1[now]) break; d1+=(ll)abs(h[nxt1[now]]-h[now]); now=nxt1[now]; } } ans1[i]=d1,ans2[i]=d2; } } int main() { n=read(); for(re int i=1;i<=n;++i) h[i]=read(); x0=read(); m=read(); for(re int i=1;i<=m;++i) s[i]=read(),x[i]=read(); pre(); work1(); printf("%d\n",s0); work2(); for(re int i=1;i<=m;++i) printf("%lld %lld\n",ans2[i],ans1[i]); return 0; }
要是考试有着暴力分不肯定不去想正解
\(100pts\)
注意到两个瓶颈在于初始化和跳的操作,看这个数据我们需要优化成\(O(nlogn)\)
所以很容易想到跳的操作可以使用倍增优化
那么什么时候可以使用倍增优化呢
当不断进行类似的过程且不存在最优解时可以使用
比如求和、求最值、求最终位置、求距离等等
在这个题中,当谁在开车一定时,每一个位置的后继是一定的,走的距离也是一定的,所以这个过程可以用倍增优化掉
设走的总步数是\(2^j\)不便于转移,因为会出现总步数是\(1\),还要判断是谁走的情况,所以设每个人分别走了\(2^j\)步
代码还是比较简单的,注意分清每个数组的意义,别搞混了
void pre2()//倍增预处理 { for(re int i=1;i<=n;++i) { //d2[i]表示i后的次小距离,d1[i]表示最小距离 //d4[i][j]表示从i开始,A,B分别开(1<<j)步A一个人的路程 //d5[i][j]表示从i开始,A,B分别开(1<<j)步B一个人的路程 //d3[i][j]表示从i开始,A,B分别开(1<<j)步A,B总路程 d4[i][0]=d2[i]; d5[i][0]=d1[nxt2[i]]; d3[i][0]=d4[i][0]+d5[i][0]; nxt[i][0]=nxt1[nxt2[i]]; //nxt1[i]表示B开车的下一个城市,nxt2[i]表示A开车的下一个城市 //nxt[i][j]表示i开始,A,B分别开(1<<j)步,最终位置 } for(re int j=1;j<=20;++j)//注意从小到大放到外层枚举 for(re int i=1;i<=n;++i) { nxt[i][j]=nxt[nxt[i][j-1]][j-1]; if(!nxt[i][j]) continue;//如果出去了就不用算距离了 d3[i][j]=d3[i][j-1]+d3[nxt[i][j-1]][j-1]; d4[i][j]=d4[i][j-1]+d4[nxt[i][j-1]][j-1]; d5[i][j]=d5[i][j-1]+d5[nxt[i][j-1]][j-1]; } }
使用倍增跳的过程是这样的:先让两个一起跳,即走\(d3[i][j]\),判断是否超距离,是否越界,最后如果还能走就只能让\(A\)开一次了,所以判断一下即可。
这样跳的过程复杂度就变成了\(O(nlogn)\),但预处理的过程仍然是\(O(n^2)\)的,所以必须优化这个过程
讲道理这道题倍增比预处理过程的优化要好像,要用到双向链表
一般要用到双向链表的地方是将一个数组位置快速删除且能正常遍历,且删除的目的是让前面扫描过元素的不再被考虑(很重要的性质)
比如这道题,我们预处理时可以先开结构体按海拔排序,记录他们原来的位置,再开个桶记录原来位置到现在结构体下标的映射。在原来数组从左向右扫描,设它在结构体中的位置是\(pos[i]\),它的次近和最近海拔一定是在\(pos[i]-2,pos[i]-1,pos[i]+1pos[i]+2\)(因为极端情况是两个都在一边)。然后在这四个位置上找最小值次小值就行(注意判断越界的情况),然后把\(pos[i]\)删去,这样才能保证下一次寻找时不会考虑到原来数组中在它左边元素,这些操作用双向链表可以很好完成。
所以这道题就\(AC\)了,总的来说,暴力分很足,难点在于双向链表和倍增
\(100pts\ Code\)
#include<cstdio> #include<iostream> #include<cstring> #include<algorithm> #include<cmath> #define re register #define maxn 100100 #define MIN 2147483647 #define ll long long using namespace std; inline int read() { int x=0,f=1; char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();} while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();} return x*f; } struct Cup{ ll h; int l,r,pos; }City[maxn]; bool cmp(Cup A,Cup B) { return A.h<B.h; } int s[maxn],x[maxn],x0,s0,n,m,tmp1,tmp2,tmp3,tmp4,pos1,pos2; int nxt1[maxn],nxt2[maxn],post[maxn],c[120],nxt[maxn][23]; ll ans1[maxn],ans2[maxn],d1[maxn],d2[maxn],d3[maxn][23],d4[maxn][23],d5[maxn][23]; double ans=MIN,dis1,dis2; bool CMP(ll a,ll b,ll c,ll d)//按关键字比较 { if(a<c) return true; else if(a==c&&b<d) return true; return false; } void del(int id)//双向链表 { City[City[id].l].r=City[id].r; City[City[id].r].l=City[id].l; } void pre() { for(re int i=1;i<=n;++i) { tmp1=tmp2=tmp3=tmp4=MIN; pos1=pos2=0; int tmp=post[i];//表示他在结构体中的位置 c[1]=City[tmp].l,c[2]=City[City[tmp].l].l; c[3]=City[tmp].r,c[4]=City[City[tmp].r].r; for(re int j=1;j<=4;++j) { int now=c[j]; if(now==0||now==n+1) continue;//这里注意要判断全了 if(CMP(abs(City[tmp].h-City[now].h),City[now].h,tmp1,tmp3))//找最小值次小值,tmp1是最小距离,tmp3是最小距离的海拔,tmp2,tmp4是次小 { tmp2=tmp1,tmp4=tmp3,pos2=pos1; tmp1=abs(City[tmp].h-City[now].h); tmp3=City[now].h; pos1=now; } else if(CMP(abs(City[tmp].h-City[now].h),City[now].h,tmp2,tmp4)) { tmp2=abs(City[tmp].h-City[now].h); tmp4=City[now].h; pos2=now; } } nxt1[i]=City[pos1].pos,nxt2[i]=City[pos2].pos;//注意这里的i,City[pos1].pos都是原数组位置 d1[i]=tmp1,d2[i]=tmp2; del(tmp);//删除 } } void pre2()//倍增预处理 { for(re int i=1;i<=n;++i) { //d2[i]表示i后的次小距离,d1[i]表示最小距离 //d4[i][j]表示从i开始,A,B分别开(1<<j)步A一个人的路程 //d5[i][j]表示从i开始,A,B分别开(1<<j)步B一个人的路程 //d3[i][j]表示从i开始,A,B分别开(1<<j)步A,B总路程 d4[i][0]=d2[i]; d5[i][0]=d1[nxt2[i]]; d3[i][0]=d4[i][0]+d5[i][0]; nxt[i][0]=nxt1[nxt2[i]]; //nxt1[i]表示B开车的下一个城市,nxt2[i]表示A开车的下一个城市 //nxt[i][j]表示i开始,A,B分别开(1<<j)步,最终位置 } for(re int j=1;j<=20;++j)//注意从小到大放到外层枚举 for(re int i=1;i<=n;++i) { nxt[i][j]=nxt[nxt[i][j-1]][j-1]; if(!nxt[i][j]) continue;//如果出去了就不用算距离了 d3[i][j]=d3[i][j-1]+d3[nxt[i][j-1]][j-1]; d4[i][j]=d4[i][j-1]+d4[nxt[i][j-1]][j-1]; d5[i][j]=d5[i][j-1]+d5[nxt[i][j-1]][j-1]; } } void work1()//找点 { int tmp=x0; for(re int i=1;i<=n;++i) { dis1=dis2=0; int now=i; x0=tmp; for(re int j=20;j>=0;--j)//按上面的方法走就行 { if(nxt[now][j]==0||x0-d3[now][j]<0) continue; x0-=d3[now][j],dis1+=d5[now][j],dis2+=d4[now][j]; now=nxt[now][j]; } if(nxt2[now]!=0&&x0-d2[now]>=0) dis2+=d2[now];//小A还能再走 double t; if(dis1<=1e-7) t=MIN;//不要写if(dis==0),因为有精度问题,还有t要赋值足够大 else t=1.0*dis2/dis1; if(t<ans) ans=t,s0=i; } } void work2() { for(re int i=1;i<=m;++i) { int now=s[i]; for(re int j=20;j>=0;--j) { if(nxt[now][j]==0||x[i]-d3[now][j]<0) continue; ans1[i]+=d5[now][j],ans2[i]+=d4[now][j],x[i]-=d3[now][j]; now=nxt[now][j]; } if(nxt2[now]!=0&&x[i]-d2[now]>=0) ans2[i]+=d2[now]; } } int main() { n=read(); for(re int i=1;i<=n;++i) { City[i].h=read(); City[i].pos=i;//它在结构体中的位置->原位置 } sort(City+1,City+1+n,cmp); for(re int i=1;i<=n;++i) { post[City[i].pos]=i;//原位置->它在结构体中的位置 City[i].l=i-1; City[i].r=i+1; } pre(); pre2(); x0=read(); m=read(); for(re int i=1;i<=m;++i) s[i]=read(),x[i]=read(); work1(); printf("%d\n",s0); work2(); for(re int i=1;i<=m;++i) printf("%lld %lld\n",ans2[i],ans1[i]); return 0; }