leetcode|刷算法 线段树原理以及模板

线段树出现的题目特征

线段树使用的题目。每次操作都要得到返回结果的。

比如

699. 掉落的方块 - 力扣(LeetCode)

2286. 以组为单位订音乐会的门票 - 力扣(LeetCode)

1845. 座位预约管理系统 - 力扣(LeetCode)

线段树的原理

线段树的原理就是使用递归的思想对数值进行更新。

比如查询6到10区间上的最大值。如果传统的需要遍历一遍。但是如果有一个node刚好记录了这个范围6到10上的最大值,那么直接将这里的node上的数值返回即可。

另外,除了最大值,还有一个范围上的累加和,使得一个范围上进行更新操作,一个范围上进行累加操作。

原数组 orginArr

范围1到n上,需要使用4*(n+1) 的长度来记录树 使用arrSum表示

节点使用node标识,范围左边是left,右边是right

根节点就是 node=1 left=1 right=n

任意节点的左节点就是 node*2 左边边界 left 右边边界 right/2

任意节点的右节点就是 node*2+1 左边边界 right/2+1 右边边界 right

当左边界,右边界相同时候。left也就是1-n范围上的这个值对应的 arrSum[node] 就是表示原数组的该值origin[left]

线段树的模板

模板改自左程云老师的模板,属于二次改造,就高级写法进行降级,使得易懂。对单字母的变量进行多个字母命名,使得看着没有这么眼花


    public static void main(String[] args) {
        int[] origin = {2, 1, 1, 2, 3, 4, 5};
        SegmentTree seg = new SegmentTree(origin);
        int start = 2;
        int end = 5;
        int value = 4;
        seg.add(start, end, value);  // start到end区间上统一增加值
        seg.update(start, end, value);  //  start到end区间上统一更新
        long sum = seg.querySum(start, end);  //  start到end区间求和
    }

这里入参会看着简洁些。start,end,value 也容易理解

另外的参数是left,right,node。

对于维护的数组,sumArr 树节点上的累加和 ,taskArr 树节点上的需要下发的增加数值的任务,changeArr 树节点上的需要下发的修改任务

taskArr[node] = 5 node的left和right。比如为6和10,也就是范围6到10上,都需要增加10

模板如下
public static void main(String[] args) {
    int[] origin = {2, 1, 1, 2, 3, 4, 5};
    SegmentTree seg = new SegmentTree(origin);
    int start = 2;
    int end = 5;
    int value = 4;
    seg.add(start, end, value);  // start到end区间上统一增加值
    seg.update(start, end, value);  //  start到end区间上统一更新
    long sum = seg.querySum(start, end);  //  start到end区间求和
    System.out.println(sum);
    System.out.println("对数器测试开始...");
    System.out.println("测试结果 : " + (test() ? "通过" : "未通过"));

}

// 线段树模板
public static class SegmentTree {
    int right;
    int left = 1;
    int[] arr;
    int[] sumArr;
    int[] taskArr;
    int[] changeArr;
    boolean[] updateArr;

    public SegmentTree(int[] origin) {
        right = origin.length;
        int length = right + 1;
        arr = new int[length];
        for (int i = 1; i < length; i++) {
            arr[i] = origin[i - 1];
        }
        sumArr = new int[length * 4];
        taskArr = new int[length * 4];
        // changeArr和updateArr是一起使用的,但是一般题目可以不需要updateArr,因为通常修改的数值是大于0的。可以用是否为0来判断
        changeArr = new int[length * 4];
        updateArr = new boolean[length * 4];
        // 构建线段树的 SumArr
        buildSumArr(left, right, 1);
    }

    public void buildSumArr(int left, int right, int node) {
        if (left == right) { // 一定要拆解到单个节点的时候才可以进行赋值
            sumArr[node] = arr[left];
            return;
        }
        int mid = (left + right) / 2;
        buildSumArr(left, mid, node * 2); // 左节点
        buildSumArr(mid + 1, right, node * 2 + 1); // 右节点
        // 求左右节点的和
        sumArr[node] = sumArr[node * 2] + sumArr[node * 2 + 1];
    }



    private void refresh(int root, int leftNum, int rightNum) {
        // update和change必须是一起出现的
        // task是单独出现的,而且上面操作的会将task给覆盖掉

        if (updateArr[root]) {  // 它以及下面的树需要更新
            updateArr[root * 2] = true;
            updateArr[root * 2 + 1] = true;

            changeArr[root * 2] = changeArr[root]; // 需要更新的值
            changeArr[root * 2 + 1] = changeArr[root]; // 需要更新的值

            taskArr[root * 2] = 0; // 既然都更新了,那么lazy的值就可以清空了
            taskArr[root * 2 + 1] = 0;

            sumArr[root * 2] = changeArr[root] * leftNum; // 更新求和信息
            sumArr[root * 2 + 1] = changeArr[root] * rightNum; // 更新求和信息
            updateArr[root] = false;  // 自身更新好了
        }
        if (taskArr[root] != 0) {     // 这个值是需要下发的值
            taskArr[root * 2] += taskArr[root]; // 自身需要下发的值,加上来自上面的值
            sumArr[root * 2] += taskArr[root] * leftNum; // 结算上面下发的值

            taskArr[root * 2 + 1] += taskArr[root];
            sumArr[root * 2 + 1] += taskArr[root] * rightNum;  // 同上
            taskArr[root] = 0;  // 需要下发的值更新
        }
    }

