【学习笔记】倍增

【学习笔记】倍增

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

ST 表

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

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

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

以区间查询最大值为例:

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

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

  2. 初始化 st 表。

    • 首先,\(2^j \le n\),所以 \(1 \le j \le \log_2n\)

    • 枚举第 \(i\) 项,右边界 \(i+2^j-1 \le n\)(因为 \(i\) 下标从 1 开始,所以最后要 \(-1\)),所以 \(i \le n-2^j+1\)

    • 得到转移方程:\(st[i][j] = \max(st[i][j-1], st[i+2^{j-1}][j-1])\)

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

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

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

      应该满足 \(L+2^k-1 \le R\),所以 \(k \le \log_2(R-L+1)\)。不妨取等,保证是所求区间内的最大子区间。

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

      设这个区间的起点为 \(S\),可得 \(S+2^k-1 = R\),所以 \(S = R-2^k+1\)

  4. 查询结果即为 \(\max(st[L][k], st[R-2^k+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(pos-1) \mid l\leq t\leq r\}\),即以 \(pos\) 为左端点,右端点范围是 \([l,r]\) 的最大子段。可以看出,\(pos\) 的位置是固定的。所以 \(sum(pos-1)\) 也是固定的。所以我们要求这个的最大值,只要 \(sum(t)\) 最大就可以了。即要求 \(sum(t)\)\([l, r]\) 中的最大值,那怎么快速地求出这个最大值呢?很显然,这就是 RMQ 的活。具体计算的时候还要考虑上界 \(r\) 是否超过了 \(n\)

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

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

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

平行并查集好题!

题目的限制条件是某些位置必须填一样的数字,先考虑最朴素的可行方法,对于区间 \([l_1,r_1]\)\([l_2,r_2]\),我们一一把对应位置加入同一集合,即合并 \(l_1+i\)\(l_2+i\),其中 \(0 \leq i\leq r_1-l_1+1\)。同一集合内的所有位置,填的数字必须相同,故设 \(S\) 为集合数量,则 \(ans = 9 \cdot 10^{S-1}\),这是由于每个集合可以填 \(0 \sim 9\) 共有 10 种选择,含最高位的集合不能选 \(0\) 所以只有 9 种选择。复杂度 \(O(n^2\log n)\)

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

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

  • 首先初始化所有的 \(fa[i][j] = i\),其中 \(j \in [0, \log n]\)

  • 每次合并时,假设输入的数为 \(l_1, r_1, l_2, r_2\),如何保证 \([l_1, r_1]\) 内的点与 \([l_2, r_2]\) 内的点都有合并呢?

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

    \(k = \log_2(r_1-l_1+1)\),需要合并的就是 \(fa[l_1][k]\)\(fa[l_2][k]\),以及 \(fa[r_1-2^k+1][k]\)\(fa[r_2-2^k+1][k]\)

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

复杂度为 \(O(n\log^2n)\)

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 @ 2024-07-25 13:52  FlyPancake  阅读(113)  评论(0)    收藏  举报
// music