【学习笔记】倍增
【学习笔记】倍增
倍增法,顾名思义就是翻倍。它能够使线性的处理转化为对数级的处理,大大地优化时间复杂度。
ST 表
RMQ 是 Range Maximum/Minimum Query 的缩写,表示区间最大(最小)值。
而 ST 表是用于解决可重复贡献问题的数据结构。
记
除了 RMQ 以外,还有其他的“可重复贡献问题”。例如"区间按位和”、“区间按位或”、“区间 GCD”,ST 表都能高效地解决。
如果分析一下,“可重复贡献问题”一般都带有某种类似 RMQ 的影子。例如"区间按位与“就是每一个二进制位取最小值。而”区间 GCD”则是每一个质因数的指数取最小值。
解决 RMQ 问题,支持
以区间查询最大值为例:
虽然应该写成
-
显然,
等于 (输入数据每一项的值)。 -
初始化 st 表。
-
首先,
,所以 。 -
枚举第
项,右边界 (因为 下标从 1 开始,所以最后要 ),所以 。 -
得到转移方程:
。举个例子:
。想象第 项后面画 4 格的最大值就是第 项后面画 2 格的最大值与第 项后面画 2 格的最大值的最大值。
-
-
查询的时候要找到
中的两个重叠的子区间,返回这两个的最大值。-
从
开始考虑,定义 为 中的 。(吐槽)应该满足
,所以 。不妨取等,保证是所求区间内的最大子区间。 -
从
开始考虑,我们反向找一个与从 开始考虑相同的子区间,由于对称性,此时 相同。设这个区间的起点为
,可得 ,所以 。
-
-
查询结果即为
。
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 1e5+5;
ll a[N];
ll st[N][17];
int main(){
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
int n, m; cin>>n>>m;
for(int i=1; i<=n; i++){
cin>>a[i];
st[i][0] = a[i];
}
for(int j=1; j<=__lg(n); j++){
for(int i=1; i<=n-(1<<j)+1; i++){
st[i][j] = max(st[i][j-1], st[i+(1<<(j-1))][j-1]);
}
}
while(m--){
int l, r; cin>>l>>r;
int k = __lg(r-l+1);
cout<<max(st[l][k], st[r-(1<<k)+1][k])<<"\n";
}
return 0;
}
P2048 [NOI2010] 超级钢琴
因为求连续区间的和,自然想到前缀和预处理。
我们定义
接着相当于
我们假设当前最大的三元组是
最后实现的时候还要注意一点,一般问题里 RMQ 数组里面记录的是最优解的值,但此题查询区间最大值的时候查询的是最优解的位置。
Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 5e5+5;
ll sum[N];
ll st[N][20];
int n, k, L, R;
void initst(){
for(int i=1; i<=n; i++)
st[i][0] = i;
for(int j=1; j<=__lg(n); j++){
for(int i=1; i<=n-(1<<j)+1; i++){
int x = st[i][j-1], y = st[i+(1<<(j-1))][j-1];
st[i][j] = sum[x]>sum[y] ? x : y;
}
}
}
int query(int l, int r){
int k = __lg(r-l+1);
int x = st[l][k], y = st[r-(1<<k)+1][k];
return sum[x]>sum[y] ? x : y;
}
struct node{
int pos, l, r, t;
node(int pos, int l, int r) : pos(pos), l(l), r(r), t(query(l, r)){}
friend bool operator <(node a, node b){
return sum[a.t]-sum[a.pos-1] < sum[b.t]-sum[b.pos-1];
}
};
priority_queue<node> Q;
int main(){
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin>>n>>k>>L>>R;
for(int i=1; i<=n; i++){
cin>>sum[i];
sum[i] += sum[i-1];
}
initst();
for(int i=1; i<=n; i++){
if(i+L-1 <= n)
Q.push({i, i+L-1, min(n, i+R-1)});
}
ll ans = 0;
for(int i=1; i<=k; i++){
node p = Q.top(); Q.pop();
ans += sum[p.t]-sum[p.pos-1];
if(p.l < p.t) Q.push({p.pos, p.l, p.t-1});
if(p.r > p.t) Q.push({p.pos, p.t+1, p.r});
}
cout<<ans;
return 0;
}
P3295 [SCOI2016] 萌萌哒
平行并查集好题!
题目的限制条件是某些位置必须填一样的数字,先考虑最朴素的可行方法,对于区间
接着考虑优化,优化的瓶颈在于合并的次数。可以在线段树上做并查集。但是此题不需要在线询问,所以可以使用 ST 表。
具体来讲:
-
首先初始化所有的
,其中 。 -
每次合并时,假设输入的数为
,如何保证 内的点与 内的点都有合并呢?类似 ST 表,我们可以找到
中的两个(最大的)重叠的子区间,最后再下放这些记号。这样我们最多每次只要合并 2 组区间。记
,需要合并的就是 和 ,以及 和 。 -
最后计算答案。将所有层的对应端点合并即可,做法是将每层和它的上一层合并。记点
在第 层(也就是 )的所在集合的最左侧端点为 。所以只需要合并 和 ,以及 和 。
复杂度为
Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 1e5+5;
const int MOD = 1e9+7;
int fa[N][17];
int find(int x, int k){
if(fa[x][k] == x) return x;
return fa[x][k] = find(fa[x][k], k);
}
void merge(int x, int y, int k){
int fx = find(x, k), fy = find(y, k);
if(fx != fy) fa[fx][k] = fy;
}
int main(){
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
int n, m; cin>>n>>m;
for(int i=1; i<=n; i++){
for(int j=0; j<=__lg(n); j++){
fa[i][j] = i;
}
}
for(int i=1; i<=m; i++){
int l1, r1, l2, r2; cin>>l1>>r1>>l2>>r2;
int j = __lg(r1-l1+1);
merge(l1, l2, j);
l1 = r1-(1<<j)+1, l2 = r2-(1<<j)+1;
merge(l1, l2, j);
}
for(int j=__lg(n); j>=1; j--){
for(int i=1; i<=n-(1<<j)+1; i++){
int fi = find(i, j);
merge(i, fi, j-1);
merge(i+(1<<(j-1)), fi+(1<<(j-1)), j-1);
}
}
ll ans = 0;
for(int i=1; i<=n; i++){
if(fa[i][0] == i)
ans = !ans ? 9 : ans*10ll % MOD;
}
cout<<ans;
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】