Leetcode--2569. 更新数组后求和查询(浅谈对线段树相关理解)

文章介绍了如何使用线段树解决LeetCode的一道困难题,强调了线段树在处理区间问题时的优势。线段树是一种二叉树结构,适用于区间查询和更新,其时间复杂度为O(logN)。文章还提到了二叉索引树(树状数组)作为空间优化的选择,并对比了两者在不同场景下的适用性。
摘要由CSDN通过智能技术生成

题目背景:

1.该题来自Leetcode--2569. 困难题。

2.该题对于没有系统学习的同志来讲确实是一道困难题。但如果系统了解过一些数据结构。那么这道题难度急转直下,是一道简单的模板题(稍加变形)。

闲言少叙,书归正传!

题目描述:

 


我们此前说过这道题在经过系统地了解数据结构后,我们会很快得到需要使用“线段树”来进行设计。

在这里,我们借此机会来系统了解(记录)“线段树”的相关知识

前置知识

线段树

线段树的概念

线段树擅长处理区间问题。形如下图,线段树是一个完美二叉树,换言之可以使用数组存放相关数组。树上的每一个节点维护一个区间,而根维护整个区间。一般,我们的子节点维护父节点二分后的其中一个区间。对此我们可以在O(logN)的时间内维护区间。

基于线段树的RMQ问题

 为了更好地理解线段树的概念和操作,我们引入经典的题目来讲解相关操作。

 

基于线段树的RMQ查询

 

像这样,就算要查询比较大的区间,也会因为较为靠上的节点范围较大,而只需要查找较小的节点个数就可以获取最后的答案。

 PS:注意在没有交集时,返回不影响答案的值。例如:在求和中返回0,求积时返回1。

基于线段树的RMQ更新

 如上图,我们将a0值进行更改,我们只需要访问4个节点。也就是子区间访问父区间的过程

基于线段树的RMQ的复杂度分析

 代码实现

const int MAX_N = 1 << 17;

//线段树
int dat[2 * MAX_N - 1];

void init(int _n) {
    int n = 1;
    while(n < _n) n *= 2;//为了方便将_n扩展到2的幂

    for(int i = 0; i <= 2 * n - 1; ++i) dat[i] = INT_MAX;
    return ;
}

//创建线段树--编号从1开始,区间设计[l, r)
void build(int id, int l, int r, int nums[]) {
    if (l - r == 1) {
        dat[id] = nums[l];
        return ;
    }
    
    int mid = (l + r) / 2; //部分版本int mid = (l + r) >> 1;
    build(2 * id, l, mid, nums);
    bulid(2 * id, mid, r, nums);

    dat[id] = std::min(dat[2 * id], dat[2 * id + 1]);
    return ;
}

//更新第k个值--可以计算得到 id = k + n;
void updata(int id, int l, int r, int k, int x) {
    if(l == k && k + 1 == r) {
        dat[id] = std::min(dat[id], x);
        return;
    }

    if(k < l || k >= r) return INT_MAX;
    if(l <= k && k < r) {
        int mid = (l +r) / 2;
        updata(2 * id, l, mid, k, x);
        updata(2 * id + 1, mid, r, k, x);
        
        dat[id] = std::min(dat[2 * id], dat[2 * id + 1]);
    }
}

//从编号从0起
void updata2(int k, int x) {
    int id = k + n - 1;
    dat[id] = x;
    while(id > 1) {
        id = id / 2;
        dat[id] = std::min(dat[2 * id], dat[2 * id + 1]);
    }
}

//在编号区间id中查询[q_l, q_r)
int query(int id, int l, r, int q_l, int q_r) {
    if(l <= q_l && q_r <= r) {//查询区间包含编号区间
        return dat[id];
    }
    if(q_l >= r || q_r <= l) {
        return INT_MAX;
    }

    int mid = (l + r) / 2;
    int vl = query(2 * id, l, mid, q_l, q_r);
    int v2 = query(2 * id + 1, mid, r, q_l, q_r);

    retuen dat[id] = std::min(v1, v2);
}

 特此说明:

在updata函数中,我们说明过,如果更新第k个值可以直接计算得到对应的编号区间id = k + n - 1;

至于为何成立这涉及到在线段树中,所有节点和为2*n - 1;其中n为偶数,最后一行会有n个节点,那么前面就有n-1个节点。也就是说在偶数情况下,无论编号从0开始还是1开始都是正确的。但是一旦变成奇数,那么编号从1开始就会产生错误。

二叉索引树

二叉索引树看似高大上,其实它一点也不高级。它有另外一个名字--树状数组。说白了,也就是线段树的空间优化。

我们以和为例子来进行讲解。

 我们将这些不要的灰色点去掉,就得到了树状数组。

 同时我们给出树状数组每一个节点的编号的二进制编码。

BIT求和

 BIT更新

代码实现

int sum(int i) {
    int sum = 0;
    while(i) {
        sum += dat[i];
        
        i -= i & (-i);
    }
    rteurn sum;
}

//在第i个值上加x
void add(int i, int x) {
    while(i <= n) {
        dat[i] += x;
        
        i += i & (-i);
    }

    return ;
}

