【线段树】Codevs线段树练习1.2.3及线段树学习笔记

今天发现不学线段树不行了于是干了一天线段树。当然先从弱弱的codevs开始。
没有看别的资料,看了看wikipedia上的线段树模板和简单的介绍就会了。。。
其实就是区间二分存储,每一次修改其中任意点的状态时候更新整棵树的数值就好了
线段树 Segment Tree )是一种 二叉搜索树 ,它将一个 区间 划分成一些单元区间,每个单元区间对应线段树中的一个 叶结点

对于线段树中的每一个非叶子节点[a,b],它的左子树表示的区间为[a,(a+b)/2],右子树表示的区间为[(a+b)/2+1,b]。因此线段树是平衡二叉树。叶节点数目为N,即整个线段区间的长度。

使用线段树可以快速的查找某一个节点在若干条线段中出现的次数, 时间复杂度 为O(logN)。而未优化的 空间复杂度 为2N,因此有时需要 离散化 让空间压缩。 ”————————————wikipedia
从上述描述我们可以看出,在定义线段树的时候数组的长度至少是2倍的区间长度(事实上最好再大一点到4倍,实在开不了太大也要多开上一两万个单元,以防万一)
给定整个线段区间,建立一棵线段树的时间复杂度是O(N) 。单点修改的时间复杂度是O(log N) 。单点查询的时间复杂度是  O(1) 。如果允许惰性赋值而加上延迟标记的话,许多的区间修改的时间复杂度也会是  O(log N) ,但是单点查询的时间复杂度会变成O(log N)
代码中, rt指的是root, 当前子树的根节点; l, r指的是当前子树所统计的区间[l,r]
利用完全二叉堆的性质来保存节点编号, 所以rt << 1是左子树的节点, rt << 1 | 1是右子树的节点 在查询和成端更新操作中的L和R是指修改或者查询的区间 ”——————weikipedia
常见的线段树主要分为两类,一种是支持区间求和,另一种是求区间最值(当然不仅仅是这两种应用,还可以用线段树保存很多状态)  
对于这些问题,我们如果手动模拟去做,很显然一定会TLE的,也得不到很高的分数,这时候就需要线段树优越的时间复杂度了。
关于惰性标记:
    惰性标记是标志对从某个起点开始,一定长度区间内的所有数进行的某种操作,在进行区间操作时常常使用惰性标记进行优化来减少时间复杂度。
至于为什么叫惰性,别问我,我不知道。

那么接下来我们来看一下线段树的基本操作(来自wikipedia)。 

顺便学会了宏定义的高端使用方法(P.S.wikipedia上的代码里的rchild的宏定义有问题)
首先
宏定义lchild,rchild(可以减少代码量,省力省时)
#define lchild rt << 1, l, m
 #define rchild rt << 1|1, m + 1, r

(位运算的定义可以加快速度,左子树是该节点代表区间的左半部分,右子树亦然)
对于那些不了解C++的
      <<1就是乘2,>>1就是整除2,|1就是加1(其本质是二进制的左移一位右移一位末尾加一)

建树操作 
void build(int rt= 1,int l =1, int r= N)
{ 
if (l== r)
{
 cin >> tree[rt]; //当然也可以用scanf等等
return;
} 
int m =(l + r)>> 1;
build(lchild); build(rchild);
push_up(rt); //数据向上更新
} 


 向上更新整棵树中的节点的数值
/* 对于区间求和 */
 void push_up(int rt){ tree[rt]= tree[rt<< 1]+ tree[rt<< 1| 1];}  
  /* 对于区间求最大值 */
 void push_up(int rt){ tree[rt]= max(tree[rt<< 1], tree[rt<< 1| 1]); } 


单点操作
 
 void update(int p, int delta,int rt=1,int l=1,int r= N)
{
if 
(l == r)
{ 
tree[rt]+= delta;
return;
} 
int m =(l + r)>> 1;
if (p<= m)
update(p, delta, lchild);
else update(p, delta, rchild);
push_up(rt);
}


其中P为目标点的编号,delta是变化值
区间操作
void update(int L,int R, int delta,int rt =1, int l= 1,int r = N)
{ 
if (L<= l && r<= R)
{ 
tree[rt]+= delta* (r- l +1);
lazy[rt]+= delta;
return;
} 
if (lazy[rt])
push_down(rt, r - l + 1); //更新惰性标记
int m =(l + r)>> 1;
if (L<= m) update(L, R, delta, lchild);
if (R> m) update(L, R, delta, rchild);
push_up(rt); 
}


将目标区间分割,然后对两块小区间分别进行操作 
 
更新惰性标记(区间求和)
对于区间求和, 原子数组值需要加上lazy标记乘以子树所统计的区间长度。 len为父节点统计的区间长度, 则len - (len >> 1)为左子树区间长度, len >> 1为右子树区间长度。 ”——————wikipedia 
void push_down(int rt,int len)
{ 
tree[rt << 1]+= lazy[rt]* (len- (len>> 1));
lazy[rt << 1]+= lazy[rt];
tree[rt << 1 | 1]+= lazy[rt]* (len>> 1);
lazy[rt << 1 | 1]+= lazy[rt];
lazy[rt]= 0; 
} 


