题解:P1081 [NOIP2012 提高组] 开车旅行
原题链接
第一道不看题解写出来的紫题。写篇题解纪念一下。
25.1.23 update:优化重写了建链表部分的代码。
大致思路:
先考虑模拟。对于每一个城市,我们可以预先处理出每一个城市向东的最近与次近点,建成一个链表。 然后对于每次询问进行递归模拟,最后输出小 A 和小 B 行驶的路程。
代码实现一
先实现链表部分。
注:这部分中双指针的做法有点问题,已在后文优化。
暴力查找必定超时,我们可以先对
对于第一问,我们对每一个起点进行一次模拟并更新答案,最终输出最大值对应的城市。注意转 double 并且比值相同取
对于第二问,我们同样对每一个问题进行模拟,输出两个答案。
这样就可以在洛谷上获得 80pts。听说可以直接跑过当年的数据而且比标程还快。
贴出我的代码,附注释。
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define N 100100
const int inf=1e17;
const double eps=1e-12;
int n,m,h[N],x[N],s[N],id[N],pos[N],to[N][2],f[N][2],ans[2],maxn,maxi;
double d=inf;
//to数组记录i后的最小值与次小值 以及前往的城市编号
bool cmp(int x,int y){ return h[x]<h[y];}
void update(int i,int x){//更新最小值与次小值
if(x==0||x==n+1||x<=i) return ;
int w=abs(h[x]-h[i]);//0为最小值,即为B要去的城市
if(w<=f[i][0])
f[i][1]=f[i][0],f[i][0]=w,
to[i][1]=to[i][0],to[i][0]=x;
else if(w<=f[i][1]) f[i][1]=w,to[i][1]=x;
}
void init(){
for(int s=1;s<=n;s++){//初始化处理,建立
int l=pos[s],r=pos[s],R,L;
while(l&&id[l]<=s) l--;
while(r<=n&&id[r]<=s) r++;
R=r,L=l;
if(l){l--;while(l&&id[l]<=s) l--;}
if(r<=n){r++;while(r<=n&&id[r]<=s) r++;}
if(r!=R)update(s,id[r]);
update(s,id[R]),update(s,id[L]);
if(l!=L) update(s,id[l]);
}
}
void check(int s,int fl,int sum){//进行模拟
if(!to[s][fl]||sum+f[s][fl]>maxn) return ;
ans[fl]+=f[s][fl];
check(to[s][fl],1-fl,sum+f[s][fl]);
return ;
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n;h[0]=-inf;
for(int i=1;i<=n;i++)
cin>>h[i],id[i]=i,
f[i][0]=f[i][1]=inf,
to[i][0]=to[i][1]=0;
sort(id+1,id+1+n,cmp);
for(int i=1;i<=n;i++) pos[id[i]]=i;
init();
cin>>maxn>>m;
for(int i=1;i<=n;i++){
ans[0]=ans[1]=0;
check(i,1,0);
if(!ans[0]&&!ans[1]) continue;
double dd;
if(ans[0]==0) dd=inf;
else dd=(double)ans[1]/(double)ans[0];
if(!maxi||dd<d) d=dd,maxi=i;
else if(abs(dd-d)<eps&&h[i]>h[maxi]) maxi=i;
}
cout<<maxi<<'\n';
for(int i=1;i<=m;i++){
cin>>s[i]>>maxn;
ans[0]=ans[1]=0;
check(s[i],1,0);
cout<<ans[1]<<' '<<ans[0]<<'\n';
}
return 0;
}
代码实现二 (倍增优化)
接下来我们在原来的代码上用倍增加快模拟的过程。
在
这里贴出倍增代码与修改后的初始化代码。当然你也可以按照上面的思路重新写一份。
for(int i=1;i<=n;i++)
swap(to[i][1],to[i][0]),swap(f[i][1],f[i][0]);
//此时用to[i][0]表示次近,用to[i][1]表示最近
for(int i=1,j;i<=n;i++){
//在这之后,我们要用to[i][1]表示走两步
//即为先走一个次近,再走一个最近
j=to[i][0];//次近
f[i][1]=f[i][0]+f[j][1];
to[i][1]=to[j][1];
fA[i][0]=f[i][0],fA[i][1]=f[i][0];
}
for(int k=2;k<=30;k++){
for(int i=1;i<=n;i++){
int j=to[i][k-1];
if(!j) continue;
to[i][k]=to[j][k-1];
f[i][k]=f[i][k-1]+f[j][k-1];
fA[i][k]=fA[i][k-1]+fA[j][k-1];
}
}
void check(int s){
sum=0,suma=0;
for(int k=30;k>=0;k--)
if(to[s][k]&&sum+f[s][k]<=maxn)
sum+=f[s][k],suma+=fA[s][k],s=to[s][k];
return ;
}
DLC 链表部分的优化
其实写题解的时候总是有两个点差100ms,本来以为是常数问题,卡了半天不得不承认是自己链表部分复杂度假了。因此在期末考后重新添加了后半部分题解。
在之前的链表实现中,我们通过双指针来向前和向后找下一个前往的节点。这个过程中需要在数组的左边和右边分别找两个满足条件(即编号小于
以找右端点为例,在遍历排序后的
为了省脑子我这里等找完后再更新。
优化后的链表代码:
priority_queue<int> q,q1,p,p1;
//枚举到一个数时,将更新前面所有编号比i小的节点的右边端点
for(int i=1;i<=n;i++){
while(!p1.empty()&&-p1.top()<id[i]) R[-p1.top()]=id[i],p1.pop();
while(!p.empty()&&-p.top()<id[i]) r[-p.top()]=id[i],p1.push(p.top()),p.pop();
p.push(-id[i]);
}
//枚举到一个数时,将更新后面所有编号比i小的节点的左边端点
for(int i=n;i>=1;i--){
while(!q1.empty()&&-q1.top()<id[i]) L[-q1.top()]=id[i],q1.pop();
while(!q.empty()&&-q.top()<id[i]) l[-q.top()]=id[i],q1.push(q.top()),q.pop();
q.push(-id[i]);
}
for(int i=1;i<=n;i++){
update(i,R[i]),update(i,r[i]);
update(i,l[i]),update(i,L[i]);
}
AC code
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define N 100100
const int inf=1e18;
const double eps=1e-12;
int n,m,h[N],x[N],s[N],id[N],pos[N],to[N][40],f[N][40],fA[N][40],ans[2],maxn,maxi;
int sum,suma;
double d=inf;
bool cmp(int x,int y){ return h[x]<h[y];}
void update(int i,int x){//更新最小值与次小值
if(x==0) return ;
int w=abs(h[x]-h[i]);//0为最小值,即为B要去的城市,走1<<0步
if(w<=f[i][0])// ,走1<<1步
f[i][1]=f[i][0],f[i][0]=w,
to[i][1]=to[i][0],to[i][0]=x;
else if(w<=f[i][1]) f[i][1]=w,to[i][1]=x;
}
int R[N],L[N],l[N],r[N];
priority_queue<int> q,q1,p,p1;
void init(){
//枚举到一个数时,将更新前面所有编号比i小的节点的右边端点
for(int i=1;i<=n;i++){
while(!p1.empty()&&-p1.top()<id[i]) R[-p1.top()]=id[i],p1.pop();
while(!p.empty()&&-p.top()<id[i]) r[-p.top()]=id[i],p1.push(p.top()),p.pop();
p.push(-id[i]);
}
//枚举到一个数时,将更新后面所有编号比i小的节点的左边端点
for(int i=n;i>=1;i--){
while(!q1.empty()&&-q1.top()<id[i]) L[-q1.top()]=id[i],q1.pop();
while(!q.empty()&&-q.top()<id[i]) l[-q.top()]=id[i],q1.push(q.top()),q.pop();
q.push(-id[i]);
}
for(int i=1;i<=n;i++){
update(i,R[i]),update(i,r[i]);
update(i,l[i]),update(i,L[i]);
}
for(int i=1;i<=n;i++)
swap(to[i][1],to[i][0]),swap(f[i][1],f[i][0]);
for(int i=1,j;i<=n;i++){
j=to[i][0];//次近
f[i][1]=f[i][0]+f[j][1];
to[i][1]=to[j][1];
fA[i][0]=f[i][0],fA[i][1]=f[i][0];
}
for(int k=2;k<=30;k++){
for(int i=1;i<=n;i++){
int j=to[i][k-1];
if(!j) continue;
to[i][k]=to[j][k-1];
f[i][k]=f[i][k-1]+f[j][k-1];
fA[i][k]=fA[i][k-1]+fA[j][k-1];
}
}
}
void check(int s){
sum=0,suma=0;
for(int k=30;k>=0;k--)
if(to[s][k]&&sum+f[s][k]<=maxn)
sum+=f[s][k],suma+=fA[s][k],s=to[s][k];
return ;
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n;
h[0]=-inf;
for(int i=1;i<=n;i++)
cin>>h[i],id[i]=i,
memset(f[i],0x3f,sizeof f[i]),
memset(fA[i],0x3f,sizeof fA[i]);
sort(id+1,id+1+n,cmp);
init();
cin>>maxn>>m;
for(int i=1;i<=n;i++){
check(i);
ans[0]=sum-suma,ans[1]=suma;
if(!ans[0]&&!ans[1]) continue;
double dd;
if(ans[0]==0) dd=inf;
else dd=(double)ans[1]/(double)ans[0];
if(!maxi||dd<d) d=dd,maxi=i;
else if(abs(dd-d)<eps&&h[i]>h[maxi]) maxi=i;
}
cout<<maxi<<'\n';
for(int i=1;i<=m;i++){
cin>>s[i]>>maxn;
check(s[i]);
cout<<suma<<' '<<sum-suma<<'\n';
}
return 0;
}