【五十八】【算法分析与设计】线段树和IndexTree应用,315. 计算右侧小于当前元素的个数,699. 掉落的方块,327. 区间和的个数,线段树+IndexTree求解动态前缀和,动态区间和,坐

315. 计算右侧小于当前元素的个数

给你一个整数数组 nums ,按要求返回一个新数组 counts 。数组 counts 有该性质: counts[i] 的值是 nums[i] 右侧小于 nums[i] 的元素的数量。

示例 1:

 
 

输入:nums = [5,2,6,1] 输出:[2,1,1,0] 解释: 5 的右侧有 2 个更小的元素 (2 和 1) 2 的右侧仅有 1 个更小的元素 (1) 6 的右侧有 1 个更小的元素 (1) 1 的右侧有 0 个更小的元素

示例 2:

输入:nums = [-1] 输出:[0]

示例 3:

输入:nums = [-1,-1] 输出:[0,0]

提示:

  • 1 <= nums.length <= 10(5)

  • -10(4) <= nums[i] <= 10(4)

题目解题思路

1.

给我们一个nums数组,例如,5,2,6,1。需要填写count数组,并返回。count数组的意思是,count[i]表示nums[i]元素右边有多少个小于自己的元素。

不妨假设我们当前位置是i位置,思考如何填写count[i]的值。

如果我们直接遍历后面的元素,计数有多少个小于自己的元素,初步估计时间复杂度是O(n^2)。

nums.length最大值是10^5。O(n^2)计算出次数是10^10次。

大多数在线编程平台(如 LeetCode, Codeforces, HackerRank 等)通常对每个测试用例设置了时间限制,这个限制一般在 1 到 2 秒之间。这意味着你的代码需要在这个时间范围内完成计算并输出结果。

操作数量:(10^{10}) 操作对应于 10,000,000,000 次迭代或方法调用,这是一个相当大的数字。

单次操作耗时:在标准桌面或服务器处理器上,每秒可以执行大约 (10^8) 到 (10^9) 个简单操作(这取决于操作的复杂性和处理器的性能)。这意味着理论上,(10^{10}) 操作至少需要 10 到 100 秒才能完成,远远超出了大多数在线评测系统的标准时间限制。

因此对于10^10次的代码我们应该认为一定会超时,而不去写这样的代码。

2.

继续思考如何计算count[i]的数值。我们希望计算右边比自己小的数,遇到的问题是我不知道右边哪些数比我小,那些数比我大,所以我必须每个数都遍历,然后去判断是不是比我小。这才是问题本身。

如果我们可以不去比较,直接判断那些数比我小那些数比我大,我们就可以以更快的方式得到答案。

如何不去比较得出一些数与我的大小的比较?很简单,只需要排序就可以了。

如果把一些数进行升序排序,那么左边的数就是比我小的数,这是显而易见的。

我想要找右边比我小的数的个数,那么只需要把所有数排序,用一个count数组记录右边数的个数即可。

当我i==2的时候,右边数1、6分别出现一次,在count里面记录,只需要遍历sort_nums小于2的区域计数即可,问题就转变为动态前缀和。此时问题是如何解决count与sort_nums下标绑定的问题。

如果以sort_nums为下标,会发现小于0的部分没办法当作是下标。

我们只需要让count数组和sort_nums一一绑定就可以了,当然可以再绑定一个下标,让这三者同时绑定在一起。

此时我们需要一个map,用于快速查找nums[i]==2对应的下标1,然后通过下标1去找count 0~1-1区间和。

问题就真正变成动态查找 0~i-1区间前缀和+单点更新count数组。

3.

于是我们的解题思路是,从后往前遍历nums数组,i位置,首先通过map查找i位置nums对应的下标index,然后计算0~index-1 count区间和,填写到ret数组i位置。

然后把nums[i]对应index位置count值++。

暴力求解动态前缀和(超时)

