【学习笔记】势能线段树——线段树的小套路,让暴力显得更加优美

一些同类型的套路

线段树维护的区间和要具有可加性,并且对于区间的修改需要打lazy标记或者标记永久化。

然而,一些操作,比如对 \(l\)\(r\) 区间内的每个数开方、取模等不具备可整体修改性质的操作就无从下手了,并且(至少是我知道的)别的数据结构们也维护不了这些操作。

但是,这些仍然可以用线段树来维护,只是需要一点点的小变动。

P4145 上帝造题的七分钟 2 / 花神游历各国

题意概述:

维护一个序列,支持区间开方与求和操作。

解析:

区间每个数都开方的话,显然无法在sum上直接操作,然而暴力的单点修改显然会超时,而 lazy标记显然也无法胜任。

本题的精髓在于 \(\sqrt{1} = 1\)
最大的数据是 \(1e12\) 的,向下取整后,最多进行 \(6\)sqrt操作后就能到达 \(1\)

所以,我们可以记录下当前区间的最大值,如果该区间的最大值 \(≤ 1\) 了,就可以不用管它了,否则,我们对每个区间都直接递归到底。

因为最多进行六次修改就到 \(1\) 了,所以均摊时间复杂度还是 \(O(\log{n})\) 的,总复杂度是 \(O(n\log{n})\) 的,可过。

Code

#include<cmath>
#include<cstdio>
#include<algorithm>

#define LL long long

using namespace std;

const int MAXN = 1e5 + 10;
int n, m;
LL num[MAXN];

struct Segment_Tree{
    struct Tree{
        int l, r;
        LL max;
        LL sum;
    }tr[MAXN << 2];

    inline int lson(int rt){
        return rt << 1;
    }

    inline int rson(int rt){
        return rt << 1 | 1;
    }

    inline void Pushup(int rt){
        tr[rt].sum = tr[lson(rt)].sum + tr[rson(rt)].sum;
        tr[rt].max = max(tr[lson(rt)].max, tr[rson(rt)].max);
    }

    void Build(int rt, int l, int r){
        tr[rt].l = l;
        tr[rt].r = r;

        if(l == r){
            tr[rt].sum = num[l];
            tr[rt].max = num[l];
            return;
        }

        int mid = (l + r) >> 1;
        Build(lson(rt), l, mid);
        Build(rson(rt), mid + 1, r);

        Pushup(rt);
    }

    void Update(int rt, int l, int r){
        if(tr[rt].l == tr[rt].r){
            tr[rt].sum = sqrt(tr[rt].sum);
            tr[rt].max = sqrt(tr[rt].max);
            return;
        }

        int mid = (tr[rt].l + tr[rt].r) >> 1;
        if(l <= mid && tr[lson(rt)].max > 1)
            Update(lson(rt), l, r);
        if(r > mid && tr[rson(rt)].max >1)
            Update(rson(rt), l, r);

        Pushup(rt); 
    }

    LL Query_Sum(int rt, int l, int r){
        if(tr[rt].l >= l && tr[rt].r <= r)
            return tr[rt].sum;

        int mid = (tr[rt].l + tr[rt].r) >> 1;
        if(r <= mid) return Query_Sum(lson(rt), l, r);
        else if(l > mid) return Query_Sum(rson(rt), l, r);
        else return Query_Sum(lson(rt), l, r) + Query_Sum(rson(rt), l, r);
    }
}S;

inline LL read(){
    LL x = 0, f = 1;
    char c = getchar();

    while(c < '0' || c > '9'){
        if(c == '-') f = -1;
        c = getchar();
    }
    while(c >= '0' && c <= '9'){
        x = (x << 1) + (x << 3) + (c ^ 48);
        c = getchar();
    }

    return x * f;
}

int main(){
    n = read();
    for(register int i = 1; i <= n; i++)
        num[i] = read();

    S.Build(1, 1, n);

    m = read();
    for(register int i = 1; i <= m; i++){
        int opt, l, r;
        opt = read(), l = read(), r = read();

        if(l > r) swap(l, r);

        if(opt == 0) S.Update(1, l, r);
        else printf("%lld\n", S.Query_Sum(1, l, r));
    }

    return 0;
}

CF438D The Child and Sequence

题意概述:

维护一个序列,支持区间的取模、求和和单点修改。

解析:

同样的无法用lazy标记维护。

思路大致是一样的,同样维护区间的最大值,比较区间最大值是否大于等于模数,如果大于等于,进行递归,直到叶子节点。

均摊时间复杂度仍然是 \(O(\log{n})\) 的,可过。

Code

#include<cstdio>
#include<algorithm>

