【学习笔记】倍增

【学习笔记】倍增

倍增法,顾名思义就是翻倍。它能够使线性的处理转化为对数级的处理,大大地优化时间复杂度。

ST 表

RMQ 是 Range Maximum/Minimum Query 的缩写,表示区间最大(最小)值。
而 ST 表是用于解决可重复贡献问题的数据结构。
f(l,r)[l,r] 这个区间的答案,可重复贡献问题就是,对所有 RLf(l,r) 可以表示为 f(l,R)f(L,r) 的合并。(或者说,可以把大区间的答案拆成一些小区间的答案来计算,只要这些小区间的并是 [l,r] 即可)

除了 RMQ 以外,还有其他的“可重复贡献问题”。例如"区间按位和”、“区间按位或”、“区间 GCD”,ST 表都能高效地解决。
如果分析一下,“可重复贡献问题”一般都带有某种类似 RMQ 的影子。例如"区间按位与“就是每一个二进制位取最小值。而”区间 GCD”则是每一个质因数的指数取最小值。

解决 RMQ 问题,支持 O(nlogn) 预处理,O(1) 查询,但是不支持修改,所以拓展性较差。

以区间查询最大值为例:

st[i][j] 表示第 i 项在 [i,i+2j] 内的最大值。
虽然应该写成 sti,j,但是个人感觉括号表示更清晰啦~

  1. 显然,st[i][0] 等于 a[i](输入数据每一项的值)。

  2. 初始化 st 表。

    • 首先,2jn,所以 1jlog2n

    • 枚举第 i 项,右边界 i+2j1n(因为 i 下标从 1 开始,所以最后要 1),所以 in2j+1

    • 得到转移方程:st[i][j]=max(st[i][j1],st[i+2j1][j1])

      举个例子:st[1][2]=max(st[1][1],st[3][1])。想象第 1 项后面画 4 格的最大值就是第 1 项后面画 2 格的最大值与第 3 项后面画 2 格的最大值的最大值。

  3. 查询的时候要找到 [L,R] 中的两个重叠的子区间,返回这两个的最大值。

    • L 开始考虑,定义 k[L,L+2k] 中的 k(吐槽)

      应该满足 L+2k1R,所以 klog2(RL+1)。不妨取等,保证是所求区间内的最大子区间。

    • R 开始考虑,我们反向找一个与从 L 开始考虑相同的子区间,由于对称性,此时 k 相同。

      设这个区间的起点为 S,可得 S+2k1=R,所以 S=R2k+1

  4. 查询结果即为 max(st[L][k],st[R2k+1][k])

#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] 超级钢琴

因为求连续区间的和,自然想到前缀和预处理。

我们定义 sum() 为前缀和,可得 MAX(pos,l,r)=max{sum(t)sum(pos1)ltr},即以 pos 为左端点,右端点范围是 [l,r] 的最大子段。可以看出,pos 的位置是固定的。所以 sum(pos1) 也是固定的。所以我们要求这个的最大值,只要 sum(t) 最大就可以了。即要求 sum(t)[l,r] 中的最大值,那怎么快速地求出这个最大值呢?很显然,这就是 RMQ 的活。具体计算的时候还要考虑上界 r 是否超过了 n

接着相当于 pos 个单调数组里求前 k 小的问题。可用优先队列解决。

我们假设当前最大的三元组是 (pos,l,r)。最优解位置是 tans 累加这个三元组的贡献。由于 t 已经被选中,对于当前 post 已经不能重复选中,但最优解还可能存在于 t 左右的两端区间中,所以为了避免重复且不丧失其他较优解,我们仍然要把 (pos,l,t1),(pos,t+1,r) 扔回优先队列里面去。显然地,在放回去之前应该保证区间的存在,即应该满足 ltrt

最后实现的时候还要注意一点,一般问题里 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] 萌萌哒

平行并查集好题!

题目的限制条件是某些位置必须填一样的数字,先考虑最朴素的可行方法,对于区间 [l1,r1][l2,r2],我们一一把对应位置加入同一集合,即合并 l1+il2+i,其中 0ir1l1+1。同一集合内的所有位置,填的数字必须相同,故设 S 为集合数量,则 ans=910S1,这是由于每个集合可以填 09 共有 10 种选择,含最高位的集合不能选 0 所以只有 9 种选择。复杂度 O(n2logn)

接着考虑优化,优化的瓶颈在于合并的次数。可以在线段树上做并查集。但是此题不需要在线询问,所以可以使用 ST 表。

具体来讲:fa[i][j] 表示区间 [i,i+2j] 所在集合的最左侧的点。

  • 首先初始化所有的 fa[i][j]=i,其中 j[0,logn]

  • 每次合并时,假设输入的数为 l1,r1,l2,r2,如何保证 [l1,r1] 内的点与 [l2,r2] 内的点都有合并呢?

    类似 ST 表,我们可以找到 [l1,r1] 中的两个(最大的)重叠的子区间,最后再下放这些记号。这样我们最多每次只要合并 2 组区间

    k=log2(r1l1+1),需要合并的就是 fa[l1][k]fa[l2][k],以及 fa[r12k+1][k]fa[r22k+1][k]

  • 最后计算答案。将所有层的对应端点合并即可,做法是将每层和它的上一层合并。记点 i 在第 j 层(也就是 [i,i+2j])的所在集合的最左侧端点为 fai,j。所以只需要合并 fa[i][j1]fa[fai,j][j1],以及 fa[i+2j1][j1]fa[fai,j+2j1][j1]

复杂度为 O(nlog2n)

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;
}
posted @   FlyPancake  阅读(51)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
// music
点击右上角即可分享
微信分享提示