线段树算法学习

http://www.cnblogs.com/TenosDoIt/p/3453089.html#b

线段树的思想是 将一整个区间二分拆成多个不重合的2段区间,知道不能拆分为止。

例如对于数组[2, 5, 1, 4, 9, 3]可以构造如下的二叉树(背景为白色表示叶子节点,非叶子节点的值是其对应数组区间内的最小值,例如根节点表示数组区间arr[0...5]内的最小值是1):

空间消耗: 如果假定原数组的长度为n,那么线段树的节点数就设为4*n。  原理:假设n=2^h,则 从第0行(i=0)开始,第i行有2^i个节点,一共有h行,所以节点总数为1+2+4+8+...+2^h=2^(h+1)-1=2*2^h - 1     等于2n-1 , 这时候难道线段树的空间复杂度就是O(2n-1)吗?  不是的。 这里我们假设了n=2^h,但是题目中的n可没说一定是2的幂次,这导致最后一行有一些节点没有用到(如上图最后一行所示),不过这没有关系,我们可以多开辟一些空间,这不影响

这时我们可以找一个最小且满足n<=2^h  的h,这时候就可以说线段数的空间复杂度是(2*2^h - 1)了。

但是常用的并不是这个空间复杂度,而是4*n,这是为什么呢?

因为开辟空间的时候去判断最大的h为多少略显麻烦,事实上我们的空间不用这么精打细算,可不可以找到一个方便得到的“可行”的空间复杂度呢?

可以这样考虑:当我们找到一个最小且满足n<=2^h  的h时,即 2^(h-1) <= n <= 2^h , 所以我们得到 2^h <= 2n , 所以接着上面的结论 复杂度(2*2^h - 1)可以拓展成(2*2n - 1) 即(4*n)。

算法分几个部分:

const int maxn=2e2+9;
int arr[maxn];

struct  seg //segtree下标从1开始
{
    int val;
}segTree[4*maxn];   //4倍空间

1、build建树

void build(int node, int istart, int iend)
{
    if(istart == iend)
        segTree[node].val = arr[istart];
    else
    {
        int mid=istart+(iend-istart)/2 ;
        build(2*node, istart, mid);
        build(2*node+1, mid+1, iend);
//        回溯计算当前节点的val
        segTree[node].val = min(segTree[2*node].val, segTree[2*node+1].val);
    }
}

2、查询指定区间的最值

//qstart qend     待查询区间起始位置
int query(int node, int nstart, int nend, int qstart, int qend)
{
    if(qend < nstart || qstart > nend)
        return INF;  // 无效,置无穷大
    if(qstart <= nstart && qend >= nend)
        return segTree[node].val;
    //即将用到子节点,更新子节点的add
    return min( query(2*node, nstart, mid, qstart, qend),
                query(2*node+1, mid+1, nend, qstart, qend));
}

3、单节点更新,这是线段树相比RMQ等算法的优势地方,他可以在O(logn)的时间复杂度下修改元素值并更新所有相关的最值。

增加某个元素值,并更新最值

//index   指定增加值的数组下标
void UpdateOne(int node, int nstart, int nend, int index, int addval)
{
    if(nstart == nend)
    {
        if(nstart == index)
            segTree[node].val += addval;
        return ;
    }
    int mid=nstart+(nend-nstart)/2 ;
    UpdateOne(2*node, nstart, mid, index, addval);
    UpdateOne(2*node+1, mid+1, nend, index, addval);
//    自下向上回溯节点
    segTree[node].val = min( segTree[2*node].val , segTree[2*node+1].val);
}

4、区间更新   这也是线段树的优势,并且,算法通过增加一个延迟更新标记的变量AddMark,大大减少了更新最值的时间复杂度。

考虑当我们需要修改了一个区间(a,b)所有元素的val时,理论上来说,需要更新所有子节点的val (segTree[node].val) 【父节点可以通过回溯更新】,但是如果这个区间的子节点比较多的时候,如果一次性全部更新完所有子节点,复杂度肯定是O( (b-a)lgn ),这复杂度是比较高的,往往也是不必要的,(后续程序不一定会用到这些子节点,可能查询到他们的父节点时已经完成查询)。我们可以 需要的用到这些子节点时候再更新这些子节点。 当我们找到一个节点p,并且决定考虑其子节点时,那么我们就要看节点p是否被标记,如果有,就要按照标记修改其子节点的信息,并且给子节点都标上相同的标记,同时消掉节点p的标记。

