树状数组的用法总结

单点查询、区间修改,树状数组

tree的下标注意从1 到 n + 1,从0开始就死循环了

例题:

307. 区域和检索 - 数组可修改 - 力扣(LeetCode)

class NumArray {
public:

    vector<int> c;
    vector<int> array;
    int n = 0;

    int lowbit(int x) {
        return x & (-x);
    }

    NumArray(vector<int>& nums) {
        n = nums.size();
        c.resize(n + 1, 0);  // 树状数组使用1-based indexing
        array.resize(n, 0);
        
        // 初始化数组
        for (int i = 0; i < n; i++) {
            array[i] = nums[i];
        }
        
        // 构建树状数组
        for (int i = 0; i < n; i++) {
            add(i, nums[i]);
        }
    }

    void add(int index, int val) {
        // 转换为1-based indexing
        for (int i = index + 1; i <= n; i += lowbit(i)) {
            c[i] += val;
        }
    }

    int query(int x) {
        int ret = 0;
        // 转换为1-based indexing
        for (int i = x + 1; i > 0; i -= lowbit(i)) {
            ret += c[i];
        }
        return ret;
    }
    
    void update(int index, int val) {
        int add_val = val - array[index];
        add(index, add_val);
        array[index] = val;
    }
    
    int sumRange(int left, int right) {
        int ret = query(right);
        if (left > 0) ret -= query(left - 1);
        return ret;
    }
};

/**
 * Your NumArray object will be instantiated and called as such:
 * NumArray* obj = new NumArray(nums);
 * obj->update(index,val);
 * int param_2 = obj->sumRange(left,right);
 */

 

逆序对

image

直接map映射的话也不需要处理想等元素问题 

 

题目链接:315. 计算右侧小于当前元素的个数 - 力扣(LeetCode)

 

 

 

 

 

class Solution {
public:

    int lowbit(int x) {
        return x & (-x);
    }

    void add(const int& x, const int& cnt, vector<int>& vis) {

        for (int i = x; i < cnt; i += lowbit(i)) {
            vis[i]++;
        }
    }

    int query(const int& x, const vector<int>& vis) {
        int sum = 0;
        for (int i = x; i > 0; i -= lowbit(i)) {
            sum += vis[i];
        }
        return sum;

    }


    vector<int> countSmaller(vector<int>& nums) {
        int n = nums.size();
        vector<int> vis;
        set<int> uq;
        map<int, int> mapp;
        for (int i = 0; i < n; i++) {
            uq.insert(nums[i]);
        }
        int cnt = 1;
        for (auto item : uq) {
            mapp[item] = cnt++; 
        }
        vector<int> array;
        array.resize(cnt);
        vis.resize(cnt);
        vector<int> ret;
        for (int i = n - 1; i >= 0; i--) {
            int k = mapp[nums[i]];
            ret.push_back(query(k - 1, vis));
            add(k, cnt, vis);
        }
        
        std::reverse(ret.begin(), ret.end());
        return ret;
    }
};

 

 

 

题目链接:327. 区间和的个数 - 力扣(LeetCode)

 

第一步:理解问题转化

前缀和 + 树状数组(或线段树)
  • 计算前缀和数组 prefix,其中 prefix[k] = nums[0] + ... + nums[k-1](或类似定义)。
  • 区间和 S(i, j)可以表示为 prefix[j+1] - prefix[i]
  • 问题转化为:对于每个 j,统计有多少个 i ≤ j使得 lower ≤ prefix[j+1] - prefix[i] ≤ upper
  • 这等价于对于每个 j,找满足 prefix[j+1] - upper ≤ prefix[i] ≤ prefix[j+1] - loweri的个数。
  • 可以使用树状数组或线段树来动态维护前缀和的值域,实现 O(log n) 的查询和更新,总复杂度 O(n log n)。

第二步:如何将每次查询优化到 O(log n)?核心思想

我们需要一种数据结构,它能动态地维护我们“已经遍历过的前缀和”(即所有 pre[i], i ≤ current_j),并支持以下两种操作:
  1. 插入:当 j向后移动时,将新的 pre[j]加入这个集合。
  2. 查询:给定一个数值范围 [L, R],快速返回当前集合中,值落在这个范围内的元素个数。

那就是可以用树状数组的一个用处:O(log n) 范围统计个数

第三步:离散化

这个问题使用离散化是因为前缀和的值范围非常大(可能达到约 n * 元素值 ≈ 10^10),无法直接用数组下标表示。离散化将这些大范围的数值映射到紧凑的整数下标

 

class Solution {
public:
    int countRangeSum(vector<int>& nums, int lower, int upper) {
        int n = nums.size();
        if (n == 0) return 0;
        
        // 1. 计算前缀和数组
        vector<long long> prefix(n + 1, 0);
        for (int i = 0; i < n; i++) {
            prefix[i + 1] = prefix[i] + nums[i];
        }
        
        // 2. 收集所有需要离散化的值
        set<long long> allNumbers;
        for (int i = 0; i <= n; i++) {
            allNumbers.insert(prefix[i]);          // 前缀和本身
            allNumbers.insert(prefix[i] - lower);  // 查询右边界
            allNumbers.insert(prefix[i] - upper);  // 查询左边界
        }
        
        // 3. 离散化映射
        unordered_map<long long, int> valToIdx;
        int idx = 1;
        for (long long num : allNumbers) {
            valToIdx[num] = idx++;
        }
        
        // 4. 初始化树状数组
        BIT bit(valToIdx.size() + 5);
        
        // 5. 插入初始前缀和 pre[0] = 0
        bit.add(valToIdx[0], 1);
        
        // 6. 遍历并统计结果
        int result = 0;
        for (int j = 0; j < n; j++) {
            // 计算当前查询的左右边界
            long long left = prefix[j + 1] - upper;
            long long right = prefix[j + 1] - lower;
            
            // 查询在 [left, right] 范围内的前缀和个数
            int leftIdx = valToIdx[left];
            int rightIdx = valToIdx[right];
            
            int count = bit.query(rightIdx) - bit.query(leftIdx - 1);
            result += count;
            
            // 插入当前前缀和
            bit.add(valToIdx[prefix[j + 1]], 1);
        }
        
        return result;
    }
    
private:
    // 树状数组实现
    class BIT {
    private:
        vector<int> tree;
        int n;
        
    public:
        BIT(int size) : n(size), tree(size + 1, 0) {}
        
        // 在位置 idx 增加 1
        void add(int idx, int delta) {
            while (idx <= n) {
                tree[idx] += delta;
                idx += idx & -idx;
            }
        }
        
        // 查询前缀和 [1, idx]
        int query(int idx) {
            int sum = 0;
            while (idx > 0) {
                sum += tree[idx];
                idx -= idx & -idx;
            }
            return sum;
        }
    };
};

 

posted @ 2026-01-12 22:28  WTSRUVF  阅读(6)  评论(0)    收藏  举报