线段树进阶

线段树进阶

永久化标记

永久化标记的思想是每个节点记录一个懒标记,不下传懒标记,而是在查询的时候将路径上的懒标记合并到答案上来。

P5057

struct Node
{
    int rev;
} t[400005];

int n, m;

void update(int i, int l, int r, int a, int b)
{
    if (b <= l || a >= r) return;

    if (l >= a && r <= b)
    {
        t[i].rev = 1 - t[i].rev;
    }
    else
    {
        int mid = (l + r) >> 1;
        update(LT(i), l, mid, a, b);
        update(RT(i), mid, r, a, b);
    }
}

int query(int i, int l, int r, int x, int op)
{
    if (t[i].rev) op = 1 - op;
    if (l == r - 1)
    {
        return op;
    }
    else
    {
        int mid = (l + r) >> 1;
        if (x < mid)
        {
            return query(LT(i), l, mid, x, op);
        }
        else
        {
            return query(RT(i), mid, r, x, op);
        }
    }
}

int main()
{
    FR;
    scanf("%d %d", &n, &m);

    rep(i, 1, m)
    {
        int op;
        scanf("%d", &op);
        if (op == 1)
        {
            int l, r;
            scanf("%d %d", &l, &r);
            update(1, 1, n + 1, l, r + 1);
        }
        else
        {
            int k;
            scanf("%d", &k);
            printf("%d\n", query(1, 1, n + 1, k, 0));
        }
    }
    return 0;
}

权值线段树

权值线段树是一种基于线段树对值域区间进行操作的一种线段树,其可以向其值域区间内插入数字,然后统计值域区间内的信息。

UVA12983

int n, m;
ll arr[1005];

ll discre[1005];

ll dim[1005][1005];

void add(ll t[], int x, ll val)
{
    while (x < 30005)
    {
        t[x] += val;
        x += lowbit(x);
    }
}

ll query(ll t[], int x)
{
    ll ans = 0;
    while (x >= 1)
    {
        ans += t[x];
        x -= lowbit(x);
    }
    return ans;
}

void solve(int id)
{
    memset(dim, 0, sizeof(dim));
    scanf("%d %d", &n, &m);
    rep(i, 1, n)
    {
        scanf("%lld", arr + i);
        discre[i] = arr[i];
    }
    sort(discre + 1, discre + 1 + n);
    int ed = unique(discre + 1, discre + 1 + n) - discre;
    unordered_map<ll, int> up;
    reb(i, 1, ed)
    {
        up[discre[i]] = i;
    }

    rep(i, 1, n)
    {
        arr[i] = up[arr[i]];
    }
    ll ans = 0;
    rep(i, 1, n)
    {
        ans += query(dim[m - 1], arr[i] - 1);
        per(i, m - 1, 2)
        {
            add(dim[i], arr[i], query(dim[i - 1], arr[i] - 1));
        }
        add(dim[1], arr[i], 1);
    }

    printf("Case #%d: %lld\n", id, ans);
}

int main()
{
    FR;
    int T;
    scanf("%d", &T);
    rep(i, 1, T)
    {
        solve(i);
    }
    return 0;
}

除此之外,权值线段在离散化之后可以轻松维护中位数,插入,删除,查询的诗句复杂度都是 O ( log ⁡ n ) O(\log n) O(logn)的。

ABC 218G

我们要求出每个叶子节点的中位数,可以使用对顶堆或权值线段树,其中对顶堆不好处理删除元素,因此我们采用权值线段树维护。

计算完之后,就可以在树上进行博弈论DP。

核心代码为获取第K大数:

int getKth(int i, int l, int r, int k)
{
    if (l == r - 1)
    {
        return l;
    }
    else
    {
        int mid = HF(l + r);
        if (nodes[LT(i)] >= k)
        {
            return getKth(LT(i), l, mid, k);
        }
        else
        {
            return getKth(RT(i), mid, r, k - nodes[LT(i)]);
        }
    }
}