更新惰性标记(区间最值)
对于区间求最大值, 子树的值不需要乘以长度, 所以不需要传递参数len。 ” ——————wikipedia
void push_down(int rt)
{ 
tree[rt << 1]+= lazy[rt];
lazy[rt << 1]+= lazy[rt];
tree[rt << 1 | 1]+= lazy[rt];
lazy[rt << 1 | 1]+= lazy[rt];
lazy[rt]= 0;
}


区间数值查询
int query(int L,int R, int rt= 1,int l =1, int r= N)
{ 
if (L<= l && r<= R)return tree[rt];
if (lazy[rt]) push_down(rt, r - l +1);
int m =(l + r)>> 1, ret= 0;
if (L<= m) ret+= query(L, R, lchild);
if (R> m) ret+= query(L, R, rchild);
return ret; 
}


至于单点数据查询,输出对应的tree数组中元素值就好了
 
那么让我们看几个最简单地裸线段树的例题(来自Codevs) 

1.codevs1080 线段树练习 

                     题目描述 Description

一行N个方格,开始每个格子里都有一个整数。现在动态地提出一些问题和修改:提问的形式是求某一个特定的子区间[a,b]中所有元素的和;修改的规则是指定某一个格子x,加上或者减去一个特定的值A。现在要求你能对每个提问作出正确的回答。1N<100000,,提问和修改的总数m<10000条。

输入描述 Input Description

输入文件第一行为个整数N,接下来是nn个整数,表示格子中原来的整数。接下一个正整数m,再接下来有m行,表示m个询问,第一个整数表示询问代号,询问代号1表示增加,后面的两个数xA表示给位置X上的数值增加A,询问代号2表示区间求和,后面两个整数表示ab,表示要求[a,b]之间的区间和。

输出描述 Output Description

共m行,每个整数

样例输入 Sample Input

6

3

4

1 3 5

2 1 4

1 1 9

2 2 6

样例输出 Sample Output

22

22

