NeFut Logo NeFut
EN 管理员登录

倍增法:高效区间查询与动态覆盖的数学原理与实现

发布于:2026-05-27 09:17 最后更新:2026-06-06 13:04
#C++ #Tutorial

核心逻辑与数学原理

倍增法的指数级跳跃

倍增(Binary Lifting)的核心在于将线性搜索的步长转化为二进制权值的指数级跃迁。设目标转移步数为 $K$,任何正整数 $K$ 都可以被唯一拆分为二进制形式:

$$K = \sum_{i=0}^{\lfloor \log_2 K \rfloor} c_i \cdot 2^i \quad (c_i \in \{0, 1\})$$

通过预处理出步长为 $2^i$ 的状态转移结果,任意距离的跳跃都可以在 $O(\log K)$ 的时间复杂度内通过拼凑二进制位完成。这消除了 $O(K)$ 的逐步遍历,将时间复杂度强行压制在对数级别。

区间最值与幂等覆盖

在区间最值(RMQ)问题中,倍增法的数学基础来源于算子的幂等性(Idempotency),即 $f(x, x) = x$。对于极值算子 $ ext{max}$ 或 $ ext{min}$,两个重叠子区间的最值并集即为全集最值。设区间长度为 $L$,计算出 $k = \lfloor \log_2 L \rfloor$。则区间 $[L, R]$ 可以被两条长度为 $2^k$ 的线段完全覆盖:

$$[L, R] = [L, L + 2^k - 1] \cup [R - 2^k + 1, R]$$

利用这一几何覆盖性质,RMQ 查询可以在 $O(1)$ 时间内完成。


状态设计与算法推导

跳跃跨步的状态设计

定义 $dp[i][j]$ 表示从起点 $i$ 出发,向后跳跃 $2^j$ 步所到达的终点节点编号或积累的权值。基于状态机的阶段划分,跳跃 $2^j$ 步等价于先跳跃 $2^{j-1}$ 步到达中间节点,再从该中间节点续跳 $2^{j-1}$ 步。状态转移方程:

$$dp[i][j] = dp[\,dp[i][j-1]\,][j-1]$$

基准状态 $dp[i][0]$ 由题目给定的直接转移关系(邻接表或单步指针)初始化。外层循环枚举幂次 $j$,内层循环枚举空间节点 $i$,严格遵循拓扑序。

RMQ 极端应用推导

在复杂场景(如环形区间、动态跨步覆盖)中,结合倍增法的指针跳跃。若要覆盖某个区间,将“下一个能覆盖到的最远右端点”抽象为单步转移 $nxt[i]$。建立倍增表:

$$f[i][j] = f[\,f[i][j-1]\,][j-1]$$

其中 $f[i][j]$ 表示从 $i$ 开始,消耗 $2^j$ 个区间所能到达的最远右端点。通过在倍增表上从大到小枚举 $j$,可在 $O(\log N)$ 时间内找出覆盖目标区间所需的最少区间数。


C++ 标准源码

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

// 模拟 NOIP 经典应用:环形区间倍增覆盖与静态 RMQ 结合
struct Segment {
    int l, r, id;
    bool operator<(const Segment& other) const {
        return l < other.l;
    }
};

const int MAX_LOG = 20;

void solve_binary_lifting() {
    int n, m;
    if (!(cin >> n >> m)) return;

    // 考虑环形复制或线性扩张
    vector<Segment> segs(n);
    for (int i = 0; i < n; ++i) {
        cin >> segs[i].l >> segs[i].r;
        segs[i].id = i;
    }

    sort(segs.begin(), segs.end());

    // nxt[i][j] 表示从第 i 个区间开始,跳跃 2^j 个区间后能达到的最远区间的下一个索引
    vector<vector<int>> nxt(n + 1, vector<int>(MAX_LOG, n));

    // 贪心双指针预处理单步转移 nxt[i][0]
    int cur_right = 0;
    int max_r = 0, best_idx = n;
    for (int i = 0; i < n; ++i) {
        while (cur_right < n && segs[cur_right].l <= segs[i].r) {
            if (segs[cur_right].r > max_r) {
                max_r = segs[cur_right].r;
                best_idx = cur_right;
            }
            cur_right++;
        }
        // 致命踩坑点:若最远也无法延伸,必须指向自锁节点(如 n 或自身),防止越界或死循环
        nxt[i][0] = (max_r > segs[i].r) ? best_idx : n;
    }

    // 致命踩坑点:哨兵节点 n 的所有幂次跳跃必须全部指向自身,完成图论上的自环闭包
    for (int j = 0; j < MAX_LOG; ++j) {
        nxt[n][j] = n;
    }

    // 严格按照拓扑序,外层枚举幂次 j,内层枚举节点 i
    for (int j = 1; j < MAX_LOG; ++j) {
        for (int i = 0; i < n; ++i) {
            nxt[i][j] = nxt[nxt[i][j - 1]][j - 1];
        }
    }

    // 模拟一组查询:从区间 start 跨步,直到其右端点能覆盖 target_r 
    int start_node = 0;
    int target_r = m; 
    int ans = 1; // 计入初始区间
    int curr = start_node;

    for (int j = MAX_LOG - 1; j >= 0; --j) {
        // 致命踩坑点:从大到小尝试跳跃,只有当跳跃后的终点仍不能完全覆盖目标时才真正执行跳跃
        if (nxt[curr][j] != n && segs[nxt[curr][j]].r < target_r) {
            ans += (1 << j);
            curr = nxt[curr][j];
        }
    }

    // 最后补跳一步即可达成完全覆盖
    ans++; 
    cout << ans << "\n";
}

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    solve_binary_lifting();
    return 0;
}

NOIP 实战避坑指南

  1. 虚拟节点与自环未闭包导致的越界死循环 在倍增表的建立过程中,无法继续向下跳跃的边界(如到达树根、序列末尾)必须指向一个特定的虚拟节点(通常为 0N)。该虚拟节点在倍增表中的所有状态必须硬编码指向自身,即 nxt[virtual_node][j] = virtual_node。如果未做此闭包处理,虚拟节点将读取内存中的随机垃圾值,导致跳跃逻辑进入不可控的死循环,或直接触发 SIGSEGV 段错误。
  2. 从大到小拼凑跳跃时的贪心边界错误 在利用倍增表求最少跨步等精确值时,必须从大到小(如 MAX_LOG - 1 递减到 0)尝试跳跃。决策条件通常为 if (满足未达成条件) 则执行跳跃。选手常犯错误是将条件误写为 if (满足达成条件)。若直接向满足条件的方向跳跃,会由于倍增步长过大而导致“一步跨过头”,从而无法求得最优精确解。

经典 NOIP/洛谷 真题

洛谷 P1084 [NOIP2012 提高组] 疫情控制

洛谷 P4155 [SCOI2015] 国旗计划

原文链接: local://3.4

[h] 返回首页