打打打打打卡
题目概述
有 n 个城市从西到东编号 1~n,每个城市海拔为 h[i]。两个城市间的距离为海拔差的绝对值。
小 A 和小 B 从某个城市出发向东旅行,小 A 先开车,之后每天轮换。驾驶规则:
- 小 B:选择当前位置东边最近的城市(距离最近,距离相同选海拔低的)
- 小 A:选择当前位置东边第二近的城市
如果无法选择目的地或总距离超过 x,则结束旅行。
两个问题:
- 给定
x0,从哪个城市出发使 A走的路程 / B走的路程 比值最小 - 给定 m 组
(s, x),求 A 和 B 各自走的路程
数据范围
1 ≤ n, m ≤ 10^5-10^9 ≤ h[i] ≤ 10^91 ≤ s[i] ≤ n0 ≤ x[i] ≤ 10^9
算法分析
难点
- 对每个城市快速求出最近和第二近的城市
- 多次查询(m 次)不能逐次模拟
- 起点枚举(n 个)需要高效
解决方案
- 双向链表:按海拔排序,O(n log n) 预处理出每个城市的
ga[i](A的选择)和gb[i](B的选择) - 倍增:预处理走
2^j轮(A+B算一轮)的信息,O(log n) 查询 - 枚举所有起点:O(n log n) 求解问题1
算法详解
一、预处理最近和第二近城市
思路
对于城市 i,需要找东边(编号 > i)且海拔差最小的两个城市。
如果按海拔排序,离 i 最近的候选城市一定在 i 的前驱、前驱的前驱、后继、后继的后继中。
为什么?
- 海拔比 i 高的城市中,最近的两个是 i 的后继和后继的后继
- 海拔比 i 低的城市中,最近的两个是 i 的前驱和前驱的前驱
- 综合起来只需考虑这 4 个候选
实现步骤
- 按海拔排序,建立双向链表
- 从编号 1 到 n 依次处理:
- 取出当前节点的前驱、前驱的前驱、后继、后继的后继
- 从中选出最近(dist 最小,相同选海拔低)和第二近
- 记录到
gb[i](最近,B去)和ga[i](第二近,A去) - 从链表中删除当前节点(确保后面的城市只考虑东边的城市)
void preprocess() {
// 按海拔排序建链表
sort(node + 1, node + n + 1, cmp);
for (int i = 1; i <= n; i++) {
pos[node[i].id] = i;
node[i].pre = i - 1;
node[i].nxt = i + 1;
}
for (int i = 1; i <= n; i++) {
int p = pos[i];
int cand[5], cnt = 0;
// 取前驱、前驱的前驱、后继、后继的后继
if (node[p].pre) cand[++cnt] = node[node[p].pre].id;
if (node[node[p].pre].pre) cand[++cnt] = node[node[node[p].pre].pre].id;
if (node[p].nxt) cand[++cnt] = node[node[p].nxt].id;
if (node[node[p].nxt].nxt) cand[++cnt] = node[node[node[p].nxt].nxt].id;
// 选出最近和第二近
int first = 0, second = 0;
for (int j = 1; j <= cnt; j++) {
if (isBetter(cand[j], first, i)) {
second = first;
first = cand[j];
} else if (isBetter(cand[j], second, i)) {
second = cand[j];
}
}
gb[i] = first; // B去最近
ga[i] = second; // A去第二近
// 删除当前节点
if (node[p].pre) node[node[p].pre].nxt = node[p].nxt;
if (node[p].nxt) node[node[p].nxt].pre = node[p].pre;
}
}
二、倍增预处理
状态定义
一轮 = A先走一步,然后B走一步(因为小A先开车)
f[i][j]:从城市 i 出发,走2^j轮后到达的城市da[i][j]:走2^j轮中 A 走的总路程db[i][j]:走2^j轮中 B 走的总路程
初始化(2^0 轮 = 1轮)
从 i 出发:
- A走到 ga[i],路程 da[i][0] = |h[ga[i]] - h[i]|
- B从 ga[i] 走到 gb[ga[i]],路程 db[i][0] = |h[gb[ga[i]]] - h[ga[i]]|
- 到达城市 f[i][0] = gb[ga[i]]
#### 递推
f[i][j] = f[f[i][j-1]][j-1]
da[i][j] = da[i][j-1] + da[f[i][j-1]][j-1]
db[i][j] = db[i][j-1] + db[f[i][j-1]][j-1]
```cpp
void initDoubling() {
// 初始化走一轮
for (int i = 1; i <= n; i++) {
if (ga[i]) {
int na = ga[i];
da[i][0] = abs(h[na] - h[i]);
if (gb[na]) {
f[i][0] = gb[na];
db[i][0] = abs(h[gb[na]] - h[na]);
}
}
}
// 倍增递推
for (int j = 1; j < LOG; j++) {
for (int i = 1; i <= n; i++) {
int mid = f[i][j-1];
if (mid) {
f[i][j] = f[mid][j-1];
da[i][j] = da[i][j-1] + da[mid][j-1];
db[i][j] = db[i][j-1] + db[mid][j-1];
}
}
}
}
三、查询函数
给定起点 s 和限制 x,求 A 和 B 走的总路程。
pair<ll, ll> query(int s, ll x) {
ll sumA = 0, sumB = 0;
int cur = s;
// 先走完整的轮次(从大到小枚举)
for (int j = LOG-1; j >= 0; j--) {
if (f[cur][j] && da[cur][j] + db[cur][j] <= x) {
sumA += da[cur][j];
sumB += db[cur][j];
x -= da[cur][j] + db[cur][j];
cur = f[cur][j];
}
}
// 最后尝试让 A 单独走一步(可能不够一轮)
if (ga[cur] && abs(h[ga[cur]] - h[cur]) <= x) {
sumA += abs(h[ga[cur]] - h[cur]);
}
return {sumA, sumB};
}
四、问题1求解
枚举所有起点,计算比值,取最小。
int solveQuestion1(ll x0) {
int ans = 1;
long double minRatio = 1e18;
for (int i = 1; i <= n; i++) {
auto [a, b] = query(i, x0);
long double ratio;
if (b == 0) ratio = 1e18; // 无穷大
else ratio = (long double)a / b;
if (ratio < minRatio ||
(ratio == minRatio && h[i] > h[ans])) {
minRatio = ratio;
ans = i;
}
}
return ans;
}

浙公网安备 33010602011771号