zkw线段树

zkw线段树由清华大学张昆玮(zkw)发明,相较于普通线段树,其优点有:

  • 非递归,时间常数小
  • 使用二进制思想,代码短,方便书写

建树

我们强制要求叶子节点(即区间大小为1的节点)排列在最后一行,并且还是一颗满二叉树(多余的节点不用)。

这样我们就可以写出建树的代码,此时只更新了叶子节点的信息,我们没有更新非叶子节点的信息。

zkw线段树

void build(int n)
{
    // 寻找最后一行的大小,也是非叶子节点的大小
    for (M = 1; M < n + 2; M <<= 1)
        ;

    // 依次填充叶子节点
    for (int i = 1; i <= n; i++)
        tree[i + M] = arr[i];
}

下面是更新非叶子节点的信息:

  • 区间和:
for (int i = M - 1; i; --i) tree[i] = tree[i << 1] + tree[i << 1 | 1];

完整的建树代码:

void build(int n)
{
    // 寻找最后一行的大小,也是非叶子节点的大小
    for (M = 1; M < n + 2; M <<= 1)
        ;

    // 依次填充叶子节点
    for (int i = 1; i <= n; i++)
        tree[i + M] = arr[i];
        
    // 更新非叶子节点信息,以区间和为例
	for (int i = M - 1; i; --i) tree[i] = tree[i << 1] + tree[i << 1 | 1];
}

单点更新

我们只需要更新叶子节点,以及向上更新父节点即可。

void update(int x, int val)
{
    // 更新叶子节点
    tree[M + x] = val;

    // 向上更新父节点
    for (int i = (M + x) >> 1; i; i >>= 1)
    {
        tree[i] = tree[i << 1] + tree[i << 1 | 1];
    }
}

单点查询

直接返回即可。

int get(int x)
{
    return tree[x + M];
}

区间查询

区间和查询的思想是逐步向上进行求和,使用双指针的方法,如果指针l或者r在某一个节点上,表示该节点管理的区间已经求和完毕,那么我们应该考虑他的兄弟节点。

停止的条件是l^r^1为0时停止,该断言为0,表示l^r为1,表示l和r互为兄弟节点,此时求和完毕。

此时我们考虑兄弟节点是否求和,如果l是左节点,说明l的右节点一定包含在区间内,应该求和,如果r是右节点,那么兄弟节点也在区间内,也应该求和,其他情况则不求和。

int querySum(int l, int r)
{
    int ans = 0;
    for (l += M - 1, R += M + 1; l ^ r ^ 1; l >>= 1, r >>= 1)
    {
        if (~l & 1)
            ans += tree[l ^ 1];
        if (r & 1)
            ans += tree[r ^ 1];
    }
    return ans;
}

此时,zkw线段树的基础应用结束,下面讲讲更复杂的区间操作。

自底向下的标记

下面我们模仿普通线段树,实现一个带有自底向下的标记的zkw线段树,我们需要通过上传标记实现。

需要根据大小计算懒标记的数值,并向上传递。


ll querySum(int l, int r)
{
    ll ans = 0;
    int LN = 0;
    int RN = 0;
    int NN = 1;
    for (l += M - 1, r += M + 1; l ^ r ^ 1; l >>= 1, r >>= 1, NN <<= 1)
    {
        if (~l & 1)
        {
            ans += tree[l ^ 1] + lazy[l ^ 1] * NN;
            LN += NN;
        }

        if (r & 1)
        {
            ans += tree[r ^ 1] + lazy[r ^ 1] * NN;
            RN += NN;
        }

        ans += lazy[l >> 1] * LN + lazy[r >> 1] * RN;
    }

    NN = LN + RN;

    for (l >>= 1; l; l >>= 1)
    {
        ans += lazy[l] * NN;
    }
    return ans;
}