using namespace std;

const int MAXN = 1e5 + 10;
int n, m;
int num[MAXN];

struct Segment_Tree{
    struct Tree{
        int l, r;
        int max;
        long long sum;
    }tr[MAXN << 2];

    inline int lson(int rt){
        return rt << 1;
    }

    inline int rson(int rt){
        return rt << 1 | 1;
    }

    inline void Pushup(int rt){
        tr[rt].sum = tr[lson(rt)].sum + tr[rson(rt)].sum;
        tr[rt].max = max(tr[lson(rt)].max, tr[rson(rt)].max);
    }

    void Build(int rt, int l, int r){
        tr[rt].l = l;
        tr[rt].r = r;

        if(l == r){
            tr[rt].sum = tr[rt].max = num[l];
            return;
        }

        int mid = (l + r) >> 1;
        Build(lson(rt), l, mid);
        Build(rson(rt), mid + 1, r);

        Pushup(rt);
    }

    void Update_Mod(int rt, int l, int r, int data){
        if(tr[rt].l == tr[rt].r){
            tr[rt].sum %= data;
            tr[rt].max %= data;
            return;
        }

        int mid = (tr[rt].l + tr[rt].r) >> 1;
        if(l <= mid && tr[lson(rt)].max >= data) Update_Mod(lson(rt), l, r, data);
        if(r > mid && tr[rson(rt)].max >= data) Update_Mod(rson(rt), l, r, data);

        Pushup(rt);
    }

    void Update_Point(int rt, int pos, int data){
        if(tr[rt].l == tr[rt].r){
            tr[rt].sum = data;
            tr[rt].max = data;
            return;
        }

        int mid = (tr[rt].l + tr[rt].r) >> 1;
        if(pos <= mid) Update_Point(lson(rt), pos, data);
        else Update_Point(rson(rt), pos, data);

        Pushup(rt);
    }

    long long Query_Sum(int rt, int l, int r){
        if(tr[rt].l >= l && tr[rt].r <= r)
            return tr[rt].sum;

        long long ans = 0;
        int mid = (tr[rt].l + tr[rt].r) >> 1;
        if(l <= mid) ans += Query_Sum(lson(rt), l, r);
        if(r > mid) ans += Query_Sum(rson(rt), l, r);

        return ans;
    }
}S;

inline int read(){
    int x = 0, f = 1;
    char c = getchar();

    while(c < '0' || c > '9'){
        if(c == '-') f = -1;
        c = getchar();
    }
    while(c >= '0' && c <= '9'){
        x = (x << 1) + (x << 3) + (c ^ 48);
        c = getchar();
    }

    return x * f;
}

int main(){
    n = read(), m = read();
    for(register int i = 1; i <= n; i++)
        num[i] = read();

    S.Build(1, 1, n);

    for(register int i = 1; i <= m; i++){
        int opt;
        opt = read();

        if(opt == 1){
            int l, r;
            l = read(), r = read();
            printf("%lld\n", S.Query_Sum(1, l, r));
        }
        else if(opt == 2){
            int l, r, x;
            l = read(), r = read(), x = read();
            S.Update_Mod(1, l, r, x);
        }
        else{
            int k, x;
            k = read(), x = read();
            S.Update_Point(1, k, x);
        }
    }

    return 0;
}

CF920F SUM and REPLACE

题意概述:

维护一个序列,支持把区间内的每一个数 \(i\),都变成 \(\tau(i)\) 和求和。(\(\tau(i)\) 表示 \(i\) 的正约数个数)。

解析:

看到这,聪明的你应该已经明白套路了吧。

先考虑怎么求出 \(\tau(i)\)

有唯一分解定理:\(p = {p_{1}}^{k_{1}} \times {p_{2}}^{k_{2}} \times ... \times {p_{n}}^{k_{n}}\)
\(\tau(p) = \prod_{i = 1}^{n}(k_{i} + 1)\)

但每次这样求是 \(O(\sqrt{n})\) 的,会超时。
考虑线性筛,每个数有三种情况:

  1. \(i\) 是素数:
    \(i\) 只能被 \(1\) 和它本身整除,所以:$$\tau(i) = 2$$
  2. \(i\) 与一个不能整除它的素数相乘(设素数为 \(p\)):
    \(i\)\(p\)\(1\) 外没有相同的正约数,所以 \(i\) 的每个正约数和 \(p\) 的每个正约数分别相乘,得到的都是 \(i \times p\) 的正约数,且两两不同,所以:$$\tau(i) \times \tau(p)$$ 即: $$\tau(i \times p) = \tau(i) \times 2$$
  3. \(i\) 与一个能整除它的素数相乘(设素数为 \(p\)):
    \(i\)\(p\) 有除 \(1\) 外的正约数,则 \(\tau(i) \times \tau(p)\) 时,\(i / p\) 的正约数在乘以 \(p\) 后都会变成 \(i\) 的正约数。
    \(i\) 的正约数与 \(p\)\(1\)(他们是 \(p\) 仅有的正约数)相乘后,相当于每个 \(i / p\) 的正约数乘以 \(p\) 的积都出现了两遍,所以:$$\tau(i \times p) = \tau(i) \times 2 - \tau(i / p)$$

