[307]. 区域和检索 - 数组可修改

Posted Debroon

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[307]. 区域和检索 - 数组可修改相关的知识,希望对你有一定的参考价值。

区域和检索 - 数组可修改

 


题目

题目:https://leetcode-cn.com/problems/range-sum-query-mutable/

 


函数原型

class NumArray 
public:
    NumArray(vector<int>& nums) 
    void update(int index, int val) 
    int sumRange(int left, int right) 
;

 


线段树(区间树)

前置:线段树的具体介绍:https://blog.csdn.net/qq_41739364/article/details/121432200

对比 303(线段树部分),307 多了一步 update,需要更新某个位置/区间的值,其他代码同。

void update(int index, int val) 
	data[index] = val;      // 数组更新
	
	// 线段树更新
	update(0, 0, data.size()-1, index, val);


void update(int treeIndex, int l, int r, int index, int val) 
	if( l == r ) 	                                    // 找到更新的节点了
		tree[treeIndex] = val;
		return;
	
	
	int mid = l + (r - l) / 2;
	int leftTreeIndex = leftchild(treeIndex);           // 计算左孩子索引
	int rightTreeIndex = rightchild(treeIndex);         // 计算右孩子索引
		
	if (index >= mid + 1)                               // 说明在右孩子中
		update(rightTreeIndex, mid + 1, r, index, val); // 去右孩子找
	else                                                // 说明在左孩子中
		update(leftTreeIndex, l, mid, index, val);      // 去左孩子找

	// 除了叶节点要更新,其实上面的父节点也得更新
	tree[treeIndex] = tree[leftTreeIndex] + tree[rightTreeIndex];

完整代码:

class NumArray 
public:
	NumArray(vector<int>& arr) 
		if (arr.size() > 0) 
			for (auto v : arr) 
				data.push_back(v);

			tree.resize(4 * data.size());
			buildTree(0, 0, data.size() - 1);        // 创建一个线段树 
		
	
	
	int sumRange(int i, int j) 
		if (data.size() == 0)	return 0;
		return query(0, 0, data.size() - 1, i, j);   // 从根节点开始查询区间[i, j]和
	

public:
	vector<int> data;     // 数组,用于保存数据副本
	vector<int> tree;     // 线段树数组
	
	// 返回完全儿叉树的数组表示中,一个索引所表示的元素的左孩子节点的索引(知道根节点,就知道左孩子)
	int leftchild(int index)    
		return index * 2 + 1;
	
	
	// 返回完全儿叉树的数组表示中,一个索引所表示的元素的右孩子节点的索引(知道根节点,就知道右孩子)
	int rightchild(int index) 
		return index * 2 + 2;
	
	
	// 在 treeIndex 位置创建表示区间 [l, r] 的线段树
	void buildTree(int treeIndex, int l, int r) 
		if (l == r)                                 // 结束条件:只有 1 个元素
			tree[treeIndex] = data[l];               // 所存储的信息等于元素本身
			return;
		
		
		// 表示一个区间
		int leftTreeIndex = leftchild(treeIndex);    // 必定有左孩子 
		int rightTreeIndex = rightchild(treeIndex);  // 相应的右孩子 
		
		// 创建左右子树相应的区间范围 
		int mid = l + (r - l) / 2;                   // 以 (l+r)/2 为中心
		buildTree(leftTreeIndex, l, mid);            // 区间 [l, mid] 创建线段树
		buildTree(rightTreeIndex, mid+1, r);         // 区间 [mid+1, r] 创建线段树
		tree[treeIndex] = tree[leftTreeIndex] + tree[rightTreeIndex]; 
		// 把左、右子树的值加在一起
 	

	/* 查询区间
		- treeIndex 表示当前处理的线段树的节点,在 tree 这个数组的什么位置
		- 在以 treeIndex 为根的线段树中 [l, r] 区间内,寻找 [queryL, queryR] 的值
	*/
	int query(int treeIndex, int l, int r, int queryL, int queryR) 
		if (l == queryL && r == queryR)         
		// 结束条件:左边界和想查询的 queryL 重合 且 右边界和想查询的 queryR 重合
			return tree[treeIndex];                       // 当前节点值是目标值
			
		// 如果这个节点不是要找的区间,就需要到当前节点的孩子节点去找
		int mid = l + (r - l) / 2;
		int leftTreeIndex = leftchild(treeIndex);         // 计算左孩子索引
		int rightTreeIndex = rightchild(treeIndex);       // 计算右孩子索引
		
		if (queryL >= mid + 1)                            // 如果目标区间和左孩子无关
			return query(rightTreeIndex, mid + 1, r, queryL, queryR); // 去右孩子找
		else if (queryR <= mid)                           // 如果目标区间和右孩子无关
			return query(leftTreeIndex, l, mid, queryL, queryR);      // 去左孩子找

		// 如果俩种情况都不是,目标区间没有完全落在孩子的区间中,那就需要进行拼接,俩边都需要找
		// 从查找 [queryL, queryR] 变成查找[queryL, mid]、[mid + 1, queryR]
		int leftresult = query(leftTreeIndex, l, mid, queryL, mid);   
		int rightresult = query(rightTreeIndex, mid + 1, r, mid + 1, queryR);
		return leftresult + rightresult;                  // 融合
	

    void update(int index, int val) 
	    data[index] = val;      // 数组更新
	
	    // 线段树更新
	    update(0, 0, data.size()-1, index, val);
    

    void update(int treeIndex, int l, int r, int index, int val) 
	    if( l == r ) 	                                    // 找到更新的节点了
		    tree[treeIndex] = val;
		    return;
	    
	
	    int mid = l + (r - l) / 2;
	    int leftTreeIndex = leftchild(treeIndex);           // 计算左孩子索引
	    int rightTreeIndex = rightchild(treeIndex);         // 计算右孩子索引
		
	    if (index >= mid + 1)                               // 说明在右孩子中
		    update(rightTreeIndex, mid + 1, r, index, val); // 去右孩子找
	    else                                                // 说明在左孩子中
		    update(leftTreeIndex, l, mid, index, val);      // 去左孩子找

	    // 除了叶节点要更新,其实上面的父节点也得更新,重新融合一次即可
	    tree[treeIndex] = tree[leftTreeIndex] + tree[rightTreeIndex];
    
