打打打打打卡

题目概述

有 n 个城市从西到东编号 1~n,每个城市海拔为 h[i]。两个城市间的距离为海拔差的绝对值。

小 A 和小 B 从某个城市出发向东旅行,小 A 先开车,之后每天轮换。驾驶规则:

  • 小 B:选择当前位置东边最近的城市(距离最近,距离相同选海拔低的)
  • 小 A:选择当前位置东边第二近的城市

如果无法选择目的地或总距离超过 x,则结束旅行。

两个问题

  1. 给定 x0,从哪个城市出发使 A走的路程 / B走的路程 比值最小
  2. 给定 m 组 (s, x),求 A 和 B 各自走的路程

数据范围

  • 1 ≤ n, m ≤ 10^5
  • -10^9 ≤ h[i] ≤ 10^9
  • 1 ≤ s[i] ≤ n
  • 0 ≤ x[i] ≤ 10^9

算法分析

难点

  1. 对每个城市快速求出最近第二近的城市
  2. 多次查询(m 次)不能逐次模拟
  3. 起点枚举(n 个)需要高效

解决方案

  1. 双向链表:按海拔排序,O(n log n) 预处理出每个城市的 ga[i](A的选择)和 gb[i](B的选择)
  2. 倍增:预处理走 2^j 轮(A+B算一轮)的信息,O(log n) 查询
  3. 枚举所有起点:O(n log n) 求解问题1

算法详解

一、预处理最近和第二近城市

思路

对于城市 i,需要找东边(编号 > i)且海拔差最小的两个城市。

如果按海拔排序,离 i 最近的候选城市一定在 i 的前驱、前驱的前驱、后继、后继的后继中。

为什么?

  • 海拔比 i 高的城市中,最近的两个是 i 的后继和后继的后继
  • 海拔比 i 低的城市中,最近的两个是 i 的前驱和前驱的前驱
  • 综合起来只需考虑这 4 个候选

实现步骤

  1. 按海拔排序,建立双向链表
  2. 从编号 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 出发:

  1. A走到 ga[i],路程 da[i][0] = |h[ga[i]] - h[i]|
  2. B从 ga[i] 走到 gb[ga[i]],路程 db[i][0] = |h[gb[ga[i]]] - h[ga[i]]|
  3. 到达城市 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;
}
posted @ 2026-07-20 22:08  happy0x3F  阅读(8)  评论(0)    收藏  举报