【LCP2022春】52. 二叉搜索树染色【中等】带惰性标记的线段树

题目来源于leetcode,解法和思路仅代表个人观点。传送门
难度: 中等
时间:1h44m
【带惰性标记的线段树】学习

题目

欢迎各位勇者来到力扣城,本次试炼主题为「二叉搜索树染色」。

每位勇士面前设有一个二叉搜索树的模型,模型的根节点为root,树上的各个节点值均不重复。初始时,所有节点均为蓝色。现在按顺序对这棵二叉树进行若干次操作, ops[i] = [type, x, y]表示第 i次操作为:

type等于 0 时,将节点值范围在[x, y]的节点均染蓝
type 等于 1 时,将节点值范围在[x, y]的节点均染红
请返回完成所有染色后,该二叉树中红色节点的数量。

注意:

题目保证对于每个操作的xy值定出现在二叉搜索树节点中

示例 1:

输入:root = [1,null,2,null,3,null,4,null,5], ops = [[1,2,4],[1,1,3],[0,3,5]]

输出:2

解释:
第 0 次操作,将值为 2、3、4 的节点染红;
第 1 次操作,将值为 1、2、3 的节点染红;
第 2 次操作,将值为 3、4、5 的节点染蓝;
因此,最终值为 1、2 的节点为红色节点,返回数量 2

在这里插入图片描述

示例 2:

输入:root = [4,2,7,1,null,5,null,null,null,null,6]
ops = [[0,2,2],[1,1,5],[0,4,5],[1,5,7]]

输出:5

解释:
第 0 次操作,将值为 2 的节点染蓝;
第 1 次操作,将值为 1、2、4、5 的节点染红;
第 2 次操作,将值为 4、5 的节点染蓝;
第 3 次操作,将值为 5、6、7 的节点染红;
因此,最终值为 1、2、5、6、7 的节点为红色节点,返回数量 5

在这里插入图片描述

提示:

1 <= 二叉树节点数量 <= 10^5
1 <= ops.length <= 10^5
ops[i].length == 3
ops[i][0] 仅为 0 or 1
0 <= ops[i][1] <= ops[i][2] <= 10^9
0 <= 节点值 <=10^9

思路

题意分析

题目给出了二叉搜索树TreeNode。观察可知,中序遍历得到的节点值按升序排列
本题实际上与二叉搜索树并无太大关联。我们先中序遍历将节点值存入数组nums中。

节点初始颜色为蓝色,即type==0时的颜色。
题目要求多次操作后,求红色节点的数量。

我们将所有节点的值映射到0~n-1之间( n n n为节点的数量)。实际上,题目中的 操作 就是 区间修改的问题(即,将区间中数变为同一个数)。


线段树

简单版:【LeetCode每日一题2022/04/04】307. 区域和检索 - 数组可修改【中等】线段树

什么是线段树?

线段树 s e g m e n t T r e e segmentTree segmentTree是一个二叉树。

其每个节点保存至少4个值,

  1. 当前节点的编号 n o d e node node
  2. 当前节点维护区间的左端点 s s s
  3. 当前节点维护区间的右端点 e e e
  4. 当前节点维护区间的 [最小值/最大值/总和] 等。

在这里插入图片描述

假设有数组nums = {1,2,3,4,5,6,7,8},其构建的线段树如上图。红色为维护的,圆圈内的表示编号 [ l , r ] [l,r] [l,r]表示维护的区间。


线段树可以使用数组/树实现。(这里使用数组)。
设根节点的 i d id id为0,则对于某个节点的 i d = n o d e id = node id=node,其左节点的 i d = 2 ∗ n o d e + 1 id=2*node+1 id=2node+1,其右节点的 i d = 2 ∗ n o d e + 2 id=2*node+2 id=2node+2

在叶子节点存储数组 n u m s nums nums n n n个值。(一共有 n n n个叶子节点, n − 1 n-1 n1个非叶子节点)。
由于要防止越界访问,实际数组需要开 4 n 4n 4n的空间。

线段树的复杂度

时间复杂度: O ( u log ⁡ n + q log ⁡ n + n ) O(u \log n + q \log n+n) O(ulogn+qlogn+n)。其中, n n n为原数组(叶子节点)的大小, u u u为修改次数, q q q为查询次数。
建树build O ( n ) O(n) O(n)
单间修改update O ( log ⁡ n ) O(\log n) O(logn)
区间查询query O ( log ⁡ n ) O(\log n) O(logn)

空间复杂度: O ( 4 n ) O(4n) O(4n)

为什么需要惰性标记

// 修改 index 位置的值为val。
void update(int index, int val, int node, int s, int e){
	if(index < s || index > e){
		return;
	}
    // s == e == index,单点修改
    if(s == e){
        tree[node] = val;
        return;
    }
    int m = s + (e - s) /2;
    int lnode = node*2 + 1;
    int rnode = node*2 + 2;
    update(index, val, lnode, s, m);
    update(index, val, rnode, m+1, e);
    // push_up : merge and update
    tree[node] = tree[node*2+1] + tree[node*2+2];
}