void add(int l, int r, ll val)
{
    int NN = 1;
    ll LS = 0;
    ll RS = 0;
    for (l += M - 1, r += M + 1; l ^ r ^ 1; l >>= 1, r >>= 1, NN <<= 1)
    {
        if (~l & 1)
        {
            lazy[l ^ 1] += val;
            LS += val * NN;
        }

        if (r & 1)
        {
            lazy[r ^ 1] += val;
            RS += val * NN;
        }

        tree[l >> 1] += LS;
        tree[r >> 1] += RS;
    }

    LS += RS;
    for (l >>= 1; l; l >>= 1)
    {
        tree[l] += LS;
    }
}

变换求和次序

懒标记一般并不好理解且极易容易出Bug,我们通过仅仅访问前缀和的方式实现区间加法和区间查询。

我们设 b i b_i bi a i a_i ai的差分数组,如果我们要求 a i a_i ai的某一段前缀和,那么:

该公式的核心是将横向求和变换为纵向求和。

∑ i = 1 r a i = ∑ i = 1 r ∑ j = 1 i b j = ∑ i = 1 r b i × ( r − i + 1 ) = ( r + 1 ) ∑ i = 1 r b i − ∑ i = 1 r b i × i \begin{aligned} &\sum_{i=1}^{r} a_i\\=&\sum_{i=1}^r\sum_{j=1}^i b_j\\=&\sum_{i=1}^r b_i\times(r-i+1) \\=& (r+1) \sum_{i=1}^r b_i - \sum_{i=1}^r b_i\times i \end{aligned} ===i=1raii=1rj=1ibji=1rbi×(ri+1)(r+1)i=1rbii=1rbi×i

故我们需要两个结构来维护 b i b_i bi b i × i b_i \times i bi×i的前缀和。

我们可以将上述querySum方法的l设置为1,就是前缀和了。

这样我们区间加和区间求和就转换为单点修改和前缀和查询了,这一点和树状数组的思想是一致的。

可持久化标记与RMQ

接下来我们通过可持久化标记实现RMQ-zkw线段树。(以区间最大值为例)

zkw线段树是自底向上实现的线段树,那么普通线段树懒标记的方式就行不通了,我们必须找到一个自底向上的方式实现,即树上差分

现在每一个节点不再保存值,而是保存当前节点的值减去父节点的值的差

这种方法叫做可持久化标记。

那么我们的建树方式也应该做出调整,我们只需要在循环中更新差值即可。

void build(int n)
{
    // 寻找最后一行的大小,也是非叶子节点的大小
    for (M = 1; M < n + 2; M <<= 1)
        ;

    // 依次填充叶子节点
    for (int i = 1; i <= n; i++)
        tree[i + M] = arr[i];

    for (int i = M - 1; i; --i)
    {
        tree[i] = max(tree[i << 1], tree[i << 1 | 1]);
        // 更新差值
        tree[i << 1] -= tree[i];
        tree[i << 1 | 1] -= tree[i];
    }
}

然后我们根据差值计算RMQ,其实是一个决策两个子树的过程。

int queryMax(int l, int r)
{
    int ans = 0;
    int L = -INF, R = -INF; // 注意这里的初始值
    for (l += M - 1, r += M + 1; l ^ r ^ 1; l >>= 1, r >>= 1)
    {
        if (~l & 1)
            L = max(L, tree[l ^ 1]);
        if (r & 1)
            R = max(R, tree[r ^ 1]);
        L += tree[l >> 1];
        R += tree[l >> 1];
    }

    ans = max(L, R);
    for (l >>= 1; l; l >>= 1)
    {
        ans += tree[l];
    }
    return ans;
}

然后我们需要实现给某一段区间进行连续加法。

拆位线段树

拆位线段树一般用于解决区间二进制问题,例如对一段区间进行区间异或等运算。其思想是把二进制每一位都建立一颗线段树,然后按照位处理的方法处理线段树即可。

CF242E 区间异或+求和

struct Node
{
    int val;
    int laz;
} t[20][400005];

