海拔排序双向链表 O(N) 求出每座城市东侧的最近与次近城市,再用倍增预处理「走 2^k 步后到哪、两人各开多少公里」,每个询问 O(log N) 跳跃回答。
OJ: roj
题目 ID: 3154
难度:提高+/省选-
标签:倍增链表预处理python
创建: 2026-10-01 21:19
更新: 2026-10-10 11:30
形式化题目
数轴上有
从某座城市
- 当前司机为小A 时,走到东侧第二近的城市;当前司机为小B 时,走到东侧最近的城市;
- 两个候选城市距离相同时,海拔低者视为更近;
- 第一天由小A 先开车,之后每天轮换;
- 若某位司机在当前位置以东找不到自己的目标城市,或者开过去会使累计里程超过
,旅行立即结束。
需要回答两个问题:
- 给定
:从哪个城市出发,小A 的总里程与小B 的总里程之比最小?小B 里程为 时比值视为 ,两个 相等;比值并列时取出发城市海拔最高者,输出该城市编号。 - 给定
组 :输出小A、小B 各自的总里程。
flowchart LR
C1["1 · H=4"] --- C2["2 · H=5"] --- C3["3 · H=6"] --- C4["4 · H=1"] --- C5["5 · H=2"] --- C6["6 · H=3"] --- C7["7 · H=7"] --- C8["8 · H=8"] --- C9["9 · H=9"] --- C10["10 · H=10"]
C1 -- "A +1" --> C2
C2 -- "B +1" --> C3
C3 -- "A +2" --> C8
C8 -- "B +1" --> C9
C9 -. "小A 找不到次近,停" .-> C10
上图画出样例中 3 2。
正解
思路
第一步:O(N) 找出每个城市东侧的「最近」与「次近」。
如果把所有城市按海拔排成一条双向链表,那么城市
即隔着城市的
但题目要求目的地在东侧(编号更大)。技巧是按编号
第二步:把「路径完全确定」变成倍增。
起点定了,谁先开定了,之后的路径就唯一确定;每天的状态只有「当前城市 + 当前司机」。逐日模拟一次是
nxt[k][w][i]:从出发、由 (0 = 小A,1 = 小B)先开,走 步后到达的城市( 表示走不动); da[k][w][i]、db[k][w][i]:这步里两人各自的里程。
拼接
flowchart LR
subgraph K1["k = 1:共 2 步"]
s1["i, w"] -->|"1 步,换人"| m1["mid, w^1"] -->|"1 步,再换人"| e1["2^k 后,回到 w"]
end
subgraph K2["k ≥ 2:两段各 2^(k-1) ≥ 2 步(偶数)"]
s2["i, w"] -->|"2^(k-1) 步"| m2["mid, w"] -->|"2^(k-1) 步"| e2["2^k 后,仍是 w"]
end
:整段 2 步 = 1 步 + 1 步,中途换人, ; :前后两段各有偶数步,先开者换回去, 。
边界
里程数组把两段的 da、db 分别相加即可。中途任一段走不动(
第三步:跳跃回答。 从高位到低位枚举 nxt[k][w][cur] 存在且跳过去累计里程不超
第四步:两个问题的回答。 问题 2 直接 drive(S_i, X_i)。问题 1 枚举所有起点调用同一函数得
下表是样例预处理出的目标城市(「—」表示东侧不存在):
| 城市 |
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|
| 海拔 |
4 | 5 | 6 | 1 | 2 | 3 | 7 | 8 | 9 | 10 |
| 小B(最近) | 6 | 3 | 7 | 5 | 6 | 7 | 8 | 9 | 10 | — |
| 小A(次近) | 2 | 6 | 8 | 6 | 7 | 8 | 9 | 10 | — | — |
注意城市
最后用问题 1 串起全表。
代码
/**
* 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 <cstdio>
#include <vector>
#include <algorithm>
typedef long long ll;
struct Cand {
ll id;
ll dist;
ll h;
};
bool cand_less(const Cand& a, const Cand& b) {
if (a.dist != b.dist) return a.dist < b.dist;
return a.h < b.h; // 距离相同时海拔低者算更近
}
ll n;
std::vector<ll> h;
std::vector<ll> na, nb; // na[i] / nb[i]:小A / 小B 从 i 向东要去的城市
std::vector<ll> west, east;
struct Tab {
int nxt; // 走 2^k 步后到达的城市
ll da; // 这 2^k 步里小A 的里程
ll db; // 小B 的里程
};
std::vector<Tab> up; // up[(k*2+w)*(n+1)+i]
int main() {
if (scanf("%lld", &n) != 1) return 0; // 空输入安全返回
h.assign(n + 1, 0);
for (ll i = 1; i <= n; i++) scanf("%lld", &h[i]);
std::vector<ll> ord(n);
for (ll i = 0; i < n; i++) ord[i] = i + 1;
for (ll i = 0; i < n; i++) { // 按海拔升序
for (ll j = i + 1; j < n; j++) {
if (h[ord[j]] < h[ord[i]]) {
ll t = ord[i]; ord[i] = ord[j]; ord[j] = t;
}
}
}
west.assign(n + 1, 0);
east.assign(n + 1, 0);
for (ll i = 0; i + 1 < n; i++) {
east[ord[i]] = ord[i + 1];
west[ord[i + 1]] = ord[i];
}
na.assign(n + 1, 0);
nb.assign(n + 1, 0);
Cand cand[4];
for (ll i = 1; i <= n; i++) {
ll cnt = 0;
ll j = west[i];
for (ll t = 0; t < 2; t++) { // 海拔方向两侧各取 2 个候选
if (j) {
cand[cnt].id = j;
cand[cnt].dist = h[i] - h[j] > 0 ? h[i] - h[j] : h[j] - h[i];
cand[cnt].h = h[j];
cnt++;
j = west[j];
}
}
j = east[i];
for (ll t = 0; t < 2; t++) {
if (j) {
cand[cnt].id = j;
cand[cnt].dist = h[i] - h[j] > 0 ? h[i] - h[j] : h[j] - h[i];
cand[cnt].h = h[j];
cnt++;
j = east[j];
}
}
for (ll a = 0; a < cnt; a++) { // 手写插入排序,按 (距离, 海拔) 升序
for (ll b = a + 1; b < cnt; b++) {
if (cand_less(cand[b], cand[a])) {
Cand t = cand[a]; cand[a] = cand[b]; cand[b] = t;
}
}
}
nb[i] = (cnt > 0) ? cand[0].id : 0;
na[i] = (cnt > 1) ? cand[1].id : 0;
east[west[i]] = east[i]; // 删掉 i,链表里剩下的全是东边城市
west[east[i]] = west[i];
}
ll log = 1;
while ((1LL << log) < n) log++;
if (log < 1) log = 1;
up.assign((size_t)log * 2 * (n + 1), Tab());
for (ll i = 1; i <= n; i++) {
if (na[i]) {
up[0 * 2 * (n + 1) + 0 * (n + 1) + i].nxt = (int)na[i];
ll d = h[i] - h[na[i]];
up[0 * 2 * (n + 1) + 0 * (n + 1) + i].da = d > 0 ? d : -d;
}
if (nb[i]) {
up[0 * 2 * (n + 1) + 1 * (n + 1) + i].nxt = (int)nb[i];
ll d = h[i] - h[nb[i]];
up[0 * 2 * (n + 1) + 1 * (n + 1) + i].db = d > 0 ? d : -d;
}
}
for (ll k = 1; k < log; k++) {
ll pre = k - 1;
for (ll w = 0; w < 2; w++) {
ll w2 = (k == 1) ? (w ^ 1) : w; // 后一段的先开者只与 k 有关
for (ll i = 1; i <= n; i++) {
ll mid = up[(pre * 2 + w) * (n + 1) + i].nxt;
if (mid) {
Tab& cur = up[(k * 2 + w) * (n + 1) + i];
Tab& a = up[(pre * 2 + w) * (n + 1) + i];
Tab& b = up[(pre * 2 + w2) * (n + 1) + mid];
cur.nxt = b.nxt;
cur.da = a.da + b.da;
cur.db = a.db + b.db;
}
}
}
}
ll x0, m;
scanf("%lld %lld", &x0, &m);
std::vector<ll> qs(m), qx(m);
for (ll i = 0; i < m; i++) scanf("%lld %lld", &qs[i], &qx[i]);
// 问题 1:枚举起点找 a/b 最小的出发城市
ll best_s = 1, best_a = 0, best_b = 0;
for (ll s = 1; s <= n; s++) {
ll a = 0, b = 0, cur = s, w = 0;
for (ll k = log - 1; k >= 0; k--) {
Tab& t = up[(k * 2 + w) * (n + 1) + cur];
if (t.nxt && a + b + t.da + t.db <= x0) {
a += t.da;
b += t.db;
cur = t.nxt;
if (k == 0) w ^= 1; // 只有 2^0 = 1 步是奇数步,才换司机
}
}
if (s == 1) {
best_s = 1; best_a = a; best_b = b;
continue;
}
bool take;
if (b == 0) {
take = (best_b == 0 && h[s] > h[best_s]); // 两个无穷大之间比海拔
} else if (best_b == 0) {
take = true; // 有限比值优于无穷大
} else {
ll lhs = a * best_b, rhs = best_a * b; // 交叉相乘比较 a/b
take = (lhs < rhs) || (lhs == rhs && h[s] > h[best_s]);
}
if (take) {
best_s = s; best_a = a; best_b = b;
}
}
printf("%lld\n", best_s);
for (ll i = 0; i < m; i++) { // 问题 2:逐个询问模拟
ll s = qs[i], x = qx[i];
ll a = 0, b = 0, cur = s, w = 0;
for (ll k = log - 1; k >= 0; k--) {
Tab& t = up[(k * 2 + w) * (n + 1) + cur];
if (t.nxt && a + b + t.da + t.db <= x) {
a += t.da;
b += t.db;
cur = t.nxt;
if (k == 0) w ^= 1;
}
}
printf("%lld %lld\n", a, b);
}
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-01 21:19
# update_at: 2026-10-01 21:39
import sys
from array import array
# 城市编号从 1 开始,0 统一表示「不存在 / 无法前进」的哨兵。
def choose_targets(h: list[int]) -> tuple[list[int], list[int]]:
"""返回 (na, nb):na[i] / nb[i] 是小A / 小B 从 i 向东要去的城市。
把城市按海拔排成双向链表后,距离最近、次近的候选只会藏在海拔相邻的
至多 4 个城市里;按编号从西往东处理并删掉已处理城市,
链表里剩下的恰好都是东边的城市。
"""
n = len(h) - 1
order = sorted(range(1, n + 1), key=lambda c: h[c])
west = [0] * (n + 1) # 海拔排序链表的左 / 右邻居
east = [0] * (n + 1)
for prev, cur in zip(order, order[1:]):
east[prev] = cur
west[cur] = prev
na = [0] * (n + 1)
nb = [0] * (n + 1)
for i in range(1, n + 1):
cand = []
j = west[i]
for _ in range(2): # 海拔方向两侧各取 2 个候选
if j:
cand.append(j)
j = west[j]
j = east[i]
for _ in range(2):
if j:
cand.append(j)
j = east[j]
# 距离相同时海拔低者算更近,所以排序后第 1 个给小B、第 2 个给小A
cand.sort(key=lambda c: (abs(h[i] - h[c]), h[c]))
nb[i] = cand[0] if cand else 0
na[i] = cand[1] if len(cand) > 1 else 0
east[west[i]] = east[i] # 删掉 i,链表里剩下的全是东边城市
west[east[i]] = west[i]
return na, nb
def build_tables(na: list[int], nb: list[int], h: list[int]) -> tuple[int, list, list, list]:
"""建倍增表,返回 (log, nxt, da, db)。
nxt[k][w][i]:从 i 出发、由 w(0=小A,1=小B)先开,走 2^k 步后到达的城市;
da / db 同布局,记录这 2^k 步里两人各自的里程。
后 2^(k-1) 步的先开者:k=1 时只剩 1 步要换人,k>=2 时走偶数步不换人。
"""
n = len(h) - 1
log = max(1, (n - 1).bit_length()) # 任意旅程步数 <= n-1,2^log 足够覆盖
nxt = [[array('i', [0]) * (n + 1) for _ in range(2)] for _ in range(log)]
da = [[array('q', [0]) * (n + 1) for _ in range(2)] for _ in range(log)]
db = [[array('q', [0]) * (n + 1) for _ in range(2)] for _ in range(log)]
for i in range(1, n + 1):
if na[i]:
nxt[0][0][i] = na[i]
da[0][0][i] = abs(h[i] - h[na[i]])
if nb[i]:
nxt[0][1][i] = nb[i]
db[0][1][i] = abs(h[i] - h[nb[i]])
for k in range(1, log):
pre = k - 1
for w in range(2):
w2 = w ^ 1 if k == 1 else w # 后一段的先开者只与 k 有关
for i in range(1, n + 1):
mid = nxt[pre][w][i]
if mid:
nxt[k][w][i] = nxt[pre][w2][mid]
da[k][w][i] = da[pre][w][i] + da[pre][w2][mid]
db[k][w][i] = db[pre][w][i] + db[pre][w2][mid]
return log, nxt, da, db
def drive(s: int, x: int, log: int, nxt: list, da: list, db: list) -> tuple[int, int]:
"""从 s 出发最多开 x 公里,返回(小A总里程,小B总里程)。"""
a = b = 0
cur = s
w = 0 # 第一天小A先开
for k in range(log - 1, -1, -1):
step = da[k][w][cur] + db[k][w][cur]
if nxt[k][w][cur] and a + b + step <= x:
a += da[k][w][cur]
b += db[k][w][cur]
cur = nxt[k][w][cur]
if k == 0: # 只有 2^0 = 1 步是奇数步,才换司机
w ^= 1
return a, 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)]
na, nb = choose_targets(h)
log, nxt, da, db = build_tables(na, nb, h)
# 问题 1:枚举起点找 a/b 最小的出发城市;b=0 视为无穷大,同值比海拔
best_s = 1
best_a, best_b = drive(1, x0, log, nxt, da, db)
for s in range(2, n + 1):
a, b = drive(s, x0, log, nxt, da, db)
if b == 0:
take = best_b == 0 and h[s] > h[best_s] # 两个无穷大之间比海拔
elif best_b == 0:
take = True # 有限比值优于无穷大
else:
lhs, rhs = a * best_b, best_a * b # 交叉相乘比较 a/b,避免浮点误差
take = lhs < rhs or (lhs == rhs and h[s] > h[best_s])
if take:
best_s, best_a, best_b = s, a, b
out = [str(best_s)]
for s, x in queries: # 问题 2:逐个询问模拟
a, b = drive(s, x, log, nxt, da, db)
out.append(f'{a} {b}')
print('\n'.join(out))
if __name__ == "__main__":
solve()复杂度
- 时间:预处理排序 + 建表
;问题 1 枚举 个起点 ;问题 2 每次询问 ,合计 。 - 空间:三张
的表, 。
总结
- 模型:向东行驶 + 确定性选择规则 ⇒ 路径由(起点,先开者)唯一决定 ⇒ 倍增。
- 两个易错点:
- 换司机只发生在奇数步的段——倍增拼接时
要交换先开者, 不交换, 走完交换; - 距离并列按海拔低者算近;比值并列(含两个
)按起点海拔高者输出。
- 换司机只发生在奇数步的段——倍增拼接时
- 链表删除顺序(按编号从西往东处理、处理完即删)是「目的地只在东侧」这一约束的零成本实现。
图示解析
从样例表到第一次跳跃
以 drive 的二进制分解(
| 尝试 |
段长 | 是否成行 | 累计 (A, B) | 当前城市 / 先开者 |
|---|---|---|---|---|
| 3 | 8 步 | 表项为 0(走不满 8 步),跳过 | (0, 0) | 1 / A |
| 2 | 4 步 | 1→2→3→8→9,合计 +5 |
(3, 2) | 9 / A |
| 1 | 2 步 | 9 以东走不满 2 步,表项为 0,跳过 | (3, 2) | 9 / A |
| 0 | 1 步 | 9 的次近不存在,表项为 0,跳过 | (3, 2) | 结束 |
逐行对照:高位先试大步,能跳则跳,剩余距离交给低位;