因此需要在线段树结构中加入延迟更新标记,我的程序中是加入addmark,表示节点的子孙节点在原来的值的基础上加上addMark的值,同时还需要修改创建函数build 和 查询函数 query,修改的代码用红色字体表示,其中区间更新的函数为update,代码如下:

struct  seg //segtree下标从1开始
{
    int AddMark;
    int val;
}segTree[4*maxn];   //4倍空间

void build(int node, int istart, int iend)
{
//    初始化addmark = 0
    segTree[node].AddMark = 0;
    if(istart == iend)
        segTree[node].val = arr[istart];
    else
    {
        int mid=istart+(iend-istart)/2 ;
        build(2*node, istart, mid);
        build(2*node+1, mid+1, iend);
//        回溯计算当前节点的val
        segTree[node].val = min(segTree[2*node].val, segTree[2*node+1].val);
    }
}

void pushDown(int node)
{
    if(segTree[node].AddMark != 0)
    {
//        子节点增加addmark
        segTree[2*node].AddMark += segTree[node].AddMark;
        segTree[2*node+1].AddMark += segTree[node].AddMark;
//        子节点增加value
        segTree[2*node].val += segTree[node].AddMark;
        segTree[2*node+1].val += segTree[node].AddMark;
//        当前节点addmark取消
        segTree[node].AddMark = 0;
    }
}

//qstart qend     待查询区间起始位置
int query(int node, int nstart, int nend, int qstart, int qend)
{
    if(qend < nstart || qstart > nend)
        return INF;  // 无效,置无穷大
    if(qstart <= nstart && qend >= nend)
        return segTree[node].val;
    //即将用到子节点,更新子节点的add
    pushDown(node);
    int mid=nstart+(nend-nstart)/2 ;
    return min( query(2*node, nstart, mid, qstart, qend),
                query(2*node+1, mid+1, nend, qstart, qend));
}

//index   指定增加值的数组下标
void UpdateOne(int node, int nstart, int nend, int index, int addval)
{
    if(nstart == nend)
    {
        if(nstart == index)
            segTree[node].val += addval;
        return ;
    }
    int mid=nstart+(nend-nstart)/2 ;
    UpdateOne(2*node, nstart, mid, index, addval);
    UpdateOne(2*node+1, mid+1, nend, index, addval);
//    自下向上回溯节点
    segTree[node].val = min( segTree[2*node].val , segTree[2*node+1].val);
}

//nstart nend 当前区间起始位置
//astart aend   指定的更新区间起始位置
void updateArea(int node, int nstart, int nend, int astart, int aend, int addval)
{
    if(nstart > aend || nend < astart)
        return ;
    if(nstart >= astart && nend <= aend)
    {
        //先标记  不急着更新子节点的值,等需要用到子节点时再更新
        segTree[node].AddMark += addval;
        segTree[node].val += addval;
        return ;
    }
    else
    {
//        需要用到子节点了,调用pushdown更新
        pushDown(node);
        int mid=nstart+(nend-nstart)/2 ;
        updateArea(2*node, nstart, mid, astart, aend, addval);
        updateArea(2*node+1, mid+1, nend, astart, aend, addval);
        segTree[node].val=min(segTree[2*node].val, segTree[2*node+1].val);
    }
}

举个例子:

当我们要对区间[0,2]的叶子节点增加2,利用区间查询的方法从根节点开始找到了非叶子节点[0-2],把它的值设置为1+2 = 3,并且把它的延迟标记设置为2,更新完毕;当我们要查询区间[0,1]内的最小值时,查找到区间[0,2]时,发现它的标记不为0,并且还要向下搜索,因此要把标记向下传递,把节点[0-1]的值设置为2+2 = 4,标记设置为2,节点[2-2]的值设置为1+2 = 3,标记设置为2(其实叶子节点的标志是不起作用的,这里是为了操作的一致性),然后返回查询结果:[0-1]节点的值4;当我们再次更新区间[0,1](增加3)时,查询到节点[0-1],发现它的标记值为2,因此把它的标记值设置为2+3 = 5,节点的值设置为4+3 = 7;

其实当区间更新的区间左右值相等时([i,i]),就相当于单节点更新,单节点更新只是区间更新的特例

------------------------------以上----------------------------------------------------

时间: 2024-10-27 03:46:44

线段树算法学习的相关文章

线段树学习笔记

线段树学习笔记 20180112 http://www.cnblogs.com/wuyuanyuan/p/8277100.html 一定要明确需要维护的值(区间最大值.区间和--). 原文地址:https://www.cnblogs.com/wuyuanyuan/p/8278004.html

线段树学习

