倍增法

倍增法与二分法是“相反”的算法,二分法是每次缩小一半,从而以 O(logn) 的速度快速缩小定位到解;倍增法是每次扩大一倍,从而以 O(2n) 的速度快速地扩展到解空间。

倍增就是“成倍增长”,很多时候倍增的实现利用的是二进制本身的倍增特性。把一个数 n 用二进制展开,则 n=a0×20+a1×21+a2×22+a3×23+a4×24+。例如 35,它的二进制是 100011,第 5 位、第 1 位和第 0 位为 1,即 a5=a1=a0=1,把这几位的权值相加,得到 35=25+21+20=32+2+1

二进制划分反映了一种快速增长的特性,第 i 位的权值 2i 等于前面所有权值的和加 1,即 2i=2i1+2i2++21+20+1,一个整数 n,它的二进制只有 logn 位。如果要从 0 增长到 n,可以以 1,2,4,,2k 为“跳板”,快速跳到 n,这些跳板只有 k=logn 个。

倍增法的局限性是需要提前计算出第 1,2,4,,2k 个跳板,这要求数据是静态不变的,不是动态变化的。如果数据发生了变化,那么所有跳板需要重新计算,跳板就失去了意义。

例题:P4155 [SCOI2015] 国旗计划

边境上有 m 个边防站围成一圈,顺时针编号为 1m。有 n 名战士,每名战士常驻两个站 cidi,能在两个站之间移动。局长有一个“国旗计划”,让边防战士举着国旗环绕一圈。局长想知道至少需要多少战士才能完成“国旗计划”,并且他想知道在某个战士必须参加的情况下,至少需要多少名边防战士。
数据范围:n2×105,m<109,1ci,dim

题目的要求很清晰:计算能覆盖整个圆圈的最少区间(战士)。

题目给定的所有区间互相不包含,那么按区间的左端点排序后,区间的右端点也是也是单调递增的。这种情况下能用贪心法选择区间。

image

定义 gos,i 表示从第 s 个区间出发,走 2i最优区间后到达的区间。例如,gos,4 是从 s 出发到达的第 24=16 个最优的区间,sgos,4 之间的区间也都是最优的。

预计算出从所有的区间出发的 go,以它们为“跳板”,就能快速跳到目的地。

跳的时候先用大数再用小数。以从 s 跳到后面第 27 个区间为例:

  1. s16 步,到达 s 后的第 16 个区间 f1
  2. f18 步,到达 f1 后的第 8 个区间 f2
  3. f22 步到达 f3
  4. f31 步到达终点 f4

image

时间复杂度是多少?查询一次,用倍增法从 s 跳到终点的时间复杂度为 O(logn)。共有 n 次查询,总时间复杂度为 O(nlogn)

剩下的问题是如何快速预计算出 go。有非常巧妙的递推关系:gos,i=gogos,i1,i1。可以这样理解:

  1. gos,i1。从 s 起跳,先跳 2i1 步到区间 z=gos,i1
  2. goz,i1。再从 z2i1 步。

一共跳了 2i1=2i1=2i 步,这样就实现了从 s 起跳,跳到 s 的第 2i 个区间。

特别地,gos,0s 后第 20=1 个区间(用贪心法算出的下一个最优区间),gos,0 是递推式的初始条件,从它递推出了其他的所有值。递推的计算量有多大?从任意 s 到末尾,最多有个 logngos,,所以只需要递推 O(logn) 次。计算 n 个节点的 go,共计算 O(nlogn) 次。

以上所有计算,包括预计算 gon 次查询,总时间复杂度O(nlogn)

#include <cstdio>
#include <algorithm>
using namespace std;
typedef long long LL;
const int MAXN = 4e5 + 5;
struct Interval {
LL c, d; // c和d为战士的左右区间
int id; // id为战士的编号
bool operator<(const Interval& other) const {
return c < other.c;
}
};
Interval a[MAXN];
int go[MAXN][20], n, m, ans[MAXN];
void init() { // 贪心+预计算倍增
int nxt = 1;
for (int i = 1; i <= n; i++) { // 用贪心求每个区间的下一个最优区间
// 最优区间是有交集且右端点最大的区间
while (nxt <= n && a[nxt].c <= a[i].d) nxt++;
go[i][0] = nxt - 1; // 区间i的下一个区间
}
for (int i = 1; (1 << i) < n / 2; i++) // 倍增:i=1,2,4,8,...,共logn次
for (int s = 1; s <= n; s++) // 每个区间后第2的i次方个区间
go[s][i] = go[go[s][i - 1]][i - 1];
}
int calc(int x) { // 从第x个战士出发
int ret = 1, dest = a[x].c + m;
for (int i = 19; i >= 0; i--) { // 从最大的i开始找
int nxt = go[x][i];
if (nxt && a[nxt].d < dest) {
x = nxt; // 跳到新位置
ret += 1 << i; // 累加跳过的区间个数
}
}
return ret + 1;
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) {
scanf("%lld%lld", &a[i].c, &a[i].d);
a[i].id = i; // 记录战士的顺序
if (a[i].c > a[i].d) a[i].d += m; // 把环变成链
}
sort(a + 1, a + n + 1); // 按左端点排序
for (int i = 1; i <= n; i++) { // 拆环加倍成一条链
a[n + i].c = a[i].c + m;
a[n + i].d = a[i].d + m;
}
n *= 2;
init();
for (int i = 1; i <= n / 2; i++) ans[a[i].id] = calc(i); // 逐个计算每个战士
for (int i = 1; i <= n / 2; i++) printf("%d%c", ans[i], i * 2 == n ? '\n' : ' ');
return 0;
}

习题:P8251 [NOI Online 2022 提高组] 丹钓战

解题思路

先按题意模拟整个序列的入栈出栈过程。当由于一个新元素想要入栈而导致某元素被弹出时,则说明如果被弹出元素是入栈序列中的第一个元素,则它的下一个“成功的”元素是这个想要入栈的元素。因此可以预处理每一个元素的下一个“成功的”元素是谁以及相应的倍增跳跃表。对于每一个查询序列,利用倍增表就可以快速计算整个序列中“成功的”元素的数量。

参考代码
#include <cstdio>
#include <stack>
using std::stack;
const int N = 5e5 + 5;
const int LOG = 19;
int a[N], b[N], go[N][LOG];
int main()
{
int n, q; scanf("%d%d", &n, &q);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
for (int i = 1; i <= n; i++) scanf("%d", &b[i]);
stack<int> s;
for (int i = 1; i <= n; i++) {
while (!s.empty()) {
int pre = s.top();
int ta = a[pre], tb = b[pre];
if (a[i] != ta && b[i] < tb) break;
go[pre][0] = i; // 记录每个元素因为谁导致要弹出
s.pop();
}
s.push(i);
}
for (int i = 1; i < LOG; i++)
for (int j = 1; j <= n; j++)
go[j][i] = go[go[j][i - 1]][i - 1]; // 预处理倍增表
while (q--) {
int l, r; scanf("%d%d", &l, &r);
int ans = 0;
int cur = l;
for (int i = LOG - 1; i >= 0; i--)
if (go[cur][i] != 0 && go[cur][i] <= r) { // 向右跳但没有越过边界
// 跳了2的i次方步
ans += 1 << i; cur = go[cur][i];
}
printf("%d\n", ans + 1); // 要算上起点本身
}
return 0;
}
posted @   RonChen  阅读(165)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?
点击右上角即可分享
微信分享提示