学习
【参考:线段树 - OI Wiki】
入门:
【参考:这可能是全b站最通俗易懂的线段树入门教学视频了_哔哩哔哩_bilibili】
- 单点修改,区间查询
【参考:这可能是全b站最通俗易懂的线段树入门教学视频了 P2 带标记的线段树_哔哩哔哩_bilibili】标记不下放
- 区间修改,区间查询 没看懂
看这个,这个容易懂
【参考:线段树入门_哔哩哔哩_bilibili】
【参考:线段树进阶_哔哩哔哩_bilibili】区间修改、查询+lazy
线段树入门:https://wmathor.com/index.php/archives/1175/
线段树进阶:https://wmathor.com/index.php/archives/1176/
import java.io.InputStreamReader;
import java.util.Scanner;
class Node {
int l, r;
long sum; // 相当于该节点的val
long inc;
}
public class Main {
static Node[] n;
static long[] t;
static long SUM; // 全局变量[l,r]区间和
public static void main(String[] args) {
Scanner cin = new Scanner(new InputStreamReader(System.in));
int N = cin.nextInt();
int M = cin.nextInt();
n = new Node[4 * N];
t = new long[N + 1];
for (int i = 1; i <= N; i++)
t[i] = cin.nextLong();
make(1, N, 0); // 数的范围为[1,n]
for (int i = 0; i < M; i++) {
String op = cin.next();
int a = cin.nextInt();
int b = cin.nextInt();
if ("Q".equals(op)) {
SUM = 0;
query(a, b, 0);// 查询[a,b]区间和,从0号节点(即根节点)开始查
System.out.println(SUM);
} else { // "C".equals(op)
int c = cin.nextInt();
update(a, b, c, 0);
}
}
}
static void update(int l, int r, int c, int idx) {
if (l <= n[idx].l && r >= n[idx].r) { // [l,r]包括了n[idx]节点区间
n[idx].sum += (n[idx].r - n[idx].l + 1) * c; // 区间长度 * c
n[idx].inc += c; // 打上标记,表明其所有叶子节点都需要 +c
return;
}
if (n[idx].inc != 0)
pushDown(idx);
int mid = (n[idx].l + n[idx].r) >> 1;
if (r <= mid)
update(l, r, c, (idx << 1) | 1); // 更新左子树
else if (l > mid)
update(l, r, c, (idx << 1) + 2); // 更新右子树
else {
// 左右子树都要更新
update(l, r, c, (idx << 1) | 1);
update(l, r, c, (idx << 1) + 2);
}
pushUp(idx); // 修改完子树后,向上更新上父节点idx的值
}
// 从idx下推到左右子树
static void pushDown(int idx) {
int mid = (n[idx].l + n[idx].r) >> 1;
n[(idx << 1) | 1].sum += (mid - n[idx].l + 1) * n[idx].inc;
n[(idx << 1) + 2].sum += (n[idx].r - mid) * n[idx].inc;
// 把标记下推给孩子节点
n[(idx << 1) | 1].inc += n[idx].inc;
n[(idx << 1) + 2].inc += n[idx].inc;
// 取消当前节点标记
n[idx].inc = 0;
}
static void query(int l, int r, int idx) {
if (l <= n[idx].l && r >= n[idx].r) // [l,r]包括了n[idx]节点区间,就不用继续往下走了
SUM += n[idx].sum;
else {
if (n[idx].inc != 0)
pushDown(idx);
int mid = (n[idx].l + n[idx].r) >> 1;
if (r <= mid)
query(l, r, (idx << 1) | 1);
else if (l > mid)
query(l, r, (idx << 1) + 2);
else {
query(l, r, (idx << 1) | 1);
query(l, r, (idx << 1) + 2);
}
}
}
static void make(int l, int r, int idx) {
n[idx] = new Node();
n[idx].l = l;
n[idx].r = r;
if (l == r)
n[idx].sum = t[r];
else {
make(l, (l + r) >> 1, (idx << 1) | 1); // 左子树
make(((l + r) >> 1) + 1, r, (idx << 1) + 2); // 右子树
pushUp(idx); // 更新上面两个子树的父节点
}
}
static void pushUp(int idx) {
n[idx].sum = n[(idx << 1) | 1].sum + n[(idx << 1) + 2].sum;
}
}
P3374 【模板】树状数组 1
【参考:P3374 【模板】树状数组 1 - 洛谷 | 计算机科学教育新生态】
需使用快读快写模版才能全部通过
package luogu;
import java.io.*;
import java.util.Scanner;
class Node {
// Node left, right; // 左右孩子节点 一般不使用Node[]才会用到
int start, end; // 节点表示的区间[start,end]
int val; // 节点的值
int lazy; // lazy标记
}
public class Main {
static Node[] node;
static int[] arr;
static int SUM = 0;
public static void main(String[] args) throws IOException {
// Scanner sc = new Scanner(new InputStreamReader(System.in));
StreamTokenizer in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
// int n = sc.nextInt();
// int m = sc.nextInt();
in.nextToken();
int n=(int) in.nval;
in.nextToken();
int m=(int) in.nval;
arr = new int[n+1];
// 注意 node[idx].val = arr[l];
// node[idx]表示区间[1,1]对应的是第一个数,为了对齐下标这里选择从1开始
for (int i = 1; i <= n; i++) {
// arr[i] = sc.nextInt();
in.nextToken();
arr[i]=(int) in.nval;
}
node = new Node[4 * n]; // 4*n
build(1, n, 0); // 数据范围为[1,n] ,第一个根节点的下标为0
// 注意:节点的下标与节点表示的区间没有联系
for (int i = 0; i < m; i++) {
// int t = sc.nextInt();
// int x = sc.nextInt();
// int y = sc.nextInt();
in.nextToken();
int t=(int) in.nval;
in.nextToken();
int x=(int) in.nval;
in.nextToken();
int y=(int) in.nval;
if (t == 1) {
update(x, x, y, 0); // 从根节点开始使[x,x]区间的值+y
} else {
SUM = 0;
query(x, y, 0);
// System.out.println(SUM);
out.println(SUM);
}
}
out.flush();
}
// 从node[idx]开始构建范围为[l,r]的线段树
static void build(int l, int r, int idx) {
node[idx] = new Node();
node[idx].start = l;
node[idx].end = r;
if (l == r) {
node[idx].val = arr[l];
} else {
int mid = l + (r - l) / 2;
build(l, mid, idx * 2 + 1);
build(mid + 1, r, idx * 2 + 2);
pushUp(idx); // 更新父节点的值
}
}
// 从node[idx]开始把[l,r]区间内的值+val
static void update(int l, int r, int val, int idx) {
if (l <= node[idx].start && node[idx].end <= r) { // [l,r]包括了node[idx]节点区间
node[idx].val += (node[idx].end - node[idx].start + 1) * val; // 区间节点加上更新值:val*该子树所有叶子节点
node[idx].lazy += val;// 添加懒惰标记
return;
}
if (node[idx].lazy != 0)
pushDown(idx);
int mid = node[idx].start + (node[idx].end - node[idx].start) / 2;
if (r <= mid)
update(l, r, val, idx * 2 + 1); // 更新左子树
else if (l > mid)
update(l, r, val, idx * 2 + 2); // 更新右子树
else {
// 左右子树都要更新
update(l, r, val, idx * 2 + 1);
update(l, r, val, idx * 2 + 2);
}
pushUp(idx);// 修改完子树后,向上更新上父节点idx的值
}
// 从node[idx]开始查询[l,r]区间内的总和
static void query(int l, int r, int idx) {
if (l <= node[idx].start && node[idx].end <= r) { // [l,r]包括了node[idx]节点区间,就不用继续往下走了
SUM += node[idx].val;
return;
}
if (node[idx].lazy != 0)
pushDown(idx);
int mid = node[idx].start + (node[idx].end - node[idx].start) / 2;
if (r <= mid)
query(l, r, idx * 2 + 1); // 查询左子树
else if (l > mid)
query(l, r, idx * 2 + 2); // 查询右子树
else {
// 左右子树都要查询
query(l, r, idx * 2 + 1);
query(l, r, idx * 2 + 2);
}
}
// 从node[idx]下推到左右子树
static void pushDown(int idx) {
int mid = node[idx].start + (node[idx].end - node[idx].start) / 2;
// 左子树节点的值+=左子树叶子节点数 * 父节点的lazy值
node[idx * 2 + 1].val += (mid - node[idx].start + 1) * node[idx].lazy;
// 右子树节点的值+=右子树叶子节点数 * 父节点的lazy值
node[idx * 2 + 2].val += (node[idx].end - mid) * node[idx].lazy;
// 把lazy标记下推给左右子树
node[idx * 2 + 1].lazy += node[idx].lazy;
node[idx * 2 + 2].lazy += node[idx].lazy;
// 取消父节点的lazy标记
node[idx].lazy = 0;
}
static void pushUp(int idx) {
node[idx].val = node[idx * 2 + 1].val + node[idx * 2 + 2].val;
}
}
总结
【参考:关于各类「区间和」问题如何选择解决方案(含模板) - 区域和检索 - 数组可修改 - 力扣(LeetCode)】
【参考:线段树详解「汇总级别整理 🔥🔥🔥」 - 我的日程安排表 I - 力扣(LeetCode)】动态开点模板
中等
307. 区域和检索 - 数组可修改 ***
【参考:307. 区域和检索 - 数组可修改 - 力扣(LeetCode)】
【参考:【专题讲解】 线段树的典型应用 leetcode 307 Range Sum Query - Mutable_哔哩哔哩_bilibili】
- 线段树(修改&&求区间和)
TreeNode 节点范围是[start,end]
sum是指[start, end]区间和
class NumArray {
// 线段树节点定义
class TreeNode {
int start, end; // 节点区间
TreeNode left, right;
int sum; // [start, end]区间和
public TreeNode(int start, int end) {
this.start = start;
this.end = end;
}
}
// 线段树根节点
TreeNode root = null;
public NumArray(int[] nums) {
root = buildTree(nums, 0, nums.length - 1);
}
public void update(int index, int val) {
update(root, index, val);
}
public int sumRange(int left, int right) {
return query(root, left, right);
}
// 建树
TreeNode buildTree(int[] nums, int start, int end) {
if (start > end) return null;
if (start == end) {
TreeNode node = new TreeNode(start, end);
node.sum = nums[start];
return node;
} else {
TreeNode node = new TreeNode(start, end);
int mid = start + (end - start) / 2;
node.left = buildTree(nums, start, mid);
node.right = buildTree(nums, mid + 1, end);
node.sum = node.left.sum + node.right.sum;
return node;
}
}
// 将 nums[index] 的值 更新 为 val,并更新和
void update(TreeNode root, int index, int val) {
if (root.start == root.end) { // 叶子节点
root.sum = val;
return;
} else {
int mid = root.start + (root.end - root.start) / 2;
if (index <= mid)
update(root.left, index, val); // 左子树
else
update(root.right, index, val); // 右子树
root.sum = root.left.sum + root.right.sum;
}
}
// 查询[left,right]之间的和
int query(TreeNode root, int left, int right) {
if (root.start == left && root.end == right) {
return root.sum;
} else {
int mid = root.start + (root.end - root.start) / 2;
if (right <= mid)
return query(root.left, left, right);// 左子树
else if (left > mid)
return query(root.right, left, right);// 右子树
else
return query(root.left, left, mid)
+ query(root.right, mid + 1, right);
}
}
}
官方解【参考:区域和检索 - 数组可修改 - 区域和检索 - 数组可修改 - 力扣(LeetCode)】
根节点的下标为0,范围为[0,n-1]
int node, int s, int e
表示节点的下标为node,区间为[s,e]
class NumArray {
private int[] segmentTree; // 存放节点所表示区间[s,e]的和
private int n;
public NumArray(int[] nums) {
n = nums.length;
segmentTree = new int[nums.length * 4]; // 要开4*n
// 0 根节点在segmentTree数组的下标
// 0,n-1:nums数组中的位置
build(0, 0, n - 1, nums);
}
public void update(int index, int val) {
// 从根节点开始在[0,n-1]内把nums[index]的值改为val
change(index, val, 0, 0, n - 1);
}
public int sumRange(int left, int right) {
// 从根节点开始在[0,n-1]内算出[left,right]的和
return range(left, right, 0, 0, n - 1);
}
// 当前节点的下标为node
// s:start,e:end
private void build(int node, int s, int e, int[] nums) {
if (s == e) {
segmentTree[node] = nums[s];
return;
}
int m = s + (e - s) / 2;
build(node * 2 + 1, s, m, nums);
build(node * 2 + 2, m + 1, e, nums);
// 下标为node的根节点的和
segmentTree[node] = segmentTree[node * 2 + 1] + segmentTree[node * 2 + 2];
}
// 当前节点的下标为node
// 在[s,e]中把下标为index的节点的值改为val
private void change(int index, int val, int node, int s, int e) {
if (s == e) {
segmentTree[node] = val;
return;
}
int m = s + (e - s) / 2;
if (index <= m) {
change(index, val, node * 2 + 1, s, m);
} else {
change(index, val, node * 2 + 2, m + 1, e);
}
// 该节点的和为左右子树的和相加
segmentTree[node] = segmentTree[node * 2 + 1] + segmentTree[node * 2 + 2];
}
// 当前节点的下标为node
// 在[s,e]中求出[left,right]的和
private int range(int left, int right, int node, int s, int e) {
if (left == s && right == e) {
return segmentTree[node];
}
int m = s + (e - s) / 2;
if (right <= m) {
return range(left, right, node * 2 + 1, s, m);
} else if (left > m) {
return range(left, right, node * 2 + 2, m + 1, e);
} else {
// 跨越两段
// 在[s,m]中求[left,m]
return range(left, m, node * 2 + 1, s, m)
+ range(m + 1, right, node * 2 + 2, m + 1, e);
// 在[m + 1, e]中求[m + 1, right]
}
}
}
【参考:线段树详解「汇总级别整理 🔥🔥🔥」 - 区域和检索 - 数组可修改 - 力扣(LeetCode)】
动态开点模板
区间修改(使用标记add)不需要提前建树
node节点范围是[start,end]
int node 是指该节点的下标为node
class NumArray {
public NumArray(int[] nums) {
N = nums.length - 1;
for (int i = 0; i <= N; i++) {
update(root, 0, N, i, i, nums[i]);
}
}
public void update(int index, int val) {
// 更新[index,index] 就相当于只更新一个节点了
update(root, 0, N, index, index, val);
}
public int sumRange(int left, int right) {
return query(root, 0, N, left, right);
}
class Node {
// 左右孩子节点
Node left, right;
// 当前节点值,以及懒惰标记的值
int val, add;
}
private int N;
private Node root = new Node();
// 更新[l,r]
public void update(Node node, int start, int end, int l, int r, int val) {
// 找到满足要求的区间
if (l <= start && end <= r) {
// 区间节点加上更新值
// 注意:需要*该子树所有叶子节点
node.val = (end - start + 1) * val;
// 添加懒惰标记
// 对区间进行「加减」的更新操作,懒惰标记需要累加,不能直接覆盖
node.add = val;
return ;
}
int mid = (start + end) >> 1;
// 下推标记
// mid - start + 1:表示左孩子区间叶子节点数量
// end - mid:表示右孩子区间叶子节点数量 end-(mid+1)-1 = end - mid
pushDown(node, mid - start + 1, end - mid);
// [start, mid] 和 [l, r] 可能有交集,遍历左孩子区间
if (l <= mid)
update(node.left, start, mid, l, r, val);
// [mid + 1, end] 和 [l, r] 可能有交集,遍历右孩子区间
if (r > mid)
update(node.right, mid + 1, end, l, r, val);
// 向上更新
pushUp(node);
}
// 查询[l,r]
public int query(Node node, int start, int end, int l, int r) {
if (l <= start && end <= r)
return node.val;
int mid = (start + end) >> 1;
int ans = 0;
pushDown(node, mid - start + 1, end - mid);
if (l <= mid)
ans += query(node.left, start, mid, l, r);
if (r > mid)
ans += query(node.right, mid + 1, end, l, r);
// ans 把左右子树的结果都累加起来了,与树的后续遍历同理
return ans;
}
/* 上式也可以这样写
if (r <= mid)
return query(node.left, start, mid, l, r);
else if (l > mid)
return query(node.right, mid + 1, end, l, r);
else
return query(node.left, start, mid, l, r)
+query(node.right, mid + 1, end, l, r);
*/
private void pushUp(Node node) {
node.val = node.left.val + node.right.val;
}
// leftNum 和 rightNum 表示左右孩子区间的叶子节点数量
// 因为如果是「加减」更新操作的话,需要用懒惰标记的值*叶子节点的数量
private void pushDown(Node node, int leftNum, int rightNum) {
// 动态开点
if (node.left == null) node.left = new Node();
if (node.right == null) node.right = new Node();
// 如果 add 为 0,表示没有标记
if (node.add == 0) return ;
// 注意:当前节点加上标记值*该子树所有叶子节点的数量
node.left.val = node.add * leftNum;
node.right.val = node.add * rightNum;
// 把标记下推给孩子节点
// 对区间进行「加减」的更新操作,下推懒惰标记时需要累加起来,不能直接覆盖
node.left.add = node.add;
node.right.add = node.add;
// 取消当前节点标记
node.add = 0;
}
}
729. 我的日程安排表 I
【参考:729. 我的日程安排表 I - 力扣(LeetCode)】
【参考:我的日程安排表 I - 我的日程安排表 I - 力扣(LeetCode)】
非动态开点