形式化题目

NN 个城市按编号从西到东排成一行,城市 ii 的海拔 HiH_i 互不相同,城市 ii 与 jj 之间的距离为 di,j=∣Hi−Hj∣d_{i,j} = |H_i - H_j|。一次旅程从起点 SS 出发、只向编号增大的方向行驶,累计里程不得超过 XX。小A 与小B 轮流开车,第一天由小A 驾驶:小B 每天开到东边最近的城市,小A 每天开到东边第二近的城市;若到两个城市的距离相同,则海拔低的城市视为更近。当当前驾驶者前方没有符合要求的城市,或到达目标会使总里程超过 XX 时,旅程立即结束。

需要回答两个问题:

  1. 对给定的 X0X_0,求每个起点下小A 总里程 aa 与小B 总里程 bb 的比值 a/ba/b(b=0b = 0 时视为 +∞+\infty,两个 +∞+\infty 视为相等),输出比值最小的起点,并列时取海拔更高的城市;
  2. 对 MM 组询问 (Si,Xi)(S_i, X_i),输出该起点、该预算下两人的总里程。

样例 1 中 N=4N = 4,H=(2,3,1,4)H = (2, 3, 1, 4),X=3X = 3,从城市 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 — — 前方已无城市,旅程结束

表里"同距比海拔"和"找不到目标就结束"两条规则都真实触发过,后面的预处理与模拟必须原样复现它们。

正解

思路

一句话本质:把"每天开到哪"一次性预处理成查表(按海拔排序 + 树状数组,O(Nlog⁡N)O(N \log N)),把确定性的旅程链按"轮"(小A 一段 + 小B 一段)建成倍增表,每个询问从大到小贪心跳 2k2^k 轮,O(log⁡N)O(\log N) 出答案。

朴素做法卡在哪。 逐询问模拟:每天都要在当前城市的东边所有城市里挑最近/次近,单步就要扫描 O(N)O(N),一条链最多 NN 步;第一问还要枚举 NN 个起点,而 N,MN, M 可达 10510^5。瓶颈有二:其一,选目的地在不同起点、不同轮次里被反复计算,而这些链大量重叠;其二,链本身太长。两个瓶颈分别对应下面两次预处理。

观察 1:距离就是海拔差,最近/次近只看至多 4 个候选。 把城市按海拔从小到大排序。对城市 ii(排序位置 pp),东边城市按海拔排开形如:

text
… < x-2 < x-1 < H_i < x+1 < x+2 < …
      低侧      |       高侧

任何比 x−2x_{-2} 更低的东边城市 zz 都有 Hz<Hx−2H_z < H_{x_{-2}},距离 Hi−HzH_i - H_z 严格大于 x−2x_{-2} 的距离;而 x−2,x−1x_{-2}, x_{-1} 本身就有两个候选的距离不大于它,所以 zz 不可能进入距离前二,高侧对称。于是最近、次近一定在 {x−2,x−1,x+1,x+2}\{x_{-2}, x_{-1}, x_{+1}, x_{+2}\} 这至多 4 个候选之中,按 (距离, 海拔) 排序取前两个即可——"同距取海拔低者"也自动满足:同距时低侧候选的海拔必然更低。

观察 2:候选还必须"在东边",用树状数组把动态条件变成静态查询。 候选要求编号大于 ii。改为按编号从大到小处理:处理到 ii 时,树状数组里已标记的恰好是 {i+1,…,N}\{i+1, \dots, N\},按海拔排名维护它们的计数,就能 O(log⁡N)O(\log N) 地查出 ii 海拔位置两侧各两个候选(前驱、次前驱、后继、次后继)——这正是观察 1 的 4 个候选在"东边城市集合"中的对应物,同样至多 4 个。全部城市预处理完,near[i]、second[i] 查表 O(1)O(1)。

观察 3:旅程是确定性链,按"轮"倍增。 起点一定,每天去哪唯一确定:一轮 = 小A 开到次近、小B 接着开到最近;链上最多 N−1N-1 轮(每轮至少向东跨一个编号)。记 g[k][i]g[k][i] 为从城市 ii 出发走 2k2^k 轮到达的城市,da[k][i],db[k][i]da[k][i], db[k][i] 为这些轮里两人的里程,用"前半轮落点再接后半轮"递推 g[k][i]=g[k−1][g[k−1][i]]g[k][i] = g[k-1][g[k-1][i]]。走不完的轮(小A 没有次近、或小B 在落点处无路)用哨价 INF 标记,永不被选中。