仅支持单点修改操作 的线段树中(update代码如上),如果我们需要处理区间修改问题,需要如下操作:

for(auto& op:ops){
	int val = op[0]
	int left = op[1];
	int right = op[2];
	for(int i=left;i<=right;i++){
		update(i, i, val, 0, 0, n-1);
	}
}

对于涉及到的每个点都进行修改,时间复杂度为 O ( u m log ⁡ n ) O(u m \log n) O(umlogn),其中 u u u为修改次数, m m m为区间长度, n n n为原数组(叶子节点)的大小。

很明显,时间复杂度 不理想。


同样地,

// 修改[left, right]区间的值为val。
void update(int left, int right, int val, int node, int s, int e){
    if(left > e || right < s){
        return;
    }
    if(s == e){
        tree[node] = val;
        return;
    }
    
    int m = s + (e -s)/2;
    int lnode = node*2 + 1;
    int rnode = node*2 + 2;
    update(left, right, val, lnode, s, m);
    update(left, right, val, rnode, m+1, e);
    
    tree[node] = tree[lnode] + tree[rnode];
}

支持区间修改的 线段树中(update代码如上),如果我们需要处理区间修改问题,需要如下操作:

for(auto& op:ops){
	int val = op[0]
	int left = op[1];
	int right = op[2];
	update(left, right, val, 0, 0, n-1);
}

对于涉及到的每个点都进行修改,时间复杂度为 O ( u m log ⁡ n ) O(u m \log n) O(umlogn),其中 u u u为修改次数, m m m为区间长度, n n n为原数组(叶子节点)的大小。

看上去少了一个循环,但是实际上,时间复杂度 并没有什么不同。

带惰性标记的线段树

线段树进阶(懒惰标记)
Senior Data Structure · 浅谈线段树(Segment Tree)

例子

可以看到,不带惰性标记的线段树,每次进行修改时,都修改了区间长度的 m m m个节点。

我们 引入惰性标记lazy_tag,每次进行修改时,不需要遍历完 m m m个节点。

在这里插入图片描述

比如,我们需要修改区间 [ 2 , 7 ] [2,7] [2,7]的值,全部修改为1。向下遍历,仅需要修改4、5、8、13号节点。之后向上回溯,修改0、1、2、3、6。

当修改到4、5、8、13号节点时,我们就不往下遍历其子节点了(其中8、13为 叶子节点),而是打上惰性标记lazy_tag(用黑色圆圈表示)。

为什么是4、5、8、13号节点?
因为他们维护的区间在 [ 2 , 7 ] [2,7] [2,7]之间。

有朋友可能会问,难道9、10、11、12号节点不需要修改?
其实,9、10、11、12号节点并不是不需要修改。而是需要用到的时候再修改。这就是lazy_tag的思想。


比如,我们再次修改区间 [ 2 , 3 ] [2,3] [2,3]的值,全部修改为2。

对于区间 [ 2 , 2 ] [2,2] [2,2],很简单,修改后8号节点后,向上更新父节点即可。

对与区间 [ 3 , 3 ] [3,3] [3,3]。当我们"碰到"4号节点时,我们需要将4号节点的惰性标记下传给其子节点,同时更新其子节点,即push_down函数。之后的就简单了(同8号节点的修改)。

即,lazy_tag的思想:用到再修改。

在这里插入图片描述
最终结果,
在这里插入图片描述


总结

在这里,总结一下,lazy_tag的思想。

在线段的区间修改问题中,设,我们需要修改的区间为 [ l e f t , r i g h t ] [left,right] [left,right]

当我们碰到区间 [ s , e ] [s,e] [s,e],其中left<=s && e<=e时,直接计算当前节点的结果,并打上惰性标记lazy_tag


当我们下次碰到 带惰性标记的节点 时,
如果,我们不需要遍历其子节点,就直接 返回该节点的值即可。
如果,我们需要遍历其子节点,就将 惰性标记下传一层,同时更新下一层子节点(其左右孩子),即,push_down

push_down

我们需要一个与线段树空间大小( 4 n 4n 4n)相同的数组flag来存储lazy_tag

在实际使用中,push_down在修改、查询操作,都需要进行。

void push_down(int node, int s, int e){
    if(flag[node] == -1){
        // 没有惰性标记
        return;
    }
    int m = s + (e - s) / 2;
    int lnode = node*2 + 1;
    int rnode = node*2 + 2;
    
    int tag = flag[node];
    
    // 【子节点】接替惰性标记
    flag[lnode] = tag;
    flag[rnode] = tag;
    // 更新 子节点
    tree[lnode] = tag*(m-s+1);
    tree[rnode] = tag*(e-(m+1)+1);
    
    // 重置【当前节点】惰性标记
    flag[node] = -1;
}
update
void update(int left, int right, int val, int node, int s,int e){
    if(left > e || right < s){
        return;
    }
    // [s,e]在区间[left,right]中
    if(left <= s && right >= e){
        tree[node] = val * (e-s+1);
        flag[node] = val;
        return;
    }
    push_down(node, s, e);
    
    int m = s + (e -s)/2;
    int lnode = node*2 + 1;
    int rnode = node*2 + 2;
    update(left, right, val, lnode, s, m);
    update(left, right, val, rnode, m+1, e);
    // push_up : 更新当前节点
    tree[node] = tree[lnode] + tree[rnode];
}

