数据结构(六)线段树

1 线段树

1.1 什么是线段树

线段树是一种二叉搜索树,它的每个节点保存一条线段(即数组的一段子数组)

1.2 作用

用于高效解决连续区间的动态查询问题

1.3 特点

  • 时间复杂度为O(logN)
  • 未优化的空间复杂度为2N

1.4 节点

线段树的每个节点表示一个区间,子节点则分别表示父节点的左右半区间。例如父亲的区间是[ a , b ],那么 c = ( a + b ) / 2 左儿子的区间是 [ a , c ],右儿子的区间是[ c + 1 , b ]。

1.5 延迟标记

区间更新是指更新某个区间内的叶子节点的值,因为涉及到的叶子节点不止一个,而叶子节点会影响其相应的非叶父节点,那么回溯需要更新的非叶子节点也会有很多,如果一次性更新完,操作的时间复杂度肯定不是O(logn),例如当我们要更新区间[0,3]内的叶子节点时,需要更新除了叶子节点3,9外的所有其他节点。为此引入了线段树中的延迟标记概念,这也是线段树的精华所在。
延迟标记:每个节点新增加一个标记,记录这个节点是否进行了某种修改(这种修改操作会影响其子节点),对于任意区间的修改,我们先按照区间查询的方式将其划分成线段树中的节点,然后修改这些节点的信息,并给这些节点标记上代表这种修改操作的标记。在修改和查询的时候,如果我们到了一个节点p,并且决定考虑其子节点,那么我们就要看节点p是否被标记,如果有,就要按照标记修改其子节点的信息,并且给子节点都标上相同的标记,同时消掉节点p的标记。

引用自《一步一步理解线段树》

2 示例

给定一个数组 {1,5,2,7,8,0,6,9},数组长度为 8,数据类型为 int,将其存储在一棵线段树中。
注意:节点表示的区间指的是数组下标,而非数组中存储的值

这里写图片描述

其中:
黑色数字代表节点编号
红色数字代表数组下标
蓝色数字代表存储的数据内容

可以看到节点编号与数组下标之间差为固定值 7,对于长度为 n 的数组,这个差值为固定值 n-1,子节点与父节点的节点编号也存在关系(若一个节点编号为 x,则其左儿子编号为 2x,右儿子编号为 2x+1)。然而,在实际建树过程中,可能由于采用不是广度遍历的方式,节点编号与图中顺序不符,节点编号与数组下标很可能不固定,子节点与父节点编号的关系也不确定,要根据具体实现方式确定,或者直接在节点内存储左右儿子的节点编号,以便查询或修改时进行遍历。

3 用线段树解决实际问题

3.1 问题描述

农民约翰试图通过让他们玩智力玩具让奶牛保持敏锐。较大的玩具之一是谷仓里的灯。N( 2 ± N × 100000 )个畜栏依次编号为 1…N,上方有都一盏色彩斑斓的灯。

傍晚时分,所有的灯都熄灭了。奶牛用一组按钮开关( N 个)控制灯的开关,按开关 i 可以改变编号为 i 的灯的状态,从关到开或从开到关。

奶牛读取并执行 m( 1 ± m × 100000 )个操作的列表,每个操作用 0 或 1 表示。

操作一(用 0 表示)包括两个整数 Si 和 Ei( 1 ≤ Si ≤ Ei ≤ N ),要求奶牛把范围在 Si 到 Ei 的开关全部按一次。

操作二(用 1 表示)要求奶牛计算两个整数 Si 和 Ei( 1 ≤ Si ≤ Ei ≤ N )给出的范围内有多少盏灯亮着。

帮助 FJ 确保奶牛通过处理列表并产生正确的计数得到正确的答案。

3.2 输入&输出

输入
第 1 行:两个空间分离的整数 n 和 m
第 2 行…M + 1:每行有 3 个整数,代表一次操作,三个整数分别为:操作、Si 和 Ei。

输出
第 1 行…查询数:对于每一个查询操作,在单行上打印亮着的灯的数目。

示例
n = 4,m = 5。对于每一盏灯,O 代表关,* 代表开
初始:O O O O
0 1 2 –>* * O O , 改变灯 1 到 2 的状态
0 2 4 –>* O * *
1 2 3 –>1 , 计算灯 2 到 3 中亮着的数目
0 2 4 –>* * O O
1 1 4 –>2

3.3 代码

节点定义

package r4.p2525;

class Node {
    int value;  //叶子节点:1表示灯亮着,0表示灯不亮;非叶子节点:表示亮着的灯的数目
    int l;      //节点表示的区间左边界
    int r;      //节点表示的区间右边界
    int lc;     //左儿子的节点编号
    int rc;     //右儿子的节点编号
    int tag;    //延迟标记,记录节点是否被修改

    Node(int l, int r, int value, int tag) {
        this.l = l;
        this.r = r;
        this.value = value;
        this.tag = tag;
    }
}