暴力求解动态前缀和,每一次用for循环计算0~index-1区间count值,初略估计时间复杂度也是O(n),根据前面的计算还是会超时,但是还是把题解写了,因为这涉及到多个数组绑定+问题转化。如果能够把心中的想法用代码实现即使是超时也是有意义的。

 
class Solution {
public:
    vector<int> countSmaller(vector<int>& nums) {
        vector<int> ret(nums.size());  // 创建一个和输入数组同样大小的数组ret,用来存放结果
        
        vector<int> count_nums(nums.size());  // 创建一个计数数组,用来记录每个元素出现的次数
        map<int, int> nums_index;  // 创建一个map,用来存储每个数和其对应的索引
        
        // 遍历输入数组,初始化map,为每个数分配一个映射,但暂时不分配具体的索引值
        //因为我需要用map的特性先排序
        for (auto& x : nums) {
            nums_index[x];
        }

        int index = 0;  // 索引初始化为0
        // 为map中的每个元素分配一个连续的索引,按照从小到大的顺序
        //排序排好了就依次添加索引
        for (auto& x : nums_index) {
            x.second = index++;
        }
        
        // 从数组的最后一个元素向前遍历
        for (int i = nums.size() - 1; i >= 0; i--) {
            int idx = nums_index[nums[i]];  // 获取当前元素的索引
            count_nums[idx]++;  // 更新当前元素的计数
            int sum = 0;  // 初始化sum,用于计算小于当前元素的个数
            
            // 遍历当前元素索引左边的所有元素,累加它们的计数,得到小于当前元素的数量
            for (int j = 0; j < idx; j++) {
                sum += count_nums[j];
            }
            
            ret[i] = sum;  // 将计算结果存储到结果数组中
        }
        
        return ret;  // 返回结果数组
    }
};

IndexTree求解动态前缀和

利用IndexTree求解动态一维前缀和是非常好的办法。

首先IndexTree解决的问题就是动态前缀和,动态区间和查询,每一次查询时间复杂度非常快。

唯一的缺陷是没办法做到区间更新,只能做到单点更新。但是这道题目就是单点更新+动态前缀和,完美契合IndexTree。

IndexTree的代码也比较简洁。

 
class IndexTree {
private:
    int size;  // 树状数组的大小
    vector<int> tree;  // 用于存储树状数组的数据结构

    // 计算x的二进制表示中最低位的1及其后面的0组成的数值
    int lowbit(int i) { return i & -i; }

    // 在树状数组中的指定位置index加上值c
    void _add(int index, int c) {
        while (index < size) {
            tree[index] += c;
            index += lowbit(index);
        }
    }

    // 计算从数组起始到index的前缀和
    int _sum(int index) {
        int ret = 0;
        while (index > 0) {
            ret += tree[index];
            index -= lowbit(index);
        }
        return ret;
    }

public:
    // 构造函数,初始化树状数组的大小和容器
    IndexTree(int n) {
        size = n + 1;  // 因为树状数组的索引从1开始
        tree.resize(size);
    }

    // 对外暴露的添加数据的接口,对应的数组位置加1,因为索引从1开始
    void add(int index, int c) { _add(index + 1, c); }

    // 对外暴露的计算前缀和的接口,计算前缀和到index,因为索引从1开始
    int sum(int index) { return _sum(index + 1); }

    // 计算区间[left, right]的和
    int range(int left, int right) { return sum(right + 1) - sum(left); }
};
class Solution {
public:
    vector<int> countSmaller(vector<int>& nums) {
        IndexTree t(nums.size());  // 创建一个树状数组实例,大小与nums相同

        map<int, int> nums_index;  // 映射每个数到一个唯一的索引
        for (auto& x : nums)
            nums_index[x];  // 为nums中的每个元素在map中创建键
        int inx = 0;
        // 为map中的每个元素(即nums的去重排序版本)分配一个递增的索引
        for (auto& x : nums_index)
            x.second = inx++;

        vector<int> counts(nums.size());  // 创建结果数组,用于存储每个元素右侧小于它的元素数量
        for (int i = nums.size() - 1; i >= 0; i--) {
            int index = nums_index[nums[i]];  // 获取当前元素的索引
            counts[i] = t.sum(index - 1);  // 计算当前元素之前(即索引更小的元素)的累加和
            t.add(index, 1);  // 在树状数组中,当前元素的位置加1
        }

        return counts;  // 返回结果
    }
};

线段树求解动态前缀和(重新写一遍)

利用线段树求解动态前缀和查询以及单点更新。‘

暴力求解前缀和查询每一次都是O(N),如果是静态的前缀和查询只需要利用前缀和数组即可时间复杂度是O(1)。

但是动态前缀和查询,每一次更新数据对前缀和数组会造成影响,没办法直接使用前缀和数组计算。

但是线段树可以求解动态前缀和查询以及单点更新操作。