数据范围及提示 Data Size & Hint
1≤N≤100000, m≤10000 。
单点修改,区间求和。
#include
#include
#include
#include
#include
#define lchild rt<<1,l,m
#define rchild rt<<1|1,m+1,r
using namespace std;
int n,m;
int a,b,c;
int tree[400001];
int arr[100001];
void push_up(int rt)
{
    tree[rt]=tree[rt<<1]+tree[rt<<1|1];
}
void build(int rt=1,int l=1,int r=n)
{
    if (l==r)
    {
        tree[rt]=arr[l];
        return;
    }
    int m=(l+r)>>1;
    build(lchild);build(rchild);
    push_up(rt);
}
void update(int p,int delta,int rt=1,int l=1,int r=n) 
{
    if (l==r) 
    {
        tree[rt]+=delta;
        return;
    }
    int m=(l+r)>>1;
    if (p<=m) update(p,delta,lchild);
    else update(p,delta,rchild);
    push_up(rt);
}
int query(int L,int R,int rt=1,int l=1,int r=n) 
{
    if (L<=l&&r<=R) return tree[rt];
    int m=(l+r)>>1, ret=0;
    if (L<=m) ret+=query(L,R,lchild);
    if (R>m)  ret+=query(L,R,rchild);
    return ret;
}
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)scanf("%d",arr+i);
    build(1,1,n);
    scanf("%d",&m);
    for (int i=1;i<=m;i++)
    {
        scanf("%d%d%d",&a,&b,&c);
        switch(a) 
        {
            case 1:
                update(b,c);
                break;
            case 2:
                cout<

2.codevs1081线段树练习2
                     题目描述 Description

给你N个数,有两种操作


1:给区间[a,b]的所有数都增加X


2:询问第i个数是什么?

输入描述 Input Description

第一行一个正整数n,接下来n行n个整数,再接下来一个正整数Q,表示操作的个数. 接下来Q行每行若干个整数。如果第一个数是1,后接3个正整数a,b,X,表示在区间[a,b]内每个数增加X,如果是2,后面跟1个整数i, 表示询问第i个位置的数是多少。

输出描述 Output Description

对于每个询问输出一行一个答案

样例输入 Sample Input

3

1

2

3

2

1 2 3 2

2 3

样例输出 Sample Output

5

数据范围及提示 Data Size & Hint

数据范围

1<=n<=100000

1<=q<=100000
区间修改,单点查询。(将单点当成左右端点相同的区间即可)
#include
#include
#include
#include
#include
#define lchild rt<<1,l,m
#define rchild rt<<1|1,m+1,r
using namespace std;
int n,q,tree[210000],lazy[210000];
int a,b,c,d;
void push_up(int rt)
{
tree[rt]=tree[rt<<1]+tree[rt<<1|1];
}
void push_down(int rt, int len) 
{
    tree[rt<<1]+=lazy[rt]*(len-(len>>1));
    lazy[rt<<1]+=lazy[rt];
    tree[rt<<1|1]+=lazy[rt]*(len>>1);
    lazy[rt<<1|1]+=lazy[rt];
    lazy[rt]=0;
}
void build(int rt=1,int l=1,int r=n)
{
if (l==r)
{
scanf("%d",&tree[rt]);
return;
}
int m=(l+r)>>1;
build(lchild);
build(rchild);
push_up(rt);
}
void update(int L,int R,int delta,int rt=1,int l=1,int r=n) 
{
    if (L<=l&&r<=R) 
{
        tree[rt]+=delta*(r-l+1);
        lazy[rt]+=delta;
        return;
    }
    if (lazy[rt]) push_down(rt,r-l+1);
    int m=(l+r)>>1;
    if (L<=m) update(L,R,delta,lchild);
    if (R>m)  update(L,R,delta,rchild);
    push_up(rt);
}
int query(int L,int R,int rt=1,int l=1,int r=n) 
{
    if (L<=l&&r<=R) return tree[rt];
    if (lazy[rt]) push_down(rt,r-l+1);
    int m=(l+r)>>1,ret=0;
    if (L<=m) ret+=query(L,R,lchild);
    if (R>m) ret+=query(L,R,rchild);
    return ret;
}
int main()
{
scanf("%d",&n);
build();
scanf("%d",&q);
for (int i=1;i<=q;i++)
{
scanf("%d",&a);
switch (a)
{
case 1:
scanf("%d%d%d",&b,&c,&d);
update(b,c,d);
break;
case 2:
scanf("%d",&b);
cout<


3.codevs1082线段树练习3 
       题目描述 Description

给你N个数,有两种操作:


1:给区间[a,b]的所有数增加X


2:询问区间[a,b]的数的和。

输入描述 Input Description

第一行一个正整数n,接下来n行n个整数,

 

再接下来一个正整数Q,每行表示操作的个数,

 

如果第一个数是1,后接3个正整数,

 

表示在区间[a,b]内每个数增加X,如果是2,

 

表示操作2询问区间[a,b]的和是多少。

输出描述 Output Description

对于每个询问输出一行一个答案

样例输入 Sample Input

3

1

2

3

2

1 2 3 2

2 2 3

样例输出 Sample Output

9

数据范围及提示 Data Size & Hint

数据范围

1<=n<=200000

1<=q<=200000
 区间操作,区间查询(其实本来和第二题一样,但是数据范围巨大,将所有函数都改为longlong之后就没事了)

#include
#include
#include
#include
#include
#define lchild rt<<1,l,m
#define rchild rt<<1|1,m+1,r
using namespace std;
long long n,q,tree[810000],lazy[810000];
long long a,b,c,d;
void push_up(long long rt)
{
tree[rt]=tree[rt<<1]+tree[rt<<1|1];
}
void push_down(long long rt, long long len) 
{
    tree[rt<<1]+=lazy[rt]*(len-(len>>1));
    lazy[rt<<1]+=lazy[rt];
    tree[rt<<1|1]+=lazy[rt]*(len>>1);
    lazy[rt<<1|1]+=lazy[rt];
    lazy[rt]=0;
}
void build(long long rt=1,long long l=1,long long r=n)
{
if (l==r)
{
scanf("%lld",&tree[rt]);
return;
}
int m=(l+r)>>1;
build(lchild);
build(rchild);
push_up(rt);
}
void update(long long L,long long R,long long delta,long long rt=1,long long l=1,long long r=n) 
{
    if (L<=l&&r<=R) 
{
        tree[rt]+=delta*(r-l+1);
        lazy[rt]+=delta;
        return;
    }
    if (lazy[rt]) push_down(rt,r-l+1);
    int m=(l+r)>>1;
    if (L<=m) update(L,R,delta,lchild);
    if (R>m)  update(L,R,delta,rchild);
    push_up(rt);
}
long long query(long long L,long long R,long long rt=1,long long l=1,long long r=n) 
{
    if (L<=l&&r<=R) return tree[rt];
    if (lazy[rt]) push_down(rt,r-l+1);
    int m=(l+r)>>1;
long long ret=0;
    if (L<=m) ret+=query(L,R,lchild);
    if (R>m) ret+=query(L,R,rchild);
    return ret;
}
int main()
{
scanf("%lld",&n);
build();
scanf("%lld",&q);
for (int i=1;i<=q;i++)
{
scanf("%lld",&a);
switch (a)
{
case 1:
scanf("%lld%lld%lld",&b,&c,&d);
update(b,c,d);
break;
case 2:
scanf("%lld%lld",&b,&c);
printf("%lld\n",query(b,c));
break;
}
}
}


小结:作为一种中级数据结构,线段树在很多方面都有应用,在OI竞赛中,大部分时候线段树并不是单独出现也不止区间求和和区间最值两种,而是综合了多种问题或者进行了改进和加强,如 Codevs
1217 借教室 (2012年NOIP全国联赛提高组Day2T2)Codevs19461946 阿狸的打字机(2011年NOI全国竞赛)等等
为了解决这些题目,首先就需要线段树的熟练掌握。 今天的题目只是一些裸线段树的基础中的基础,巩固线段树这种数据结构还需要在今后的练习中反复强化

 

你可能感兴趣的:(【线段树】Codevs线段树练习1.2.3及线段树学习笔记)