int arr[100005];
int n, m;

void buildTree(int i, int l, int r, int bit)
{
    if (l == r - 1)
    {
        t[bit][i].val = (arr[l] & MSK(bit)) != 0;
    }
    else
    {
        int mid = HF(l + r);
        buildTree(LT(i), l, mid, bit);
        buildTree(RT(i), mid, r, bit);

        t[bit][i].val = t[bit][LT(i)].val + t[bit][RT(i)].val;
    }
}

void pushdown(int i, int l, int r, int bit)
{
    if (!t[bit][i].laz) return;

    int mid = HF(l + r);
    t[bit][LT(i)].val = (mid - l) - t[bit][LT(i)].val;
    t[bit][LT(i)].laz = 1 - t[bit][LT(i)].laz;

    t[bit][RT(i)].val = (r - mid) - t[bit][RT(i)].val;
    t[bit][RT(i)].laz = 1 - t[bit][RT(i)].laz;

    t[bit][i].laz = 0;
}

void update(int i, int l, int r, int a, int b, int bit)
{
    if (b <= l || a >= r) return;
    if (l >= a && r <= b)
    {
        t[bit][i].val = (r - l) - t[bit][i].val;
        t[bit][i].laz = 1 - t[bit][i].laz;
        return;
    }

    int mid = HF(l + r);

    pushdown(i, l, r, bit);
    update(LT(i), l, mid, a, b, bit);
    update(RT(i), mid, r, a, b, bit);
    t[bit][i].val = t[bit][LT(i)].val + t[bit][RT(i)].val;
}

int query(int i, int l, int r, int a, int b, int bit)
{

    if (b <= l || a >= r) return 0;
    if (l >= a && r <= b)
    {
        return t[bit][i].val;
    }

    int mid = HF(l + r);
    int ans = 0;

    pushdown(i, l, r, bit);
    ans += query(LT(i), l, mid, a, b, bit);
    ans += query(RT(i), mid, r, a, b, bit);

    return ans;
}

int main()
{
    FR;
    scanf("%d", &n);
    rep(i, 1, n) { scanf("%d", arr + i); }

    reb(i, 0, 20) { buildTree(1, 1, n + 1, i); }

    scanf("%d", &m);

    rep(i, 1, m)
    {
        int t;
        scanf("%d", &t);

        if (t == 1)
        {
            int l, r;
            scanf("%d %d", &l, &r);
            ll ans = 0;
            reb(i, 0, 20) { ans += 1ll * MSK(i) * query(1, 1, n + 1, l, r + 1, i); }

            printf("%lld\n", ans);
        }
        else
        {
            int l, r, x;
            scanf("%d %d %d", &l, &r, &x);

            reb(i, 0, 20)
            {
                if (x & MSK(i))
                {
                    update(1, 1, n + 1, l, r + 1, i);
                }
            }
        }
    }
    return 0;
}

暴力线段树

暴力线段树,在修改的时候进行暴力修改即可,再加上一定的优化即可ac。

CF438D

暴力取模,如果遇到区间最大值小于模数直接跳过即可,大于模数进行暴力取模。

存在结论 x m o d    p < x 2 x \mod p < \frac{x}{2} xmodp<2x,因此对一个数取模不会超过 O ( log ⁡ x ) O(\log x) O(logx)次。

int n, M;

struct Node
{
    ll sum;
    ll mx;
} t[400005];

ll arr[100005];

void buildTree(int i, int l, int r)
{
    if (l == r - 1)
    {
        t[i].mx = arr[l];
        t[i].sum = arr[l];
    }
    else
    {
        int mid = HF(l + r);
        buildTree(LT(i), l, mid);
        buildTree(RT(i), mid, r);

        t[i].sum = t[LT(i)].sum + t[RT(i)].sum;
        t[i].mx = max(t[LT(i)].mx, t[RT(i)].mx);
    }
}