线段树相较于IndexTree代码会多许多。优势是可以快速解决区间和更新操作。

 
class SegmentTree2 {
public:
    int MAXN; // 数组的最大长度
    vector<int> arr; // 原始数据数组
    vector<int> sum; // 线段树的节点值,用于存储区间和
    vector<int> lazy; // 延迟更新数组,用于优化区间更新操作

    // 构造函数,初始化线段树
    SegmentTree2(vector<int> origin) {
        MAXN = origin.size() + 1;
        arr.resize(MAXN);
        for (int i = 1; i < arr.size(); i++)
            arr[i] = origin[i - 1];

        sum.resize(MAXN << 2);
        lazy.resize(MAXN << 2, 0); // 初始化lazy数组为0
    }

    // 向上更新节点值,用于构建树或者更新后修正父节点的值
    void pushUp(int rt) {
        sum[rt] = sum[rt << 1] + sum[rt << 1 | 1];
    }

    // 构建线段树,l和r表示当前节点覆盖的区间,rt表示当前节点在数组中的位置
    void build(int l, int r, int rt) {
        if (l == r) {
            sum[rt] = arr[l];
            return;
        }
        int mid = (l + r) >> 1;
        build(l, mid, rt << 1);
        build(mid + 1, r, rt << 1 | 1);
        pushUp(rt);
    }

    // 查询区间[L, R]的和,l和r表示当前节点覆盖的区间,rt为当前节点位置
    long query(int L, int R, int l, int r, int rt) {
        if (L <= l && r <= R) {
            return sum[rt];
        }
        int mid = (l + r) >> 1;
        pushDown(rt, mid - l + 1, r - mid);
        long ret = 0;
        if (L <= mid) {
            ret += query(L, R, l, mid, rt << 1);
        }
        if (R > mid) {
            ret += query(L, R, mid + 1, r, rt << 1 | 1);
        }
        return ret;
    }

    // 区间添加值函数,为[L, R]区间的每个元素增加C
    void add(int L, int R, int C, int l, int r, int rt) {
        if (L <= l && r <= R) {
            sum[rt] += C * (r - l + 1);
            lazy[rt] += C;
            return;
        }
        int mid = (l + r) >> 1;
        pushDown(rt, mid - l + 1, r - mid);
        if (L <= mid) {
            add(L, R, C, l, mid, rt << 1);
        }
        if (R > mid) {
            add(L, R, C, mid + 1, r, rt << 1 | 1);
        }
        pushUp(rt);
    }

    // 向下传递更新信息,将当前节点的更新信息传递给其子节点
    void pushDown(int rt, int ln, int rn) {
        if (lazy[rt] != 0) {
            lazy[rt << 1] += lazy[rt];
            sum[rt << 1] += lazy[rt] * ln;
            lazy[rt << 1 | 1] += lazy[rt];
            sum[rt << 1 | 1] += lazy[rt] * rn;
            lazy[rt] = 0;
        }
    }
};
class Solution {
public:
    vector<int> countSmaller(vector<int>& nums) {
        vector<int> ret(nums.size());
        set<int> nums_set; // 使用集合对元素进行去重和排序
        for (auto x : nums)
            nums_set.insert(x);
        vector<int> nums_set_now(nums_set.begin(), nums_set.end());

        vector<int> count_nums(nums_set_now.size());
        unordered_map<int, int>nums_index;  // 为每个元素创建一个唯一索引
        for (int i = 0; i < nums_set_now.size(); i++)
            nums_index[nums_set_now[i]] = i;

        SegmentTree2 segTree(count_nums);  // 使用去重并排序后的元素初始化线段树
        segTree.build(1, count_nums.size(), 1);  // 构建线段树

        for (int i = nums.size() - 1; i >= 0; i--) {  // 从数组的最后一个元素开始
            int index = nums_index[nums[i]];  // 获取当前元素的索引
            segTree.add(index + 1, index + 1, 1, 1, count_nums.size(), 1);  // 在线段树中,增加当前元素的计数

            int sum = 0;
            if (index > 0) { 
                sum = segTree.query(1, index, 1, count_nums.size(), 1);  // 查询当前元素之前的所有元素的计数总和
            }
            ret[i] = sum;  // 将查询到的计数总和作为结果存入返回数组
        }
        return ret;  // 返回结果数组,每个元素表示对应位置元素右侧小于该元素的数量
    }
};

699. 掉落的方块

在二维平面上的 x 轴上,放置着一些方块。