总结

讲解完我们的前置知识,我们先来浅谈一下线段树和树状数组。

树状数组擅长处理前缀,而线段树擅长处理区间。而值得一提的是,我们可以使用presum[i] - presum[j]来求取[i, j]的区间和。所以,在一般情况下树状数组也擅长处理区间

那么,我们不禁发问!树状数组就一定何时何地都比线段树优秀吗?

答案是否定的,树状数组的优秀有一个前提条件:对一个值进行更迭。

但是,如果我们需要对区间内的多个值进行更迭。那么树状数组就需要一个个的遍历更新,而每次更新需要O(logN)的时间。所以最终的更新时间会降至O(NlogN)。那么,为了保证树状数组的高效。我们需要转求与线段树

那么为了保证线段树的高效性,我们可以给出两个策略。

如果对某一区间内的所有值都进行统一的更迭操作,那么我们可以使用另外的数组来存放某一区间共同加上的值。

如果对某一区间内的部分值更迭,那么我们可以采用“缓式评估”策略。


视野收束

那么我们回到此道题目上,我们看到我们需要处理的是区间问题。同时呢,他是对我们的某一区间的部分值进行更迭,所以我们在此选择使用线段树来进行维护关系

同时,我们选择使用了“缓式评估”策略。说得通俗一点就是“摆烂行为”。

AC代码

class Solution {
private:
    struct SegNode {
        int l, r, sum;
        bool lazytag;

        SegNode() {
            this->l = 0;
            this->r = 0;
            this->sum = 0;
            this->lazytag = false;
        }
    };
    
    //线段树区间设计[)
    class SegmentTree {
    private:
        vector<SegNode> arr;

    public:
        SegmentTree(vector<int>& nums) {
            int n = nums.size();
            arr.resize(4 * n + 1);

            build(1, 0, n, nums);
        }

        /*构建线段树*/
        void build(int id, int l, int r, vector<int>& nums) {
            arr[id].l = l;
            arr[id].r = r;
            arr[id].lazytag = false;
            if (r - l == 1) {
                arr[id].sum = nums[l];
                return;
            }

            int mid = (l + r) >> 1;
            build(2 * id, l, mid, nums);
            build(2 * id + 1, mid, r, nums);

            arr[id].sum = arr[2 * id].sum + arr[2 * id + 1].sum;
            return;
        }

        /*区间求和 [)*/
        int sumRange(int l, int r) {
            return query(1, l, r);
        }

        int query(int id, int l, int r) {
            if (l <= arr[id].l && arr[id].r <= r) {//包含编号区间
                return arr[id].sum;
            }
            else if (arr[id].l > r || arr[id].r <= l) {//不包含编号区间
                return 0;
            }

            //部分包含
            int res = 0;
            if (arr[2 * id].r > l) {
                res += query(2 * id, l, r);
            }
            if (arr[2 * id + 1].l < r) {
                res += query(2 * id + 1, l, r);
            }

            return res;
        }

        //反转区间值,按照题意传入[l, r],而modify区间设计为[)
        void reverseRange(int l, int r) {
            modify(1, l, r + 1);//注意,需要变化操作,原因已标注
            return;
        }

        //修改区间,区间设计[)
        void modify(int id, int l, int r) {
            if (l <= arr[id].l && arr[id].r <= r) {
                arr[id].sum = arr[id].r - arr[id].l - arr[id].sum;
                arr[id].lazytag = !arr[id].lazytag;
                return;
            }
            pushdown(id);//向下更新--缓式评估

            int mid = (arr[id].l + arr[id].r) >> 1;
            if (arr[2 * id].r > l) {
                modify(2 * id, l, r);
            }
            if (arr[2 * id + 1].l < r) {
                modify(2 * id + 1, l, r);
            }
            arr[id].sum = arr[2 * id].sum + arr[2 * id + 1].sum;

            return;
        }

        void pushdown(int id) {
            if (arr[id].lazytag) {
                int que_l = 2 * id;
                int que_r = que_l + 1;
                arr[que_l].lazytag = !arr[que_l].lazytag;
                arr[que_l].sum = arr[que_l].r - arr[que_l].l - arr[que_l].sum;

                arr[que_r].lazytag = !arr[que_r].lazytag;
                arr[que_r].sum = arr[que_r].r - arr[que_r].l - arr[que_r].sum;

                arr[id].lazytag = false;
            }
        }
    };
public:
    vector<long long> handleQuery(vector<int>& nums1, vector<int>& nums2, vector<vector<int>>& queries) {
        int n = nums1.size();
        int m = queries.size();
        SegmentTree tree(nums1);

        vector<long long> ans;
        long long sum = 0;
        for (int i = 0; i < nums2.size(); ++i) {
            sum += (long long)nums2[i];
        }

        for (int i = 0; i < m; ++i) {
            if (queries[i][0] == 1) {
                tree.reverseRange(queries[i][1], queries[i][2]);
            }
            else if (queries[i][0] == 2) {
                sum += (long long)tree.sumRange(0, n) * queries[i][1];
            }
            else {
                ans.emplace_back(sum);
            }
        }

        return ans;
    }
};

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值