惰性标记的引入,使得我们在多次修改操作的时,大大减少了需要遍历的节点数量,使得修改的时间复杂度下降为 O ( u log ⁡ n ) O(u \log n) O(ulogn)

带惰性标记的线段树 复杂度

时间复杂度: O ( u log ⁡ n + q log ⁡ n + n ) O(u \log n + q \log n + n) O(ulogn+qlogn+n)。其中 u u u为修改次数, q q q为查询次数, n n n为原数组(叶子节点)的大小。
建树bulid O ( 4 n ) O(4n) O(4n)
区间修改update O ( log ⁡ n ) O(\log n) O(logn)
区间查询query O ( log ⁡ n ) O(\log n) O(logn)

空间复杂度: O ( 8 n ) O(8n) O(8n)。线段树和其惰性标记分别需要 O ( 4 n ) 。 O(4n)。 O(4n)

代码

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    vector<int> nums;
    int n;
    vector<int> tree;	// 线段树 记录 红色节点的数量
    vector<int> flag;	// lazy_tag   -1表示没有lazy_tag
    void build(int node, int s,int e, vector<int>& nums){
        if(s == e){
            tree[node] = 0;
            flag[node] = -1;
            return;
        }
        int m = (unsigned) (s+e) >> 1;  // int m = s + (e - s) / 2
        int lnode = node*2 + 1;
        int rnode = node*2 + 2;
        build(lnode, s, m, nums);
        build(rnode, m+1, e,nums);
        
        tree[node] = 0;
        flag[node] = -1;
    }
    void push_down(int node, int s, int e){
        if(flag[node] == -1){
            // 没有惰性标记
            return;
        }
        int m = s + (e - s) / 2;
        int lnode = node*2 + 1;
        int rnode = node*2 + 2;
        
        int tag = flag[node];
        
        // 【子节点】接替惰性标记
        flag[lnode] = tag;
        flag[rnode] = tag;
        // 更新 子节点
        tree[lnode] = tag*(m-s+1);
        tree[rnode] = tag*(e-(m+1)+1);
        
        // 重置【当前节点】惰性标记
        flag[node] = -1;
    }
    void update(int left, int right, int val, int node, int s,int e){
        if(left > e || right < s){
            return;
        }
        if(left <= s && right >= e){
            tree[node] = val * (e-s+1);
            flag[node] = val;
            return;
        }
        push_down(node, s, e);
        
        int m = s + (e -s)/2;
        int lnode = node*2 + 1;
        int rnode = node*2 + 2;
        update(left, right, val, lnode, s, m);
        update(left, right, val, rnode, m+1, e);
        
        tree[node] = tree[lnode] + tree[rnode];
    }
    int query(int left, int right, int node, int s, int e){
        if(left > e || right < s){
            return 0;
        }
        if(left <= s && right >= e){
            return tree[node];
        }
        push_down(node, s, e);
        int m = s + (e -s)/2;
        int lnode = node*2 + 1;
        int rnode = node*2 + 2;
        return query(left, right, lnode, s, m) + query(left, right, rnode, m+1, e);
    }
    // 中序遍历 获得 数组
    vector<int> midOrder(TreeNode* root){
        if(root == nullptr){
            return {};
        }
        vector<int> ans = midOrder(root->left);
        ans.push_back(root->val);
        auto right = midOrder(root->right);
        ans.insert(ans.end(),right.begin(),right.end());
        return ans;
    }
    int getNumber(TreeNode* root, vector<vector<int>>& ops) {
        auto nums = midOrder(root);
        unordered_map<int,int> mmap;
        int n = nums.size();
        // 映射
        for(int i=0;i<n;i++){
            mmap[nums[i]] = i;
        }
        for(auto& op:ops){
            op[1] = mmap[op[1]];
            op[2] = mmap[op[2]];
        }
        // 线段树
        this->n = n;
        this->nums = nums;
        this->tree.resize(n*4);
        this->flag.resize(n*4);
        build(0, 0, n-1, nums);
        for(auto& op:ops){
            int val = op[0];
            int left = op[1];
            int right = op[2];
            update(left, right, val,0,0,n-1);
        }
        return query(0, n-1, 0, 0, n-1);
    }
};

算法复杂度

时间复杂度: O ( m log ⁡ n ) O(m \log n) O(mlogn)。其中, m m m o p s ops ops数组的大小, n n n为二叉树节点的数量。
空间复杂度: O ( 4 n + 4 n ) O(4n+4n) O(4n+4n)

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值