【算法】【 LeetCode】线段树

学习

【参考:线段树 - OI Wiki

入门:

【参考:这可能是全b站最通俗易懂的线段树入门教学视频了_哔哩哔哩_bilibili

  • 单点修改,区间查询

【参考:这可能是全b站最通俗易懂的线段树入门教学视频了 P2 带标记的线段树_哔哩哔哩_bilibili】标记不下放

  • 区间修改,区间查询 没看懂

【参考:这可能是全b站最通俗易懂的线段树入门教学视频了 P3 标记下传_哔哩哔哩_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)

非动态开点


  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值