观察 4:预算贪心跳整轮,最后补小A 一段。 所有轮里程为正、前缀和单调。从 k=⌈log⁡2N⌉−1k = \lceil \log_2 N \rceil - 1 降到 0,da[k][city] + db[k][city] <= 剩余预算 就跳:这恰好装下预算内最多的整轮——把最优轮数写成二进制,从高到低逐位对齐,某位为 1 当且仅当对应那 2k2^k 轮的前缀里程装得下当前剩余预算。整轮都放不下后,小A 还可以单独再开一段;若小B 此后还能接上,这一轮整轮就会被贪心跳掉,与贪心结束矛盾,故小B 必然接不上或无路、旅程就此结束,所以结尾再判一次 da[0][city] <= 剩余 即可。样例 2 从城市 1 出发 X=7X = 7 的跳跃过程:

flowchart LR
    a["城市 1,预算 7"] -->|"k=1 跳 2 轮,耗 5"| b["城市 9,剩余 2"]
    b -->|"k=0 与尾段:9 没有次近城市"| c["结束,A 3,B 2"]

贪心先在 k=1k=1 跳掉两轮(小A 共 3、小B 共 2,耗掉 5),落在城市 9;从城市 9 出发连小A 的第一段都不存在(东边只剩城市 10,凑不出次近),倍增表与尾段都被 INF 挡住,旅程结束,结果 3 2 与题面一致。

第一问:枚举起点 + 精确比大小。 每个起点跑一次 O(log⁡N)O(\log N) 得到 (a,b)(a, b),用 Fraction(a, b)(b>0b > 0)或 math.inf(b=0b = 0)表示比值,元组第二项取 −H-H 保证并列时海拔更高者胜出,一个 min 就实现了题面的全部比较规则。样例 2 在 X0=7X_0 = 7 下十个起点的结果:

起点 小A 里程 小B 里程 比值 说明
1 3 2 1.51.5
2 2 4 0.50.5 并列最小
3 2 1 22
4 2 4 0.50.5 并列最小,海拔 1<51 < 5 落败
5 5 1 55
6 5 1 55
7 2 1 22
8 2 0 ∞\infty 小B 一段都没开
9 0 0 ∞\infty 未启程
10 0 0 ∞\infty 未启程

比值最小的 0.50.5 由城市 2、4 并列,海拔 H2=5>H4=1H_2 = 5 > H_4 = 1,故输出 2,与题面一致。

代码

nearest_two 实现观察 1、2 的预处理,build_lift 实现观察 3 的倍增表,drive 是观察 4 的单次询问回答,solve 末尾的三元组 min 完成第一问的比较规则。

cpp
/**
 * 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;}
python
#!/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()

复杂度

  • 时间复杂度:预处理(排序、树状数组、建表)O(Nlog⁡N)O(N \log N);单次询问 O(log⁡N)O(\log N);第一问枚举 NN 个起点共 O(Nlog⁡N)O(N \log N)。合计 O((N+M)log⁡N)O((N + M) \log N),与 C++ 正解同阶。实测最大测例约 1.1s(本地限时 20s),按 oj-python-style 约定允许 Python TLE。
  • 空间复杂度:倍增表三层 ×\times ⌈log⁡2N⌉\lceil \log_2 N \rceil 层 ×\times (N+1)(N + 1),即 O(Nlog⁡N)O(N \log N);实测峰值约 246MB,超出题面 32MB 内存限制,按约定允许 MLE。

总结

  • 选城规则的数学本质是"距离 = 海拔差":按海拔排序后,最近/次近只可能是排序位置两侧至多 4 个候选,按 (距离, 海拔) 取前两名即可。
  • "候选必须在东边"是动态条件:按编号倒序处理 + 树状数组维护海拔排名集合,把前驱/后继查询压到 O(log⁡N)O(\log N),全部城市一次 O(Nlog⁡N)O(N \log N) 预处理,链上每步变 O(1)O(1) 查表。
  • 确定性旅程链按"轮"倍增,预算为正故可从大到小贪心跳 2k2^k 轮;唯一不能按整轮处理的是结尾小A 单独那一段,判 da[0] 即可。
  • 第一问的比值、+∞+\infty、并列取海拔更高,全部规则压缩成一个可比较的三元组键,用一个 min 完成。