题解 P1081 【开车旅行】
为了通过提高试炼场的倍增关,被迫做紫题/kel……(yrz菜菜……)
(特别鸣谢:本题解由《算法竞赛进阶指南》赞助播出,《算法竞赛进阶指南》,一本你值得拥有的算法好书!)
刚才一不小心把这个题的方法暴露了呢……没错,这个题的main idea就是:倍增!
倍增,字面意思就是“成倍增长”,也许你听说过在国际象棋棋盘上摆放麦粒的故事,体验了指数函数爆炸级别的恐怖,也在快速幂,ST表和最近公共祖先LCA中领略到这些高效的算法的神奇
没错,上面的东西都是倍增思想的具体体现
那这个题到底是怎么跟倍增的技巧扯上关系的呢???别急,咱们一步一步来,从分析题目开始!
Part I
记城市
i 的海拔高度为h_i ,城市i 和城市j 之间的距离d_{i,j} 恰好是这两个城市海拔高度之差的绝对值,即d_{i,j}=|h_i-h_j| 。小
\text{B} 总是沿着前进方向选择一个最近的城市作为目的地,而小\text{A} 总是沿着前进方向选择第二近的城市作为目的地 。
看来,小
也就是说,我们要预处理出在城市
那
,从哪一个城市出发,小
2、对任意给定的
x=x_i 和出发城市s_i ,小\text{A} 开车行驶的路程总数以及小\text {B} 行驶的路程总数。
我们发现,题目中要求的东西跟到达的城市,小
那怎么搞到这些信息呢?我们只需要知道出发的城市和行驶的天数,哦对了,还有轮到谁开车
那我们就一天一天的开,去模拟开车过程?
别忘了数据范围辣么大,他俩有时候开上十天半个月也开不完,怎么办呢?
反手一个倍增,闷声发大财!
为啥用倍增就可以呢?因为通过倍增枚举,是将每次把一个数变成它相应的两倍,也就是说:
这简直快的不是一点半点啊!
领教到倍增的威力之后,我们来看看接下来怎么做,根据刚才说的,我们是通过已知条件推出新的状态,也就是说,这是一个dp,或者说是递推的过程
动态规划需要考虑三件事:数组、方程、初始化
——
\text{v\color{red}ectorwyx}
首先是考虑状态定义的问题,根据刚才推出的关键信息,我们分别要定义三个数组——
并且,决定这三者的因素在前面也说过了,我们就用
同样,
接下来,咱们来考虑初始化的问题:
- 对于
f 数组:
(很好理解,
- 对于
da 数组:
(第一次会走到
(小
- 对于
db 数组:db_{0,j,0}=0 db_{0,j,1}=|h_j-h_{gb_j}| (和刚才
da 的思路完全一样)
我们再来考虑状态转移方程:
其实倍增的状态转移还是比较套路的,无非就是对“前一半”和“后一半”进行处理
当然这里还有一个坑点:特判
为啥呢?咱以
当
(
当
这样我们就把
我们发现,
int f[25][100005][2];
long long da[25][100005][2],db[25][100005][2];//十年OI一场空,不开long long见祖宗!
for(int i=1;i<=n;i++){
if(ga[i]){//预处理的时候一定要注意只有这个东西存在的时候才能预处理
f[0][i][0]=ga[i];
da[0][i][0]=abs(h[pos[i]].hi-h[pos[ga[i]]].hi);//注意我们在前面已经将h数组排好了序,所以别忘了取pos
db[0][i][0]=0;
}
if(gb[i]){//另一边的初始化对称过来即可
f[0][i][1]=gb[i];
da[0][i][1]=0;
db[0][i][1]=abs(h[pos[i]].hi-h[pos[gb[i]]].hi);
}
}
t=(int)(log(1.0*n)/log(2)+1);//倍增时只需要枚举到log2n就可以,所以要先把它算出来,用的是对数的换底公式:logab=logcb/logca
for(int i=1;i<=t;i++){//循环顺序很重要!一定要先枚举天数,再枚举城市,这样才能保证后面要用的东西前面已经推出来了
for(int j=1;j<=n;j++){
for(int k=0;k<=1;k++){
int l=(i==1)?k^1:k;//当k=1时,别忘了取相反
if(f[i-1][j][k])f[i][j][k]=f[i-1][f[i-1][j][k]][l];//递推f数组
if(f[i][j][k]){//别忘了必须保证存在
da[i][j][k]=da[i-1][j][k]+da[i-1][f[i-1][j][k]][l];//按照刚才推出的转移方程递推
db[i][j][k]=db[i-1][j][k]+db[i-1][f[i-1][j][k]][l];
}
}
}
}
Part III
该推的都推完啦,我们接下来要考虑的,就是如何用我们推出的东西,得到最终的答案
也就是说,我们的首要任务就是计算出
那咋办呢?我们可以借鉴倍增求LCA的思路:从大到小往上跳
也就是说:我们按照
没错,就这么简单,我们来把代码写出来!
long long da[25][100005][2],db[25][100005][2],la,lb;//再次提醒:十年OI一场空,不开long long见祖宗!
void calc(int s,long long x){
la=lb=0;//不要忘了初始化哦!
int k=0;//k依然表示谁先开车
for(int i=t;i>=0;i--){//从大往小枚举,看看能不能跳
if(f[i][s][k]&&da[i][s][k]+db[i][s][k]<=x){//如果当前城市存在,并且走完这些天还是超不过x
x-=da[i][s][k]+db[i][s][k];//剩余路程减去走了的路程
la+=da[i][s][k],lb+=db[i][s][k];//把新走的路程加到统计的答案里去
if(!i)k^=1;//别忘了i=0的时候依然要取相反
s=f[i][s][k];//跳的终点的城市里去
}
}
}
回到题目,第一问就是要求出对于给定的
long long x;
int s;
scanf("%lld",&x);//第一问:找到一个s使la:lb最小
int p=0;//p代表最后找到的城市s
long long ansa=1,ansb=0;//ansa和ansb是找到的比值最小的la和lb
for(int i=1;i<=n;i++){//枚举每个城市
calc(i,x);//计算出小A和小B的路程
if(!lb)la=1;//题目要求:如果lb为0,那就变成1
if(la*ansb<lb*ansa||(la*ansb==lb*ansa&&h[pos[i]].hi>h[pos[p]].hi))//由于比例的分数算除法会产生玄学的精度问题,所以我们把它换成乘积的形式,除此以外,比值相等时选择高度较高的一个
ansa=la,ansb=lb,p=i;//更新答案
}
printf("%d\n",p);//找到答案输出
int m;
scanf("%d",&m);
while(m--){
scanf("%d%lld",&s,&x);
calc(s,x);//直接计算calc(s,x)就行
printf("%lld %lld\n",la,lb);
}
return 0;
Part IV
上完整代码:
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
using namespace std;
struct qwq{//存储城市的结构体以及cmp
int hi,id,pre,nxt;
}h[100005];
bool cmp(qwq x,qwq y){
return x.hi<y.hi;
}
int choose(int a,int b,int i){//选择更优解
if(!a)return h[b].id;
if(!b)return h[a].id;
if(h[i].hi-h[a].hi<=h[b].hi-h[i].hi)
return h[a].id;
else return h[b].id;
}
void del(int pos){//删除链表元素
if(h[pos].nxt)h[h[pos].nxt].pre=h[pos].pre;
if(h[pos].pre)h[h[pos].pre].nxt=h[pos].nxt;
}
int n,t;
int pos[100005],ga[100005],gb[100005];//一堆变量
int f[25][100005][2];
long long da[25][100005][2],db[25][100005][2],la,lb;
void calc(int s,long long x){//calc函数
la=lb=0;
int k=0;
for(int i=t;i>=0;i--){
if(f[i][s][k]&&da[i][s][k]+db[i][s][k]<=x){
x-=da[i][s][k]+db[i][s][k];
la+=da[i][s][k],lb+=db[i][s][k];
if(!i)k^=1;
s=f[i][s][k];
}
}
}
int main(){
//Part I——预处理ga和gb
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",&h[i].hi);
h[i].id=i;
}
sort(h+1,h+1+n,cmp);
for(int i=1;i<=n;i++){
pos[h[i].id]=i;
h[i].pre=i-1;
h[i].nxt=i+1;
}
h[1].pre=h[n].nxt=0;
for(int i=1;i<n;i++){
int p=pos[i],p1=h[p].pre,p2=h[p].nxt;
if(p1&&(h[p].hi-h[p1].hi<=h[p2].hi-h[p].hi||!p2))
gb[i]=h[p1].id,ga[i]=choose(h[p1].pre,p2,p);
else gb[i]=h[p2].id,ga[i]=choose(p1,h[p2].nxt,p);
del(p);
}
//Part II——用倍增推出f,da,db
for(int i=1;i<=n;i++){
if(ga[i]){
f[0][i][0]=ga[i];
da[0][i][0]=abs(h[pos[i]].hi-h[pos[ga[i]]].hi);
db[0][i][0]=0;
}
if(gb[i]){
f[0][i][1]=gb[i];
da[0][i][1]=0;
db[0][i][1]=abs(h[pos[i]].hi-h[pos[gb[i]]].hi);
}
}
t=(int)(log(1.0*n)/log(2)+1);
for(int i=1;i<=t;i++){
for(int j=1;j<=n;j++){
for(int k=0;k<=1;k++){
int l=(i==1)?k^1:k;
if(f[i-1][j][k])f[i][j][k]=f[i-1][f[i-1][j][k]][l];
if(f[i][j][k]){
da[i][j][k]=da[i-1][j][k]+da[i-1][f[i-1][j][k]][l];
db[i][j][k]=db[i-1][j][k]+db[i-1][f[i-1][j][k]][l];
}
}
}
}
// Part III——计算calc得到答案
long long x;
int s;
scanf("%lld",&x);
int p=0;
long long ansa=1,ansb=0;
for(int i=1;i<=n;i++){
calc(i,x);
if(!lb)la=1;
if(la*ansb<lb*ansa||(la*ansb==lb*ansa&&h[pos[i]].hi>h[pos[p]].hi))
ansa=la,ansb=lb,p=i;
}
printf("%d\n",p);
int m;
scanf("%d",&m);
while(m--){
scanf("%d%lld",&s,&x);
calc(s,x);
printf("%lld %lld\n",la,lb);
}
return 0;
}