特别的 \(\tau(0) = 0, \tau(1) = 1\)

之后,我们发现,最大的 \(\tau(i) \approx \frac{i}{2}\),所以仍然能保证复杂度是 \(O(\log n)\) 的。

按照原套路,记录区间的最大值是否 \(≥ 1\) 来判断需不需要递归到叶子节点即可。

Code

#include<cstdio>
#include<algorithm>

using namespace std;

const int MAXN = 3e5 + 10, MAXM = 3e6 + 10;
int n, m, sum, max_a;
int num[MAXN];
int prime[MAXM], v[MAXM], d[MAXM];

inline int read(){
    int x = 0, f = 1;
    char c = getchar();

    while(c < '0' || c > '9'){
        if(c == '-') f = -1;
        c = getchar();
    }
    while(c >= '0' && c <= '9'){
        x = (x << 1) + (x << 3) + (c ^ 48);
        c = getchar();
    }

    return x * f;
}

void Euler(int n){
    sum = 0;
    d[1] = 1;

    for(register int i = 2; i <= n; i++){
        if(!v[i]){
            d[i] = 2;
            v[i] = i;
            prime[++sum] = i;
        }

        for(register int j = 1; j <= sum; j++){
            if(prime[j] > v[i] || prime[j] * i > n)
                break;
            v[i * prime[j]] = prime[j];
            if(i % prime[j]) d[i * prime[j]] = d[i] * 2; //等同于 d[i] * d[prime[j]]
            else d[i * prime[j]] = d[i] * 2 - d[i / prime[j]];
        }
    }
}

struct Segment_Tree{
    struct Tree{
        int l, r;
        int max;
        long long sum;
    }tr[MAXN << 2];

    inline int lson(int rt){
        return rt << 1;
    }

    inline int rson(int rt){
        return rt << 1 | 1;
    }

    inline void Pushup(int rt){
        tr[rt].sum = tr[lson(rt)].sum + tr[rson(rt)].sum;
        tr[rt].max = max(tr[lson(rt)].max, tr[rson(rt)].max);
    }

    void Build(int rt, int l, int r){
        tr[rt].l = l;
        tr[rt].r = r;

        if(l == r){
            tr[rt].sum = tr[rt].max = num[l];
            return;
        }

        int mid = (l + r) >> 1;
        Build(lson(rt), l, mid);
        Build(rson(rt), mid + 1, r);

        Pushup(rt);
    }

    void Update(int rt, int l, int r){
        if(tr[rt].l == tr[rt].r){
            tr[rt].sum = d[tr[rt].sum];
            tr[rt].max = d[tr[rt].max];
            return;
        }

        int mid = (tr[rt].l + tr[rt].r) >> 1;
        if(l <= mid && tr[lson(rt)].max > 2) Update(lson(rt), l, r);
        if(r > mid && tr[rson(rt)].max > 2) Update(rson(rt), l, r);

        Pushup(rt); 
    }

    long long Query_Sum(int rt, int l, int r){
        if(tr[rt].l >= l && tr[rt].r <= r)
            return tr[rt].sum;

        long long ans = 0;
        int mid = (tr[rt].l + tr[rt].r) >> 1;
        if(l <= mid) ans += Query_Sum(lson(rt), l, r);
        if(r > mid) ans += Query_Sum(rson(rt), l, r);

        return ans;
    }
}S;

int main(){
    n = read(), m = read();
    for(register int i = 1; i <= n; i++){
        num[i] = read();
        max_a = max(max_a, num[i]);
    }

    S.Build(1, 1, n);
    Euler(max_a);

    for(register int i = 1; i <= m; i++){
        int opt;
        opt = read();

        if(opt == 1){
            int l, r;
            l = read(), r = read();
            S.Update(1, l, r);
        }
        else{
            int l, r;
            l = read(), r = read();
            printf("%lld\n", S.Query_Sum(1, l, r));
        }
    }

    return 0;
}
posted @ 2022-09-06 11:21  TSTYFST  阅读(65)  评论(0编辑  收藏  举报