给你一个二维整数数组 positions ,其中 positions[i] = [left(i), sideLength(i)] 表示:第 i 个方块边长为 sideLength(i) ,其左侧边与 x 轴上坐标点 left(i) 对齐。

每个方块都从一个比目前所有的落地方块更高的高度掉落而下。方块沿 y 轴负方向下落,直到着陆到 另一个正方形的顶边 或者是 x 轴上 。一个方块仅仅是擦过另一个方块的左侧边或右侧边不算着陆。一旦着陆,它就会固定在原地,无法移动。

在每个方块掉落后,你必须记录目前所有已经落稳的 方块堆叠的最高高度

返回一个整数数组 ans ,其中 ans[i] 表示在第 i 块方块掉落后堆叠的最高高度。

示例 1:

输入:positions = [[1,2],[2,3],[6,1]] 输出:[2,5,5] 解释: 第 1 个方块掉落后,最高的堆叠由方块 1 组成,堆叠的最高高度为 2 。 第 2 个方块掉落后,最高的堆叠由方块 1 和 2 组成,堆叠的最高高度为 5 。 第 3 个方块掉落后,最高的堆叠仍然由方块 1 和 2 组成,堆叠的最高高度为 5 。 因此,返回 [2, 5, 5] 作为答案。

示例 2:

输入:positions = [[100,100],[200,100]] 输出:[100,100] 解释: 第 1 个方块掉落后,最高的堆叠由方块 1 组成,堆叠的最高高度为 100 。 第 2 个方块掉落后,最高的堆叠可以由方块 1 组成也可以由方块 2 组成,堆叠的最高高度为 100 。 因此,返回 [100, 100] 作为答案。 注意,方块 2 擦过方块 1 的右侧边,但不会算作在方块 1 上着陆。

提示:

  • 1 <= positions.length <= 1000

  • 1 <= left(i) <= 10(8)

  • 1 <= sideLength(i) <= 10(6)

区间压缩

坐标压缩是一种有效的技术,尤其在处理大范围但实际使用数据点稀疏的情况下,可以显著减少内存使用和提升运算效率。这里我将通过几个具体的示例来说明坐标压缩的效果和应用。

示例 1:简单压缩

假设我们有以下方块掉落的位置和大小:

 

方块列表:[[1, 2], [100000000, 3]]

  • 未压缩的情况

    • 需要构建一个能覆盖从1到100000002的线段树(因为第二个方块从100000000开始,长度为3)。

    • 这将导致线段树的节点数量非常庞大,因为节点数是线段长度的4倍,即需要大约400,000,000个节点。

  • 压缩后的情况

    • 收集关键点:{1, 3, 100000000, 100000002}

    • 映射到新坐标:{1 -> 0, 3 -> 1, 100000000 -> 2, 100000002 -> 3}

    • 构建线段树的范围变为从0到3,大大减少了所需的节点数,只需16个节点即可。

示例 2:重叠区域

假设方块的落点有重叠:

 

方块列表:[[10, 5], [14, 2], [20, 1]]

  • 未压缩的情况

    • 线段树需要覆盖从10到20的范围。

    • 虽然范围不大,但如果坐标直接用作线段树构建,还是会有一定的空间浪费。

  • 压缩后的情况

    • 收集关键点:{10, 15, 14, 16, 20, 21}

    • 映射到新坐标:{10 -> 0, 14 -> 1, 15 -> 2, 16 -> 3, 20 -> 4, 21 -> 5}

    • 只需覆盖从0到5的范围,使用更少的节点即可实现。

示例 3:密集数据

如果方块密集落在一小块区域内:

 

方块列表:[[100, 10], [105, 10], [110, 10]]

  • 未压缩的情况

    • 需要一个覆盖100到120的线段树。

  • 压缩后的情况

    • 收集关键点:{100, 110, 105, 115, 120}

    • 映射到新坐标:{100 -> 0, 105 -> 1, 110 -> 2, 115 -> 3, 120 -> 4}

    • 映射后只需覆盖0到4的范围,减少了节点的数量。

效果说明

通过坐标压缩,实际操作的坐标数量大大减少,从而显著减小了线段树所需的节点数。这样不仅节省了内存,而且由于操作的数据量减少,提高了整体的运算效率。在实际应用中,尤其是涉及到大数据处理的场景,坐标压缩是一种非常有效的优化策略。

解题思路

1.