线段树实现

package r4.p2525;

class SegTree {
    private Node[] nodes;   //节点数组
    private int total = 0;  //目前节点总数

    SegTree(int size) {
        nodes = new Node[4 * size + 1];
    }

    /**
     * 建立线段树或其子树(对应数组下标为l~r)
     *
     * @param l 范围左边界
     * @param r 范围右边界
     */
    void build(int l, int r) {
        total++;
        int now = total;
        nodes[total] = new Node(l, r, 0, 0);

        if (l == r) {
            return;
        }
        int mid = (l + r) / 2;
        nodes[now].lc = total + 1;
        build(l, mid);
        nodes[now].rc = total + 1;
        build(mid + 1, r);
    }


    /**
     * 修改l~r范围内的数组值(使用延迟标记)
     *
     * @param l         范围左边界
     * @param r         范围右边界
     * @param nodeIndex 要修改的节点编号
     */
    void change(int l, int r, int nodeIndex) {
        int nodeL = nodes[nodeIndex].lc;
        int nodeR = nodes[nodeIndex].rc;
        if (nodes[nodeIndex].l == l && nodes[nodeIndex].r == r) {
            changeValue(nodeIndex);
            changeTag(nodeIndex);
        } else {
            if (nodes[nodeIndex].tag == 1) {   //如果节点修改了,则修改其所有子节点
                update(nodeIndex);
            }
            int mid = getMid(nodeIndex);
            if (mid >= r) {
                change(l, r, nodeL);
            } else if (mid < l) {
                change(l, r, nodeR);
            } else {
                change(l, mid, nodeL);
                change(mid + 1, r, nodeR);
            }
            nodes[nodeIndex].value = nodes[nodeL].value + nodes[nodeR].value;
        }
    }

    /**
     * 查询l~r范围内的亮灯数
     *
     * @param l         范围左边界
     * @param r         范围右边界
     * @param nodeIndex 要查询的节点编号
     * @return
     */
    int query(int l, int r, int nodeIndex) {
        if (nodes[nodeIndex].l == l && nodes[nodeIndex].r == r) {
            return nodes[nodeIndex].value;
        }
        int nodeL = nodes[nodeIndex].lc;
        int nodeR = nodes[nodeIndex].rc;
        if (nodes[nodeIndex].tag == 1) {
            update(nodeIndex);
        }

        int mid = getMid(nodeIndex);
        if (mid >= r) {
            return query(l, r, nodeL);
        } else if (mid < l) {
            return query(l, r, nodeR);
        } else {
            return query(l, mid, nodeL) + query(mid + 1, r, nodeR);
        }
    }

    /**
     * 去除节点的延迟标记,并修改其子节点
     *
     * @param nodeIndex 节点编号
     */
    private void update(int nodeIndex) {
        int nodeL = nodes[nodeIndex].lc;
        int nodeR = nodes[nodeIndex].rc;
        nodes[nodeIndex].tag = 0;
        changeTag(nodeL);
        changeTag(nodeR);
        changeValue(nodeL);
        changeValue(nodeR);
    }

    /**
     * 获取区间的中间值
     *
     * @param nodeIndex 节点编号
     * @return 中间值
     */
    private int getMid(int nodeIndex) {
        return (nodes[nodeIndex].l + nodes[nodeIndex].r) / 2;
    }

    /**
     * 把某个节点范围内所有灯的开关按一遍
     * 此时值为1的叶子节点数目为未按开关前值为0的叶子节点数目
     * 即值为1的叶子节点数目=所有叶子节点个数-值为1的叶子节点数目=当前节点范围-当前节点亮着的灯的数目
     *
     * @param nodeIndex
     */
    private void changeValue(int nodeIndex) {
        nodes[nodeIndex].value = nodes[nodeIndex].r - nodes[nodeIndex].l + 1 - nodes[nodeIndex].value;
    }

    private void changeTag(int nodeIndex) {
        nodes[nodeIndex].tag = (nodes[nodeIndex].tag + 1) % 2;
    }

}

主类

package r4.p2525;

import java.util.Scanner;

public class Main {

    public static void main(String args[]) {
        Scanner scanner = new Scanner(System.in);
        int lightsNum = scanner.nextInt();
        int operationsNum = scanner.nextInt();
        SegTree segTree = new SegTree(lightsNum);
        segTree.build(1, lightsNum);

        for (int i = 0; i < operationsNum; i++) {
            int operation = scanner.nextInt();
            int start = scanner.nextInt();
            int end = scanner.nextInt();
            if (operation == 0) {
                segTree.change(start, end, 1);
            } else {
                int c = segTree.query(start, end, 1);
                System.out.println(c);
            }
        }
    }
}

输入

4 5
0 1 2
0 2 4
1 2 3
0 2 4
1 1 4

输出

1
2
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值