;

update 函数不仅可以更新区间中某个值,也可以是更新区间的所有值。

我们按照上面的查询函数来更新区间即可,但其实我们还得更新下面的叶节点。

如果叶节点也更新,就是 O ( n ) O(n) O(n) 了。

于是,更好的是懒惰更新,维持 O ( l o g n ) O(log n) O(logn)

  • 更新区间后,还需要更新叶节点这一步
  • 叶节点先不更新,我们创建一个 lazy 数组,记录未更新节点
  • 直到再碰到这个节点时,把 lazy 数组中未更新的节点进行更新

【线段树拓展】

其实我们上面的线段树,是一维线段树:


还有二维线段树:

对于二维区间查询问题,也可以使用线段树。以此类推,三维、四维等都可以。

除此之外,还有动态线段树(链式),避免数组带来的浪费。

动态线段树,是根据需要关注的区间来动态创建:

 


SQRT 分解

对比 303(SQRT 分解部分),307 多了一步 update,需要更新某个位置/区间的值,其他代码同。

SQRT 分解的单元素更新:

void update(int i, int val) 
	int b = i / B;            // 先算出对应第几组
	blocks[b] -= data[i];     // 组和 - 被更新的元素
	blocks[b] += val;         // 组合 + 更新元素
	data[i] = val;            // 单个元素更新,不能先更新,更新组合要用

完整代码:

class NumArray 
    vector<int> data;
    vector<int> blocks;  // 存储各组的和
    int N;               // 元素总数
    int B;               // 每组元素个数
    int Bn;              // 组数
    
public:
    NumArray(vector<int>& nums) 
        N = nums.size();
        if (N == 0)
            return;
        
        B = (int)sqrt(N);
        Bn = N / B + (N % B > 0 ? 1 : 0);    // 不能整除,组数+1
        
        for(auto v : nums) 
			data.push_back(v);

		blocks.assign(N, 0);
        for(int i=0; i<N; i++)       
            blocks[i / B] += nums[i];        // 求组和
    
    
    int sumRange(int left, int right) 
        int bstart = left / B;               // 起始组号
        int bend = right / B;                // 终止组号

        int res = 0;
        if( bstart == bend )                // 所求的区间和属于同一组
            for(int i=left; i<=right; i++)   // 遍历得到结果
                res += data[i];
            return res;
        

        // 所求的区间和不属于同一组,需要分成 3 组
        int x = (bstart + 1) * B;
        for(int i=left; i<x; i++)            // 头组
            res += data[i];

        for(int i=bstart+1; i<bend; i++)     // 中间
            res += blocks[i];

        for(int i=bend * B; i<=right; i ++)  // 尾组
            res += data[i];
        return res;
    

	void update(int i, int val) 
		int b = i / B;            // 先算出对应第几组
		blocks[b] -= data[i];     // 组和 - 被更新的元素
		blocks[b] += val;         // 组合 + 更新元素
		data[i] = val;            // 单个元素更新,不能先更新,更新组合要用
	
;

以上是关于[307]. 区域和检索 - 数组可修改的主要内容,如果未能解决你的问题,请参考以下文章

[307]. 区域和检索 - 数组可修改

LeetCode 307. 区域和检索 - 数组可修改

307. 区域和检索 - 数组可修改

[Leetcode] 第307题 区域和检索-数组可修改

LeetCode 307. 区域和检索 - 数组可修改

307. 区域和检索 - 数组可修改