首先我们有一个position数组,每一个变量是一个vector数组,这个vector数组有两个值,第一个值是方块下落的初始下标,第二个值是方块的底边和高的长度。

每次落下一个方块,然后记录当前情况的最高高度,放到ret数组中,最后返回ret。

如果有两个方块[1,2],[3,1],转化过来第一个方块的高度是2,落下的区间是[1,3],ret.push_back(3),第二个方块的高度是1,落下的区间是[3,4]。ret.push_back(3)。我们发现实际上两个方块不会叠在一起,所以我们不能把1~3的值设置为3,我们可以定义区间[1,1]表示[1,2),意思是1~2的区间长度,[2,1]表示[2,3)意思是2~3的区间长度。

我们需要做的是记录每一个区间的最大值,然后计算当前方块的高度和落下的区间,把这个区间的最大值设置为原先的最大值+当前方块的高度。如果查询整个区间的最大值尾插到ret即可。

用maxcount记录各个区间的最大值。

可以使用坐标压缩,因为我们关注的是最大值,并不关注区间长度,因此我们可以把所有的正方形想象成长方形,底边永远都是1,如果两个方块重叠就叠在一起,不是重叠永远都是紧挨着。

2.

首先用map记录所有需要用到的区间坐标,并且排序。

记录完毕后给map中每一个值映射索引,压缩坐标。

3.

遍历position,每一次获取对应坐标的索引。

[left,right],遍历这个区间找到区间最大值max1,然后维护这个区间的最大值newh=max1+x[1]。

用maxh记录整个区间的最大值,然后维护maxh尾插到ret中。

暴力 查询区间值+更新区间值+坐标压缩

 
class Solution {
public:
    vector<int> fallingSquares(vector<vector<int>>& positions) {
        map<int, int> nums_index; // 使用map来存储不同的坐标点,自动排序并去重
        vector<int> ret; // 用来存储每个方块掉落后的最高高度结果

        // 遍历positions数组,将每个方块的左边界和右边界(左边界+边长-1)加入到map中
        for (auto& x : positions) {
            nums_index[x[0]];
            nums_index[x[0] + x[1] - 1];
        }

        int index = 0; // 用于给map中的每个唯一坐标点分配一个连续的索引
        // 为map中每个坐标点分配索引
        for (auto& x : nums_index) {
            x.second = index++;
        }

        vector<int> maxcount(index); // 使用一个数组来记录每个坐标点的当前最大高度
        int maxh = INT_MIN; // 记录目前为止所有方块堆叠的最高高度

        // 再次遍历positions数组,计算每个方块掉落后的最高堆叠高度
        for (auto& x : positions) {
            int left = nums_index[x[0]]; // 获取当前方块左边界的索引
            int right = nums_index[x[0] + x[1] - 1]; // 获取当前方块右边界的索引

            int max1 = INT_MIN; // 用于记录当前方块下方已有方块的最大高度
            // 遍历当前方块覆盖的所有索引,找出最大高度
            for (int i = left; i <= right; i++) {
                max1 = fmax(maxcount[i], max1);
            }
            int newh = max1 + x[1]; // 计算当前方块掉落后的新高度

            // 更新当前方块覆盖的所有索引的高度值
            for (int i = left; i <= right; i++) {
                maxcount[i] = newh;
            }
            // 更新全局的最大高度
            if (newh > maxh)
                maxh = newh;
            ret.push_back(maxh); // 将当前的最大高度加入到结果数组中
        }

        return ret; // 返回结果数组
    }
};

线段树+坐标压缩

因为每一次都需要求区间最大值,也就是查询区间值,并且更新区间值,所以可以使用线段树。

 
class SegmentTree {
private:
    vector<int> tree_max; // 线段树数组,存储每个区间的最大值
    vector<bool> isupdate; // 懒惰标记数组,标记该区间是否需要更新
    vector<int> change; // 如果有更新,记录需要更新的新值
    int size; // 线段树管理的原始数据大小

    // 向上更新函数,更新父节点的最大值
    void pushup(int rt) {
        tree_max[rt] = max(tree_max[rt * 2], tree_max[rt * 2 + 1]);
    }

    // 懒惰传播函数,将当前节点的更新信息传递给子节点
    void pushdown(int rt, int ln, int rn) {
        if (isupdate[rt]) {
            isupdate[rt * 2] = true;
            isupdate[rt * 2 + 1] = true;
            change[rt * 2] = change[rt];
            change[rt * 2 + 1] = change[rt];
            tree_max[rt * 2] = change[rt];
            tree_max[rt * 2 + 1] = change[rt];
            isupdate[rt] = false;
        }
    }