此题题意很好懂:  给你N个数,Q个操作,操作有两种,‘Q a b ’是询问a~b这段数的和,‘C a b c’是把a~b这段数都加上c. 需要用到线段树的,update:成段增减,query:区间求和 介绍Lazy思想:lazy-tag思想,记录每一个线段树节点的变化值,当这部分线段的一致性被破坏我们就将这个变化值传递给子区间,大大增加了线段树的效率. 在此通俗的解释我理解的Lazy意思,比如现在需要对[a,b]区间值进行加c操作,那么就从根节点[1,n]开始调用update函数进行操作,如果

线段树学习(一)

看到UESTC的数据结构专题快要结束了,感觉自己真心浪费了好多时间,没有像鑫航学姐那样叮嘱的一样,紧紧的跟住训练. 所以下决心认认真真的开始学习下线段树的知识,以前对于线段树的学习都是一知半解的,就是说,我只知道线段树是用来单点更新和区间查值的,其实,线段树的功能远远不止这些. 先来说下,线段树是用来求解有关区间问题的绝逼法宝,为什么说绝逼呢.因为它能够在logn的时间内完成每次的查询. 查询的分类: 1.区间查询 -访问某段区间的某些性质(比如说,最大值,最小值,连续和,等等) 2.区间更新

决策树算法学习笔记

决策树算法 决策树的基本思想与人自身的决策机制非常类似,都是基于树结构进行决策,即对于不论什么问题.我们都先抽出当中的几个主要特征.然后对这些特征一个一个的去考察,从而决定这个问题应该属于的类别.比如我们要去商场买电脑,我们一般要通过考察电脑的CPU.内存,硬盘.显存等这些特征来推断这台电脑是好电脑还是一般电脑.当我们做推断的时候.我们都是首先看这个电脑的CPU怎么样,是i3?i5?还是i7?假设是i7我们就有更大概率倾向于觉得这台电脑是好电脑.然后我们再依次考察内存,硬盘.显存等.终于根据这些

[转]机器学习——C4.5 决策树算法学习

1. 算法背景介绍 分类树(决策树)是一种十分常用的分类方法.它是一种监管学习,所谓监管学习说白了很简单,就是给定一堆样本,每个样本都有一组属性和一个类别,这些类别是事先确定的,那么通过学习得到一个分类器,这个分类器能够对新出现的对象给出正确的分类.这样的机器学习就被称之为监督学习.C4.5分类树就是决策树算法中最流行的一种.下面给出一个数据集作为算法例子的基础,比如有这么一个数据集,如下: 我们将以这个数据集作讨论的基础.进行分类的目的就是根据某一天的天气状态,如天气,温度,湿度,是否刮风,来

机器学习实践之决策树算法学习

关于本文说明,本人原博客地址位于http://blog.csdn.net/qq_37608890,本文来自笔者于2017年12月06日 18:06:30所撰写内容(http://blog.csdn.net/qq_37608890/article/details/78731169).   本文根据最近学习机器学习书籍 网络文章的情况,特将一些学习思路做了归纳整理,详情如下.如有不当之处,请各位大拿多多指点,在此谢过. 一.决策树(decision tree)概述 1.决策树概念 决策树(decis

树——线段树

线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点. 对于线段树中的每一个非叶子节点[a,b],它的左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b].因此线段树是平衡二叉树,最后的子节点数目为N,即整个线段区间的长度. 使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为O(logN).而未优化的空间复杂度为2N,因此有时需要离散化让空间压缩. 案例:节点更新,查找最小值 #1077

Poj 2528-Mayor&#39;s posters 线段切割

题目:http://poj.org/problem?id=2528 Mayor's posters Time Limit: 1000MS   Memory Limit: 65536K Total Submissions: 55156   Accepted: 16000 Description The citizens of Bytetown, AB, could not stand that the candidates in the mayoral election campaign have

逆序对 线段树&amp;树状数组

17年的时候在HDU新生赛的时候遇到这样一道题目, 当时对于这种题目, 只会n^2去数左边比他大的个数 再相加一下 就是答案了. 无奈n是1e5 毫无疑问的T了. 后来学长说这个不就是归并排序吗, 你去学一下归并就可以做了, 然后我去学了归并, 又交了一发, 结果竟然还是T(这Y的不是耍我玩吗). 然后从另一位学长哪里听说了用线段树去求逆序对, 把n^2变成nlogn就不会T了,最后, 我又学了线段树,终于这回AC了.写这个帖子的时候,我顺便去HDU找了找这道题目,结果找不到这道题目,竟然没挂出