按海拔排序后用树状数组一次预处理每城最近/次近东部城市,把确定性旅程建成按轮倍增表,预算贪心跳 2^k 轮单次询问 O(log N)。
OJ: roj
题目 ID: 3600
难度:提高+/省选-
标签:倍增树状数组排序python
创建: 2026-10-02 09:53
更新: 2026-10-10 11:30
形式化题目
需要回答两个问题:
- 对给定的
,求每个起点下小A 总里程 与小B 总里程 的比值 ( 时视为 ,两个 视为相等),输出比值最小的起点,并列时取海拔更高的城市; - 对
组询问 ,输出该起点、该预算下两人的总里程。
样例 1 中 1 1):
| 天 | 驾驶员 | 当前城市 | 去往 | 当日距离 | 选择依据 |
|---|---|---|---|---|---|
| 1 | 小A | 1 | 2 | 1 | 东边城市 2/3/4 距离为 1/1/2;城市 3 同距但海拔更低,被视为最近,于是城市 2 是次近,小A 选次近 |
| 2 | 小B | 2 | 4 | 1 | 东边城市 3/4 距离为 2/1,城市 4 最近 |
| 3 | 小A | 4 | — | — | 前方已无城市,旅程结束 |
表里"同距比海拔"和"找不到目标就结束"两条规则都真实触发过,后面的预处理与模拟必须原样复现它们。
正解
思路
一句话本质:把"每天开到哪"一次性预处理成查表(按海拔排序 + 树状数组,
朴素做法卡在哪。 逐询问模拟:每天都要在当前城市的东边所有城市里挑最近/次近,单步就要扫描
观察 1:距离就是海拔差,最近/次近只看至多 4 个候选。 把城市按海拔从小到大排序。对城市
… < x-2 < x-1 < H_i < x+1 < x+2 < …
低侧 | 高侧任何比
观察 2:候选还必须"在东边",用树状数组把动态条件变成静态查询。 候选要求编号大于 near[i]、second[i] 查表
观察 3:旅程是确定性链,按"轮"倍增。 起点一定,每天去哪唯一确定:一轮 = 小A 开到次近、小B 接着开到最近;链上最多 INF 标记,永不被选中。
观察 4:预算贪心跳整轮,最后补小A 一段。 所有轮里程为正、前缀和单调。从 da[k][city] + db[k][city] <= 剩余预算 就跳:这恰好装下预算内最多的整轮——把最优轮数写成二进制,从高到低逐位对齐,某位为 1 当且仅当对应那 da[0][city] <= 剩余 即可。样例 2 从城市 1 出发
flowchart LR
a["城市 1,预算 7"] -->|"k=1 跳 2 轮,耗 5"| b["城市 9,剩余 2"]
b -->|"k=0 与尾段:9 没有次近城市"| c["结束,A 3,B 2"]
贪心先在 INF 挡住,旅程结束,结果 3 2 与题面一致。
第一问:枚举起点 + 精确比大小。 每个起点跑一次 Fraction(a, b)(math.inf(min 就实现了题面的全部比较规则。样例 2 在
| 起点 | 小A 里程 | 小B 里程 | 比值 | 说明 |
|---|---|---|---|---|
| 1 | 3 | 2 | ||
| 2 | 2 | 4 | 并列最小 | |
| 3 | 2 | 1 | ||
| 4 | 2 | 4 | 并列最小,海拔 |
|
| 5 | 5 | 1 | ||
| 6 | 5 | 1 | ||
| 7 | 2 | 1 | ||
| 8 | 2 | 0 | 小B 一段都没开 | |
| 9 | 0 | 0 | 未启程 | |
| 10 | 0 | 0 | 未启程 |
比值最小的
代码
nearest_two 实现观察 1、2 的预处理,build_lift 实现观察 3 的倍增表,drive 是观察 4 的单次询问回答,solve 末尾的三元组 min 完成第一问的比较规则。
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-10 11:30
* update_at: 2026-10-10 11:30
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll INF = (ll)1000000000000000000LL;
int n, rounds_count, current_city; vector<ll> h; vector<int> nearv, secondv; vector<vector<int> > gup; vector<vector<ll> > da, db;
int prefix_sum(vector<int>&bit,int p){int total=0; while(p){total+=bit[p]; p-=p&-p;} return total;}
void mark_bit(vector<int>&bit,int p){int nn=bit.size()-1; while(p<=nn){bit[p]++; p+=p&-p;}}
int kth_bit(vector<int>&bit,int k){int nn=bit.size()-1,pos=0,step=1; while((step<<1)<=nn) step<<=1; while(step){int nxt=pos+step; if(nxt<=nn && bit[nxt]<k){k-=bit[nxt];pos=nxt;} step>>=1;} return pos+1;}
bool cmp_height_idx(int a,int b){ return h[a] < h[b]; }
bool cmp_candidate(int a,int b){ ll da=llabs(h[a]-h[current_city]), db=llabs(h[b]-h[current_city]); if(da!=db) return da<db; return h[a]<h[b]; }
void nearest_two(){ vector<int> order(n); for(int i=0;i<n;i++) order[i]=i+1; sort(order.begin(),order.end(),cmp_height_idx); vector<int> rank(n+1); for(int i=0;i<n;i++) rank[order[i]]=i+1; vector<int> bit(n+1); nearv.assign(n+1,0); secondv.assign(n+1,0); for(int city=n;city>=1;city--){ int r=rank[city]; int less=prefix_sum(bit,r-1); int east=n-city; vector<int> cands; int ks[4]={less,less-1,less+1,less+2}; for(int z=0;z<4;z++){int kk=ks[z]; if(1<=kk && kk<=east) cands.push_back(order[kth_bit(bit,kk)-1]);} current_city=city; sort(cands.begin(),cands.end(),cmp_candidate); if(!cands.empty()){nearv[city]=cands[0]; if(cands.size()>1) secondv[city]=cands[1];} mark_bit(bit,r); } }
ll add_inf(ll x,ll y){ if(x>=INF || y>=INF || x+y>=INF) return INF; return x+y; }
void build_lift(){ rounds_count=max(1, (int)ceil(log2(max(1,n)))); while((1<<rounds_count)<=n-1) rounds_count++; gup.assign(rounds_count, vector<int>(n+1)); da.assign(rounds_count, vector<ll>(n+1,INF)); db.assign(rounds_count, vector<ll>(n+1,INF)); for(int i=1;i<=n;i++){int s=secondv[i]; if(!s) continue; da[0][i]=llabs(h[i]-h[s]); int t=nearv[s]; if(t){db[0][i]=llabs(h[s]-h[t]); gup[0][i]=t;}} for(int k=1;k<rounds_count;k++){ for(int i=0;i<=n;i++){int mid=gup[k-1][i]; gup[k][i]=gup[k-1][mid]; da[k][i]=add_inf(da[k-1][i],da[k-1][mid]); db[k][i]=add_inf(db[k-1][i],db[k-1][mid]); } } }
pair<ll,ll> drive(int start,ll budget){ int city=start; ll remain=budget,total_a=0,total_b=0; for(int k=rounds_count-1;k>=0;k--){ ll cost=da[k][city]+db[k][city]; if(cost<=remain){total_a+=da[k][city]; total_b+=db[k][city]; remain-=cost; city=gup[k][city];}} if(da[0][city]<=remain) total_a+=da[0][city]; return {total_a,total_b}; }
bool better_start(int cand,int best,ll ca,ll cb,ll ba,ll bb){ if(best==0) return true; if(cb==0 && bb==0){ if(h[cand]!=h[best]) return h[cand]>h[best]; return cand<best;} if(cb==0) return false; if(bb==0) return true; __int128 left=(__int128)ca*bb, right=(__int128)ba*cb; if(left!=right) return left<right; if(h[cand]!=h[best]) return h[cand]>h[best]; return cand<best; }
int main(){ios::sync_with_stdio(false); cin.tie(nullptr); if(!(cin>>n)) return 0; h.assign(n+1,0); for(int i=1;i<=n;i++) cin>>h[i]; ll x0; cin>>x0; int m; cin>>m; vector<pair<int,ll> > queries(m); for(int i=0;i<m;i++) cin>>queries[i].first>>queries[i].second; nearest_two(); build_lift(); int ans=0; ll besta=0,bestb=0; for(int st=1;st<=n;st++){pair<ll,ll> p=drive(st,x0); if(better_start(st,ans,p.first,p.second,besta,bestb)){ans=st;besta=p.first;bestb=p.second;}} cout<<ans<<'\n'; for(int i=0;i<m;i++){pair<ll,ll> p=drive(queries[i].first,queries[i].second); cout<<p.first<<' '<<p.second<<'\n';} return 0;}#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-02 10:08
# update_at: 2026-10-02 10:08
import sys
from fractions import Fraction
from math import inf
INF = 10**18 # 倍增表里"这一轮走不完"的哨兵代价,恒大于预算上限 10^9
def prefix(bit: list[int], p: int) -> int:
"""树状数组前缀和:已标记(东部)城市里排名落在前 p 名的数量。"""
total = 0
while p:
total += bit[p]
p -= p & -p
return total
def mark(bit: list[int], p: int) -> None:
"""把排名 p 的城市计入树状数组——它此后就是更小编号城市的"东部城市"。"""
n = len(bit) - 1
while p <= n:
bit[p] += 1
p += p & -p
def kth(bit: list[int], k: int) -> int:
"""第 k 小已标记排名所在的位置(树状数组二进制提升,O(log n))。"""
n = len(bit) - 1
pos = 0
step = 1 << (n.bit_length() - 1)
while step:
nxt = pos + step
if nxt <= n and bit[nxt] < k:
k -= bit[nxt]
pos = nxt
step >>= 1
return pos + 1
def nearest_two(h: list[int]) -> tuple[list[int], list[int]]:
"""每个城市的最近、次近东部城市(0 表示不存在)。
距离即海拔差绝对值,同距视为海拔低者更近。按海拔升序、从大编号往小编号处理,
已标记的恰是东部城市;候选只需已标记排名中紧邻它的至多四个(低侧两个、高侧两个),
更外侧的东部城市距离严格更大,进不了前二。
"""
n = len(h) - 1
order = sorted(range(1, n + 1), key=h.__getitem__) # 海拔升序的城市编号
rank = [0] * (n + 1)
for r, city in enumerate(order, 1):
rank[city] = r # 树状数组用 1-based 排名
bit = [0] * (n + 1)
near = [0] * (n + 1)
second = [0] * (n + 1)
for city in range(n, 0, -1):
r = rank[city]
less = prefix(bit, r - 1) # 海拔比它低的东部城市个数
east = n - city # 东部城市总数(恰有 n-city 个已标记)
# 低侧第 less、less-1 名,高侧第 less+1、less+2 名
cands = [order[kth(bit, k) - 1] for k in (less, less - 1, less + 1, less + 2) if 1 <= k <= east]
cands.sort(key=lambda c: (abs(h[city] - h[c]), h[c])) # 距离相同视为海拔低者更近
if cands:
near[city] = cands[0]
second[city] = cands[1] if len(cands) > 1 else 0
mark(bit, r)
return near, second
def build_lift(h: list[int], near: list[int], second: list[int]) -> tuple[list, list, list, int]:
"""倍增表:g[k][i] 从城市 i 出发走 2^k 轮(小A一段+小B一段)到达的城市,
da/db 分别是这些轮里小A、小B 的里程;走不完的轮用 INF 代价标记。"""
n = len(h) - 1
rounds = max(1, (n - 1).bit_length()) # 2^rounds > n-1,任何合法轮数都能二进制拆分
g = [[0] * (n + 1) for _ in range(rounds)]
da = [[INF] * (n + 1) for _ in range(rounds)]
db = [[INF] * (n + 1) for _ in range(rounds)]
for i in range(1, n + 1):
s = second[i]
if not s: # 小A没有第二近可选:一轮都开不起来
continue
da[0][i] = abs(h[i] - h[s]) # 第 1 天:小A开到次近城市
t = near[s] # 第 2 天:小B在小A落点处接棒
if t:
db[0][i] = abs(h[s] - h[t]) # 小B开到最近城市
g[0][i] = t
for k in range(1, rounds):
for i in range(n + 1):
m = g[k - 1][i] # 前半轮落点,后半轮从那里接着走
g[k][i] = g[k - 1][m]
da[k][i] = da[k - 1][i] + da[k - 1][m]
db[k][i] = db[k - 1][i] + db[k - 1][m]
return g, da, db, rounds
def drive(start: int, budget: int, g: list, da: list, db: list, rounds: int) -> tuple[int, int]:
"""从 start 出发、预算 budget,返回 (小A总里程, 小B总里程)。"""
city, remain, total_a, total_b = start, budget, 0, 0
for k in range(rounds - 1, -1, -1): # 从大到小贪心跳跃,装下尽可能多的整轮
cost = da[k][city] + db[k][city]
if cost <= remain:
total_a += da[k][city]
total_b += db[k][city]
remain -= cost
city = g[k][city]
# 整轮都放不下后,小A还能单独再开一段(此后小B必然接不上,旅程结束)
if da[0][city] <= remain:
total_a += da[0][city]
return total_a, total_b
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n = next(data)
h = [0] + [next(data) for _ in range(n)]
x0 = next(data)
m = next(data)
queries = [(next(data), next(data)) for _ in range(m)]
near, second = nearest_two(h)
g, da, db, rounds = build_lift(h, near, second)
# 第一问:枚举起点,比值取最小;b=0 视为无穷大,无穷大之间相等,同比值取海拔更高者
ranked = []
for start in range(1, n + 1):
total_a, total_b = drive(start, x0, g, da, db, rounds)
ranked.append((Fraction(total_a, total_b) if total_b else inf, -h[start], start))
out = [str(min(ranked)[2])]
for start, budget in queries:
total_a, total_b = drive(start, budget, g, da, db, rounds)
out.append(f"{total_a} {total_b}")
print("\n".join(out))
if __name__ == "__main__":
solve()复杂度
- 时间复杂度:预处理(排序、树状数组、建表)
;单次询问 ;第一问枚举 个起点共 。合计 ,与 C++ 正解同阶。实测最大测例约 1.1s(本地限时 20s),按 oj-python-style 约定允许 Python TLE。 - 空间复杂度:倍增表三层
层 ,即 ;实测峰值约 246MB,超出题面 32MB 内存限制,按约定允许 MLE。
总结
- 选城规则的数学本质是"距离 = 海拔差":按海拔排序后,最近/次近只可能是排序位置两侧至多 4 个候选,按 (距离, 海拔) 取前两名即可。
- "候选必须在东边"是动态条件:按编号倒序处理 + 树状数组维护海拔排名集合,把前驱/后继查询压到
,全部城市一次 预处理,链上每步变 查表。 - 确定性旅程链按"轮"倍增,预算为正故可从大到小贪心跳
轮;唯一不能按整轮处理的是结尾小A 单独那一段,判 da[0]即可。 - 第一问的比值、
、并列取海拔更高,全部规则压缩成一个可比较的三元组键,用一个 min完成。