    // 区间更新函数
    void update_range(int L, int R, int C, int l, int r, int rt) {
        if (L <= l && r <= R) {
            isupdate[rt] = true;
            change[rt] = C;
            tree_max[rt] = C;
            return;
        }
        int mid = (l + r) / 2;
        pushdown(rt, mid - l + 1, r - mid);
        if (L <= mid)
            update_range(L, R, C, l, mid, rt * 2);
        if (R > mid)
            update_range(L, R, C, mid + 1, r, rt * 2 + 1);
        pushup(rt);
    }

    // 区间查询函数
    int query_range(int L, int R, int l, int r, int rt) {
        if (L <= l && r <= R) {
            return tree_max[rt];
        }
        int mid = (l + r) / 2;
        pushdown(rt, mid - l + 1, r - mid);
        int ans = INT_MIN;
        if (L <= mid)
            ans = max(ans, query_range(L, R, l, mid, rt * 2));
        if (R > mid)
            ans = max(ans, query_range(L, R, mid + 1, r, rt * 2 + 1));
        return ans;
    }

public:
    SegmentTree(int n)
        : size(n), tree_max(4 * n), isupdate(4 * n, false), change(4 * n) {}

    void update(int L, int R, int C) { update_range(L, R, C, 1, size, 1); }

    int query(int L, int R) { return query_range(L, R, 1, size, 1); }
};
class Solution {
public:
    vector<int> fallingSquares(vector<vector<int>>& positions) {
        vector<int> result; // 存储每次方块落下后的最大高度
        map<int, int> coordCompress; // 坐标压缩映射

        // 压缩坐标,将方块的左右边界都映射到一个连续的整数区间
        for (const auto& pos : positions) {
            coordCompress[pos[0]];
            coordCompress[pos[0] + pos[1] - 1];
        }

        int compressedIdx = 0;
        for (auto& coord : coordCompress) {
            coord.second = ++compressedIdx;
        }

        SegmentTree st(compressedIdx); // 初始化线段树
        int maxHeight = 0; // 记录当前的最大高度

        // 处理每个掉落的方块
        for (const auto& pos : positions) {
            int left = coordCompress[pos[0]]; // 获取压缩后的左边界
            int right =coordCompress[pos[0] + pos[1] - 1]; // 获取压缩后的右边界
            int currentHeight = st.query(left, right); // 查询当前区间的最大高度
            int newHeight = currentHeight + pos[1]; // 新方块落下后的高度
            st.update(left, right, newHeight); // 在区间上更新新的高度值
            maxHeight = max(maxHeight, newHeight); // 更新当前已经落稳的方块堆叠的最高高度
            result.push_back(maxHeight); // 将当前最大高度添加到结果数组
        }
        
            return result; // 返回结果数组,每个元素代表在对应方块掉落后的最高堆叠高度
        }

};

327. 区间和的个数

给你一个整数数组 nums 以及两个整数 lowerupper 。求数组中,值位于范围 [lower, upper] (包含 lowerupper)之内的 区间和的个数

区间和 S(i, j) 表示在 nums 中,位置从 ij 的元素之和,包含 ij (ij)。

示例 1:

输入:nums = [-2,5,-1], lower = -2, upper = 2 输出:3 解释:存在三个区间:[0,0]、[2,2] 和 [0,2] ,对应的区间和分别是:-2 、-1 、2 。

示例 2:

输入:nums = [0], lower = 0, upper = 0 输出:1

提示:

  • 1 <= nums.length <= 10(5)

  • -2(31) <= nums[i] <= 2(31)1

  • -10(5) <= lower <= upper <= 10(5)

  • 题目数据保证答案是一个 32 位 的整数

解题思路

1.

遍历所有区间,利用前缀和数组计算出区间和,比较是否再lower,upper之间内,如果在就ret++。

时间复杂度是O(N^2),最大次数是10^10,显然会超时。但是还是把所思考的代码写出来。

2.

如果我们可以像315. 计算右侧小于当前元素的个数这题一样,遍历一个前缀和后,通过排序直接计算个数就可以简单很多。

lower<=prev[i]-x<=upper ==> prev[i]-upper<=x<=prev[i]-lower