void update(int i, int l, int r, int x, ll val)
{

    if (l == r - 1)
    {
        t[i].sum = val;
        t[i].mx = val;
    }
    else
    {
        int mid = HF(l + r);

        if (x < mid)
        {
            update(LT(i), l, mid, x, val);
        }
        else
        {
            update(RT(i), mid, r, x, val);
        }

        t[i].sum = t[LT(i)].sum + t[RT(i)].sum;
        t[i].mx = max(t[LT(i)].mx, t[RT(i)].mx);
    }
}

ll query(int i, int l, int r, int a, int b)
{

    if (b <= l || a >= r) return 0;
    if (l >= a && r <= b)
    {
        return t[i].sum;
    }

    int mid = HF(l + r);
    ll ans = 0;

    ans += query(LT(i), l, mid, a, b);
    ans += query(RT(i), mid, r, a, b);

    return ans;
}

void mod(int i, int l, int r, int a, int b, ll m)
{

    if (b <= l || a >= r) return;
    if (l >= a && r <= b && t[i].mx < m)
    {
        return;
    }

    if (l == r - 1)
    {
        t[i].mx = t[i].mx % m;
        t[i].sum = t[i].sum % m;
        return;
    }
    int mid = HF(l + r);

    mod(LT(i), l, mid, a, b, m);
    mod(RT(i), mid, r, a, b, m);

    t[i].sum = t[LT(i)].sum + t[RT(i)].sum;
    t[i].mx = max(t[LT(i)].mx, t[RT(i)].mx);
}

int main()
{
    FR;
    scanf("%d %d", &n, &M);
    rep(i, 1, n) { scanf("%lld", arr + i); }
    buildTree(1, 1, n + 1);
    rep(i, 1, M)
    {
        int op;
        scanf("%d", &op);
        if (op == 1)
        {
            int l, r;
            scanf("%d %d", &l, &r);
            printf("%lld\n", query(1, 1, n + 1, l, r + 1));
        }
        else if (op == 2)
        {
            int l, r;
            ll m;
            scanf("%d %d %lld", &l, &r, &m);
            mod(1, 1, n + 1, l, r + 1, m);
        }
        else
        {
            int k;
            ll x;
            scanf("%d %lld", &k, &x);

            update(1, 1, n + 1, k, x);
        }
    }
    return 0;
}

P4145

存在 x > 4 x>4 x>4 x < x 2 \sqrt{x} < \frac{x}{2} x <2x,因此对一个数开方不会超过 O ( log ⁡ x ) O(\log x) O(logx)次。故使用暴力线段树即可,维护最大值,如果最大值等于 1 1 1,则可以跳过区间。

计算几何覆盖线段树

一般计算几何扫描线问题都需要维护一段被线段覆盖的长度,此时我们可以使用线段树维护:

void updateSelf(int i, int l, int r)
{
    if (t[i].cnt > 0)
        t[i].len = decre[r] - decre[l];
    else
        t[i].len = t[i << 1].len + t[(i << 1) | 1].len;
}
void cover(int i, int l, int r, int L, int R)
{
    if (r <= L || l >= R)
        return;
    if (l >= L && r <= R)
    {
        t[i].cnt++;
        updateSelf(i, l, r);
        return;
    }

    int mid = (l + r) >> 1;
    cover(i << 1, l, mid, L, R);
    cover((i << 1) | 1, mid, r, L, R);
    updateSelf(i, l, r);
}
void uncover(int i, int l, int r, int L, int R)
{
    if (r <= L || l >= R)
        return;
    if (l >= L && r <= R)
    {
        t[i].cnt--;
        updateSelf(i, l, r);
        return;
    }

    int mid = (l + r) >> 1;
    uncover(i << 1, l, mid, L, R);
    uncover((i << 1) | 1, mid, r, L, R);
    updateSelf(i, l, r);
}
int query()
{
    return t[1].len;
}

注意,此代码仅支持先覆盖后退出,有时需要离散化。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值