LCP 52. 二叉搜索树染色
题目来源于leetcode,解法和思路仅代表个人观点。传送门。
难度: 中等
时间:1h44m
【带惰性标记的线段树】学习
题目
欢迎各位勇者来到力扣城,本次试炼主题为「二叉搜索树染色」。
每位勇士面前设有一个二叉搜索树的模型,模型的根节点为root
,树上的各个节点值均不重复。初始时,所有节点均为蓝色。现在按顺序对这棵二叉树进行若干次操作, ops[i] = [type, x, y]
表示第 i
次操作为:
type
等于 0 时,将节点值范围在[x, y]
的节点均染蓝
type
等于 1 时,将节点值范围在[x, y]
的节点均染红
请返回完成所有染色后,该二叉树中红色节点的数量。
注意:
题目保证对于每个操作的x
、y
值定出现在二叉搜索树节点中
示例 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为节点的数量)。实际上,题目中的 操作 就是 区间修改的问题(即,将区间中数变为同一个数)。
线段树
什么是线段树?
线段树 s e g m e n t T r e e segmentTree segmentTree是一个二叉树。
其每个节点保存至少4个值,
- 当前节点的编号 n o d e node node。
- 当前节点维护区间的左端点 s s s。
- 当前节点维护区间的右端点 e e e。
- 当前节点维护区间的 [最小值/最大值/总和] 等。
假设有数组
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=2∗node+1,其右节点的
i
d
=
2
∗
n
o
d
e
+
2
id=2*node+2
id=2∗node+2。
在叶子节点存储数组 n u m s nums nums的 n n n个值。(一共有 n n n个叶子节点, n − 1 n-1 n−1个非叶子节点)。
由于要防止越界访问,实际数组需要开 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为原数组(叶子节点)的大小。
看上去少了一个循环,但是实际上,时间复杂度 并没有什么不同。
带惰性标记的线段树
例子
可以看到,不带惰性标记的线段树,每次进行修改时,都修改了区间长度的 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)。