对于一个prev[i]前缀和,只需要看值范围在[prev[i]-upper,prev[i]-lower]范围内的前缀和个数。

问题转化为求处于某个值区间的个数。

让所有可能值排序,然后利用map映射索引,我们就可以找到值对应的索引。

问题转化为求区间和,利用前缀和数组可以求解区间和。

动态区间和,求解完一个prev[i]需要把当前前缀和加入到数组中,单点更新。

区间查询+单点更新,线段树和IndexTree都可以解决。

前缀和+暴力计算(超时)

 
class Solution {
public:
    // 函数接受一个整数数组 nums 以及两个整数 lower 和 upper 作为输入
    int countRangeSum(vector<int>& nums, int lower, int upper) {
        using LL = long long; // 使用long long类型以支持大整数运算,防止溢出
        vector<LL> prevsum(nums.size()); // 创建一个前缀和数组,用于存储从数组开始到当前位置的元素之和
        LL sum = 0; // 初始化sum变量,用于计算前缀和
        for (LL i = 0; i < nums.size(); i++) { // 遍历数组,计算每个位置的前缀和
            sum += nums[i]; // 累加当前元素到sum
            prevsum[i] = sum; // 将当前的累计和存储到前缀和数组中
        }
        LL ret = 0; // 初始化结果变量ret,用于记录满足条件的区间和的个数
        for (LL left = 0; left < nums.size(); left++) { // 用两层循环遍历所有可能的子数组(区间)
            for (LL right = left; right < nums.size(); right++) { // 内层循环确定子数组的右边界
                LL rangesum = prevsum[right] - ((left - 1 >= 0) ? prevsum[left - 1] : 0); // 计算子数组的和
                if (rangesum >= lower && rangesum <= upper) ret++; // 如果子数组的和落在[lower, upper]内,则计数加一
            }
        }

        return ret; // 返回满足条件的区间和的个数
    }
};

前缀和+线段树+动态区间和

 
class SegmentTree {
private:
    vector<int> sum; // 线段树数组,存储区间和
    vector<int> lazy; // 懒惰标记数组,用于区间增加操作
    int size; // 线段树管理的元素数量

    // 向上更新节点,用于更新父节点的值为其两个子节点的和
    void pushup(int rt) {
        sum[rt] = sum[rt << 1] + sum[rt << 1 | 1];
    }

    // 向下传递懒惰标记,将当前节点的懒惰标记传递给其子节点
    void pushdown(int rt, int ln, int rn) {
        if (lazy[rt] != 0) {
            sum[rt << 1] += lazy[rt] * ln;
            sum[rt << 1 | 1] += lazy[rt] * rn;
            lazy[rt << 1] += lazy[rt];
            lazy[rt << 1 | 1] += lazy[rt];
            lazy[rt] = 0;
        }
    }

    // 区间添加操作
    void _add(int L, int R, int C, int l, int r, int rt) {
        if (r < L || l > R)
            return;
        if (L <= l && r <= R) {
            sum[rt] += C * (r - l + 1);
            lazy[rt] += C;
            return;
        }
        int mid = (l + r) >> 1;
        pushdown(rt, mid - l + 1, rn - mid); // 下传懒惰标记
        _add(L, R, C, l, mid, rt << 1);
        _add(L, R, C, mid + 1, r, rt << 1 | 1);
        pushup(rt);
    }

    // 区间查询操作
    int _query(int L, int R, int l, int r, int rt) {
        if (r < L || l > R)
            return 0;
        if (L <= l && r <= R) {
            return sum[rt];
        }
        int mid = (l + r) >> 1;
        pushdown(rt, mid - l + 1, rn - mid); // 下传懒惰标记
        return _query(L, R, l, mid, rt << 1) + _query(L, R, mid + 1, r, rt << 1 | 1);
    }

public:
    SegmentTree(int _size) : size(_size) {
        sum.resize(size << 2);
        lazy.resize(size << 2);
    }
    void add(int L, int R, int C) { _add(L, R, C, 0, size - 1, 1); }
    int query(int L, int R) { return _query(L, R, 0, size - 1, 1); }
};
class Solution {
public:
    using LL = long long; // 使用long long类型以支持大数运算
    int countRangeSum(vector<int>& nums, int lower, int upper) {
        map<LL, int> value_index; // 用于坐标压缩的映射
        LL sum = 0;

        // 初始化坐标压缩映射
        for (auto& x : nums) {
            sum += x;
            value_index[sum];
            value_index[sum - lower];
            value_index[sum - upper];
        }

        int inx = 0;
        for (auto& x : value_index) {
            x.second = inx++;
        }

        SegmentTree st(inx); // 初始化线段树

        int ret = 0;
        sum = 0;
        for (auto& x : nums) {
            sum += x;
            // 如果当前前缀和本身就在[lower, upper]范围内
            if (sum <= upper && sum >= lower)    ret++;
            // 计算符合条件的区间和的数量
            int left = value_index[sum - upper]; // 计算当前和减去上界后对应的压缩后的索引
            int right = value_index[sum - lower]; // 计算当前和减去下界后对应的压缩后的索引
            ret += st.query(left, right); // 查询线段树中[left, right]范围内的计数

            // 更新线段树,增加当前和的计数
            st.add(value_index[sum], value_index[sum], 1);
        }

        return ret; // 返回满足条件的区间和的总数
    }
};

