《算法竞赛进阶指南》0x57倍增优化DP AcWing293 开车旅行
题目链接:https://www.acwing.com/problem/content/295/
题目给定n个城市,在一个方向上有序排列,每个城市有高度,有两个人a,b,定义两个城市之间的距离是高度之差的绝对值。b只会选择右边距离他最小的一个作为下一个点,a只会选择右边次小的点作为下一个点。a先走。问题一:给出一个最大距离X0,问从哪一个点开始走,la/lb最大?问题二:给出一系列起点S和距离X,求a,b走的距离。
本问题中,只要起点S和距离X确定了那么路线就一定唯一。首先,下一个点的产生可以通过“邻值查找”相同的算法利用链表或者是set进行处理,时间复杂度是O(nlogn),这里要同时查找最小值和次小值,故可以扫描最近的可能的四个点,找到最小的和次小的。获得ga和gb数组(下一个点的位置)之后就可以初始化f数组,表示的是k=0/1从j位置开始,走了1<<i步之后的位置,通过dp进行转移即可,注意dp的边界,i为0的时候,走一步的情况只需要用gagb数组更新即可,i为1的时候需要两个人来移动。下面就是更新da数组db数组表示a,b走过的距离。分别如f进行递推计算即可。最终得到f数组。
如果计算从S开始,最远移动距离是X,可以将步数按照二进制位进行划分,最多不超过logn次计算,通过da数组和db数组即可计算出a、b移动的距离,这里由于是a先走,而且步数一直是偶数,所以一直都是从a开始走,最后一个可能步数是奇数,但是也是从a开始走的。
问题一直接枚举每个起点即可,时间复杂度是O(nlogn),问题二直接计算calc函数,时间复杂度是O(mlogn)
注意:如果距离相同的话取海拔低的,第一种问题中最终如果两个比值相同的话就取海拔最高的。
代码:
#include<iostream>
#include<cstdio>
#include<set>
#include<vector>
#include<algorithm>
using namespace std;
const int maxn = 100010,M=17;
typedef long long ll;
typedef pair<ll,int> PLI;
const ll inf=1e13;
int n;
int h[maxn];
int f[M][maxn][2];
int ga[maxn],gb[maxn];
ll da[M][maxn][2],db[M][maxn][2];
void init_g(){//由于高度是不一样的所以直接用set求邻值即可
set<PLI> S;
S.insert({inf,0});//所有不能到的位置都是0
S.insert({inf+1,0});
S.insert({-inf,0});
S.insert({-inf-1,0});//防止越界,插入四个无穷大
for(int i=n;i;i--){//从i之后的中寻找邻值
PLI t={h[i],i};
set<PLI>::iterator j=S.lower_bound(t);
j++;//找到其上第二个,往下搜索四个值的最小值以及次小值
vector<PLI> cand;
for(int k=0;k<4;k++){
cand.push_back(*j);
j--;
}
ll d1=inf,d2=inf;//最小值以及次小值
int p1=0,p2=0;
for(int k=3;k>=0;k--){//如果距离相等的话就选择海拔较低的点,所以从下往上看
ll d=abs(h[i]-cand[k].first);
if(d<d1){//次小与最小的更新 ,注意赋值的先后顺序
d2=d1,p2=p1;
d1=d,p1=cand[k].second;
}else if(d<d2){
d2=d,p2=cand[k].second;
}
}
ga[i]=p2,gb[i]=p1;//最小次小都以及找到
S.insert(t);
}
}
void init_f(){
for(int i=0;i<M;i++){//阶段
for(int j=1;j<=n;j++){//出发点
if(!i)f[0][j][0]=ga[j],f[0][j][1]=gb[j];
else{
for(int k=0;k<2;k++){
if(i==1)f[i][j][k]=f[0][f[0][j][k]][1-k];
else f[i][j][k]=f[i-1][f[i-1][j][k]][k];
}
}
}
}
}
int get_dist(int i,int j){
return abs(h[i]-h[j]);
}
void init_d(){
for(int i=0;i<M;i++){
for(int j=1;j<=n;j++){
if(!i){
da[0][j][0]=get_dist(j,ga[j]);
da[0][j][1]=0;
db[0][j][1]=get_dist(j,gb[j]);
db[0][j][0]=0;
}else{
for(int k=0;k<2;k++){
if(i==1){
da[1][j][k]=da[0][j][k]+da[0][f[0][j][k]][1-k];
db[1][j][k]=db[0][j][k]+db[0][f[0][j][k]][1-k];
}else{
da[i][j][k]=da[i-1][j][k]+da[i-1][f[i-1][j][k]][k];
db[i][j][k]=db[i-1][j][k]+db[i-1][f[i-1][j][k]][k];
}
}
}
}
}
}
//从p开始,最远距离是x,a走过的距离,b走过的距离
void calc(int p,int x,int& la,int& lb){//保存a,b走的路的长度信息
la=lb=0;
// 倍增,将步数按照二进制从高到低进行枚举
// 走过的都是偶数步,一定是从a开始走的,只有最后一步是1,也是a走的
for(int i=M-1;i>=0;i--){//倒序判断长度是否可走,将步数表示成二进制
if(f[i][p][0] && la+da[i][p][0]+lb+db[i][p][0] <= x){
la+=da[i][p][0];
lb+=db[i][p][0];
p=f[i][p][0];//下一个位置
}
}
}
int main(){
cin>>n;
for(int i=1;i<=n;i++)scanf("%d",&h[i]);
init_g();
init_f();
init_d();
int p,x;
scanf("%d",&x);
int res=0,max_h=0;
double min_ratio=inf;
for(int i=1;i<=n;i++){
int la,lb;
calc(i,x,la,lb);
double ratio=lb ? (double)la/lb : inf;
if(ratio<min_ratio || ratio==min_ratio && h[i]>max_h){//ratio相等的时候输出海拔最大的城市
min_ratio=ratio;
max_h=h[i];
res=i;
}
}
cout<<res<<endl;
int m;
cin>>m;
while(m--){
scanf("%d%d",&p,&x);
int la,lb;
calc(p,x,la,lb);
printf("%d %d
",la,lb);
}
return 0;
}

![《算法竞赛进阶指南》0x57倍增优化DP AcWing293 开车旅行
[编程语言教程]](https://www.zixueka.com/wp-content/uploads/2024/01/1706714768-7277a39d0a7303a.jpg)