    public void update(int start, int end, int value) {
            // 这里的left,right,1非常重要,1代表的是节点的位置,而left和right代表的是改节点的管辖范围。这里是root开始,root为1
            // int left, int right, int node  代表这个节点的范围,每次会更新传递
            // 这里的left和right最终会聚合到一起,代表的是arr的位置
            update(start, end, value, left, right, 1);
        }

        public void update(int start, int end, int value, int left, int right, int node) {
            // 满足更新的条件
            if (start <= left && right <= end) {
                updateArr[node] = true;  // 它以及下面的树需要更新
                changeArr[node] = value;  //更新数字
                sumArr[node] = value * (right - left + 1); // 求和
                taskArr[node] = 0;
                return;
            }
            // 任务下发。
            int mid = (left + right) / 2;
            // 左树的个数,和右树的个数
            refresh(node, mid - left + 1, right - mid);
            if (start <= mid) {
                update(start, end, value, left, mid, node * 2);
            }
            if (end > mid) {
                update(start, end, value, mid + 1, right, node * 2 + 1);
            }
            sumArr[node] = sumArr[node * 2] + sumArr[node * 2 + 1];
        }

        // 添加
        public void add(int start, int end, int value) {
            add(start, end, value, left, right, 1);
        }

        public void add(int start, int end, int value, int left, int right, int node) {
            // 开始点和结束点,把树的左右范围给包括了
            if (start <= left && right <= end) {
                sumArr[node] += value * (right - left + 1); // 这里的结算逻辑非常重要,会根据不同的题目进行变换
                taskArr[node] += value;
                return;
            }
            int mid = (left + right) / 2;
            // 这里最要是对于changeArr和taskArr的操作
            refresh(node, mid - left + 1, right - mid);

            if (start <= mid) { // 如果开始位置比mid大,那么左边就没有下发的意义了,只要下发右边就行了
                add(start, end, value, left, mid, node * 2); // 左节点 这里和初始化的策略一样
            }
            if (end > mid) {
                add(start, end, value, mid + 1, right, node * 2 + 1); // 右节点
            }
            sumArr[node] = sumArr[node * 2] + sumArr[node * 2 + 1];
        }

        public long querySum(int start, int end) {
            return querySum(start, end, left, right, 1);
        }

        public long querySum(int start, int end, int left, int right, int node) {
            // 这里是返回当前节点,因为start和end已经被包围了。
            if (start <= left && right <= end) {
                return sumArr[node];
            }
            int mid = (left + right) / 2;
            refresh(node, mid - left + 1, right - mid);
            long ans = 0;
            if (start <= mid) { // 下发左边的
                ans += querySum(start, end, left, mid, node * 2);
            }
            if (end > mid) { // 下发右边的
                ans += querySum(start, end, mid + 1, right, node * 2 + 1);
            }
            return ans;
        }
    }
//=============== 下面是用于对比测试的代码

    public static class Right {
        public int[] arr;

        public Right(int[] origin) {
            arr = new int[origin.length + 1];
            for (int i = 0; i < origin.length; i++) {
                arr[i + 1] = origin[i];
            }
        }

        public void update(int L, int R, int C) {
            for (int i = L; i <= R; i++) {
                arr[i] = C;
            }
        }

        public void add(int L, int R, int C) {
            for (int i = L; i <= R; i++) {
                arr[i] += C;
            }
        }

        public long query(int L, int R) {
            long ans = 0;
            for (int i = L; i <= R; i++) {
                ans += arr[i];
            }
            return ans;
        }

    }


    public static int[] genarateRandomArray(int len, int max) {
        int size = (int) (Math.random() * len) + 1;
        int[] origin = new int[size];
        for (int i = 0; i < size; i++) {
            origin[i] = (int) (Math.random() * max) - (int) (Math.random() * max);
        }
        return origin;
    }

    public static boolean test() {
        int len = 100;
        int max = 1000;
        int testTimes = 5000;
        int addOrUpdateTimes = 1000;
        int queryTimes = 500;
        for (int i = 0; i < testTimes; i++) {
            int[] origin = genarateRandomArray(len, max);
            SegmentTree seg = new SegmentTree(origin);
            int N = origin.length;
            Right rig = new Right(origin);
            for (int j = 0; j < addOrUpdateTimes; j++) {
                int num1 = (int) (Math.random() * N) + 1;
                int num2 = (int) (Math.random() * N) + 1;
                int L = Math.min(num1, num2);
                int R = Math.max(num1, num2);
                int C = (int) (Math.random() * max) - (int) (Math.random() * max);
                if (Math.random() < 0.5) {
                    seg.add(L, R, C);
                    rig.add(L, R, C);
                } else {
                    seg.update(L, R, C);
                    rig.update(L, R, C);
                }
            }
            for (int k = 0; k < queryTimes; k++) {
                int num1 = (int) (Math.random() * N) + 1;
                int num2 = (int) (Math.random() * N) + 1;
                int L = Math.min(num1, num2);
                int R = Math.max(num1, num2);
                long ans1 = seg.querySum(L, R);
                long ans2 = rig.query(L, R);

                if (ans1 != ans2) {
                    System.out.println(k);
                    System.out.println(ans1 + "   " + ans2);
                    return false;
                }
            }
        }
        return true;
    }
总结

个人练习了3道线段树的题目。写出来非常不容易,带着模板都没这么简单可以做出来

更多的是要学会这里递归的思想,做到灵活运用。像是2286题目 和区间没有关系,每次都是需要细化到单点。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值