前缀和+IndexTree+动态区间和

 
class indextree {
private:
    using LL = long long; // 使用long long类型以支持大整数运算,避免溢出
    LL size; // 1~size-1,树状数组的有效范围
    vector<LL> tree; // 树状数组

    LL lowbit(LL i) { // 计算lowbit,用于确定更新和查询的范围
        return i & -i;
    }

    void _add(LL index, LL c) { // 内部的添加函数,用于更新树状数组
        while (index < size) {
            tree[index] += c;
            index += lowbit(index);
        }
    }

    LL sum(LL index) { // 计算前缀和,用于区间和查询
        LL ret = 0;
        while (index > 0) {
            ret += tree[index];
            index -= lowbit(index);
        }
        return ret;
    }

    LL _rangesum(LL left, LL right) { // 内部的范围求和函数
        return sum(right) - sum(left - 1);
    }
public:
    indextree(LL n) { // 构造函数,初始化树状数组的大小
        size = n + 1;
        tree.resize(size);
    }
    LL rangesum(LL left, LL right) { // 公开的范围求和接口
        return _rangesum(left + 1, right + 1);
    }
    void add(LL index, LL c) { // 公开的添加接口,用于更新树状数组
        _add(index + 1, c);
    }
};
class Solution {
public:
    using LL = long long; // 使用long long类型以支持大整数运算

    int countRangeSum(vector<int>& nums, int lower, int upper) {
        vector<LL> prevsum(nums.size()); // 用于存储前缀和
        LL sum = 0;
        for (LL i = 0; i < nums.size(); i++) { // 计算前缀和数组
            sum += nums[i];
            prevsum[i] = sum;
        }

        map<LL, LL> prev_index; // 坐标压缩映射
        for (LL i = 0; i < nums.size(); i++) { // 初始化坐标压缩映射
            prev_index[prevsum[i]]; // 当前前缀和
            prev_index[prevsum[i] - lower]; // 当前前缀和减去lower
            prev_index[prevsum[i] - upper]; // 当前前缀和减去upper
        }

        LL index = 0; // 压缩坐标索引
        for (auto& x : prev_index) { // 为每一个前缀和分配一个唯一的索引
            x.second = index++;
        }

        indextree t(prev_index.size()); // 初始化树状数组

        LL ret = 0; // 存储满足条件的区间和数量
        for (LL i = 0; i < prevsum.size(); i++) { // 遍历每个前缀和
            if (prevsum[i] >= lower && prevsum[i] <= upper) ret++; // 如果当前前缀和本身就在范围内
            LL left = prev_index[prevsum[i] - upper]; // 计算符合条件的左边界索引
            LL right = prev_index[prevsum[i] - lower]; // 计算符合条件的右边界索引
            ret += t.rangesum(left, right); // 查询当前满足条件的区间和数量
            t.add(prev_index[prevsum[i]], 1); // 在树状数组中记录当前前缀和出现的次数
        }

        return ret; // 返回满足条件的区间和总数。

        }

};

结尾

最后,感谢您阅读我的文章,希望这些内容能够对您有所启发和帮助。如果您有任何问题或想要分享您的观点,请随时在评论区留言。

同时,不要忘记订阅我的博客以获取更多有趣的内容。在未来的文章中,我将继续探讨这个话题的不同方面,为您呈现更多深度和见解。

谢谢您的支持,期待与您在下一篇文章中再次相遇!

  • 34
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

妖精七七_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值