周中训练笔记11——线段树总结
2017-10-15 18:11
253 查看
线段树专题接近尾声了,是时候总结一波了,说来惭愧,线段树专题差不多都是参照题解才做出来的,虽然知道是套模板但是具体细节真的很拿人啊。。。
主要总结一下线段树的模板吧,题目的代码实现都要以模板为框架构造:
首先提出一个问题:
给你n个数,有两种操作:
1:给第i个数的值增加X
2:询问区间[a,b]的总和是什么?
输入描述
输入文件第一行为一个整数n,接下来是n行n个整数,表示格子中原来的整数。接下一个正整数q,再接
下来有q行,表示q个询问,第一个整数表示询问代号,询问代号1表示增加,后面的两个数x和A表示给
位置X上的数值增加A,询问代号2表示区间求和,后面两个整数表示a和b,表示要求[a,b]之间的区间和。
样例输入
4
7 6 3 5
2
1 1 4
2 1 2
样例输出
17
数据范围
1 <= n,q <= 100000
看到这个问题,最朴素的想法是用一个数组模拟,求和时 [ a , b ]中逐个累加 , 最后输出 。
但是,由于数据量比较大,时间复杂度太高,时间上无法承受。
这时我们可以用线段树( Segment Tree ),这种特殊的数据结构解决这个问题。
那么什么是线段树呢?
这就是一棵典型的线段树
一 般的线段树上的每一个节点T[a , b],代表该节点维护了原数列[ a , b ]区间的信息。对于每一个节点他至少有
三个信息:左端点,右端点,我们需要维护的信息(在本题中我们维护区间和)。由于线段树是一个二叉树,而且是一个平衡二叉树,如果当前结点的编号是i,左端点为L ,右端点为 R , 那么左儿子的 编号为 i*2 ,左端点为 L ,右端点为 (L + R)/2 ; 同理右儿子的 编号为 i*2+1,左端点为(L+R)/2 ,右端点为 R
。如果当前结点的左端点等于右端点,那么该节点就是叶子节点,直接在该节点赋值即可。显然线段树是递归定义的。
线段树就是这样一种数据结构,讲一个大区间分为若干个不相交的区间,每次维护都在小区间上处理,并且查
询也在这些被分解的区间中信息合并出我们需要的结果,这就是线段树高效的原因。
线段树的存储:
线段树的存储可用链表和数组模拟。(采用数组写法,便于理解)
1.链表存储:
struct node
{
int Left, Right;
node *Leftchild , *Rightchild;
};
2.数组模拟
struct Tree
{
int l, r;
long long sum;
} tr[maxN << 2];
注意:数组的空间要开四倍大小,防止访问越界,(理论上大于maxN的最小2x的两倍)
建树:
线段树的构建是自顶点而下,即从根节点开始递归构建,根据线段树定义,当左端点等于右端点时(达到递归边界),直接赋值即可,回溯时也要维护区间,代码如下:
void Build_Tree ( int x , int y , int i )
{
tr[i].l = x;
tr[i].r = y;
if( x == y )tr[i].sum = a[x] ; //找到叶子节点,赋值
else
{
ll mid = (tr[i].l tr[i].r ) >> 1 ;
Build_Tree ( x , mid , i << 1); //左子树
Build_Tree ( mid + 1 , y , i << 1 | 1); //右子树
tr[i].sum = tr[i << 1].sum + tr[i << 1 | 1].sum; //回溯维护区间和
}
}
维护树:
维护树的方法也很好理解,如果目标更新节点在左儿子里,去左儿子中查找;反之,在右儿子中。不断递归,知道找到需要维护的节点,更新它,回溯是一路更新回来。这就是维护的过程,代码如下:
void Update_Tree ( int q , int val , int i )
{
if(tr[i].l == q && tr[i].r == q) //找到需要修改的叶子节点
{
tr[i].sum = val ; //更新当前结点
}
else //当前结点是非叶子结点
{
long long mid = (tr[i].l tr[i].r ) >> 1 ; //取中间
if ( q <= mid ) //目标节点在左儿子中
{
Update_Tree ( q , val , i << 1 );
}
else if( q > mid ) //目标节点在右儿子中
{
Update_Tree ( q , val , i << 1 | 1 );
}
tr[i].sum = tr[i << 1].sum + tr[i << 1 | 1].sum; //回溯
}
}
查询树:
题目中让我们查询区间求和,不难想到如果当前结点的区间完全被目标区间包含,直接返回当前结点的sum值,
否则分类讨论。具体过程通过以下代码理解:
long long Query_Tree ( int q , int w , int i )
{
if ( q <= tr[i].l && w >= tr[i].r ) return tr[i].sum; //当前结点的区间完全被目标区间包含
else
{
long long mid = (tr[i].l tr[i].r) >> 1;
if( q > mid ) //完全在左儿子
{
return Query_Tree ( q , w , i << 1 | 1);
}
else if (w <= mid ) //完全在右儿子
{
return Query_Tree ( q , w , i << 1);
}
else //目标区间在左右都有分布
{
return Query_Tree ( q , w , i << 1) + Query_Tree ( q , w , i << 1 | 1 );
}
}
}
主程序:
int main ( )
{
int N, M, q, val, l, r;
scanf("%d", &N);
for ( int i = 1 ; i <= N ; i++ )scanf("%d", &a[i]);
Build_Tree ( 1 , N , 1);
cin >> M ;
while (M--)
{
int op ;
cin >> op ;
if ( op == 1 )
{
scanf("%d%d", &q, &val);
Update_Tree ( q , val , 1);
}
else
{
scanf("%d%d", &l, &r);
printf("%lld\n", Query_Tree ( l , r, 1 ));
}
}
return 0 ;
}
线段树的性质:
假设线段树处理的数列长度为N,那么总结点数不超过2*N(满二叉树是最大情况);
线段分解数量级:线段树能把任意一条长度为M的线段分为不超过2Log2(M)条线段(我们知道一个很大的数,Log一下就变小了),这条性质使线段树的查询与修改复杂度都在O(Log2(n))的范围内解决。
由于线段树是一颗二叉树,深度约为Log2(N)左右。
综上,线段树空间消耗O(n),由于它深度性质,使它在解决问题上有较高的效率。
主要总结一下线段树的模板吧,题目的代码实现都要以模板为框架构造:
首先提出一个问题:
给你n个数,有两种操作:
1:给第i个数的值增加X
2:询问区间[a,b]的总和是什么?
输入描述
输入文件第一行为一个整数n,接下来是n行n个整数,表示格子中原来的整数。接下一个正整数q,再接
下来有q行,表示q个询问,第一个整数表示询问代号,询问代号1表示增加,后面的两个数x和A表示给
位置X上的数值增加A,询问代号2表示区间求和,后面两个整数表示a和b,表示要求[a,b]之间的区间和。
样例输入
4
7 6 3 5
2
1 1 4
2 1 2
样例输出
17
数据范围
1 <= n,q <= 100000
看到这个问题,最朴素的想法是用一个数组模拟,求和时 [ a , b ]中逐个累加 , 最后输出 。
但是,由于数据量比较大,时间复杂度太高,时间上无法承受。
这时我们可以用线段树( Segment Tree ),这种特殊的数据结构解决这个问题。
那么什么是线段树呢?
这就是一棵典型的线段树
一 般的线段树上的每一个节点T[a , b],代表该节点维护了原数列[ a , b ]区间的信息。对于每一个节点他至少有
三个信息:左端点,右端点,我们需要维护的信息(在本题中我们维护区间和)。由于线段树是一个二叉树,而且是一个平衡二叉树,如果当前结点的编号是i,左端点为L ,右端点为 R , 那么左儿子的 编号为 i*2 ,左端点为 L ,右端点为 (L + R)/2 ; 同理右儿子的 编号为 i*2+1,左端点为(L+R)/2 ,右端点为 R
。如果当前结点的左端点等于右端点,那么该节点就是叶子节点,直接在该节点赋值即可。显然线段树是递归定义的。
线段树就是这样一种数据结构,讲一个大区间分为若干个不相交的区间,每次维护都在小区间上处理,并且查
询也在这些被分解的区间中信息合并出我们需要的结果,这就是线段树高效的原因。
线段树的存储:
线段树的存储可用链表和数组模拟。(采用数组写法,便于理解)
1.链表存储:
struct node
{
int Left, Right;
node *Leftchild , *Rightchild;
};
2.数组模拟
struct Tree
{
int l, r;
long long sum;
} tr[maxN << 2];
注意:数组的空间要开四倍大小,防止访问越界,(理论上大于maxN的最小2x的两倍)
建树:
线段树的构建是自顶点而下,即从根节点开始递归构建,根据线段树定义,当左端点等于右端点时(达到递归边界),直接赋值即可,回溯时也要维护区间,代码如下:
void Build_Tree ( int x , int y , int i )
{
tr[i].l = x;
tr[i].r = y;
if( x == y )tr[i].sum = a[x] ; //找到叶子节点,赋值
else
{
ll mid = (tr[i].l tr[i].r ) >> 1 ;
Build_Tree ( x , mid , i << 1); //左子树
Build_Tree ( mid + 1 , y , i << 1 | 1); //右子树
tr[i].sum = tr[i << 1].sum + tr[i << 1 | 1].sum; //回溯维护区间和
}
}
维护树:
维护树的方法也很好理解,如果目标更新节点在左儿子里,去左儿子中查找;反之,在右儿子中。不断递归,知道找到需要维护的节点,更新它,回溯是一路更新回来。这就是维护的过程,代码如下:
void Update_Tree ( int q , int val , int i )
{
if(tr[i].l == q && tr[i].r == q) //找到需要修改的叶子节点
{
tr[i].sum = val ; //更新当前结点
}
else //当前结点是非叶子结点
{
long long mid = (tr[i].l tr[i].r ) >> 1 ; //取中间
if ( q <= mid ) //目标节点在左儿子中
{
Update_Tree ( q , val , i << 1 );
}
else if( q > mid ) //目标节点在右儿子中
{
Update_Tree ( q , val , i << 1 | 1 );
}
tr[i].sum = tr[i << 1].sum + tr[i << 1 | 1].sum; //回溯
}
}
查询树:
题目中让我们查询区间求和,不难想到如果当前结点的区间完全被目标区间包含,直接返回当前结点的sum值,
否则分类讨论。具体过程通过以下代码理解:
long long Query_Tree ( int q , int w , int i )
{
if ( q <= tr[i].l && w >= tr[i].r ) return tr[i].sum; //当前结点的区间完全被目标区间包含
else
{
long long mid = (tr[i].l tr[i].r) >> 1;
if( q > mid ) //完全在左儿子
{
return Query_Tree ( q , w , i << 1 | 1);
}
else if (w <= mid ) //完全在右儿子
{
return Query_Tree ( q , w , i << 1);
}
else //目标区间在左右都有分布
{
return Query_Tree ( q , w , i << 1) + Query_Tree ( q , w , i << 1 | 1 );
}
}
}
主程序:
int main ( )
{
int N, M, q, val, l, r;
scanf("%d", &N);
for ( int i = 1 ; i <= N ; i++ )scanf("%d", &a[i]);
Build_Tree ( 1 , N , 1);
cin >> M ;
while (M--)
{
int op ;
cin >> op ;
if ( op == 1 )
{
scanf("%d%d", &q, &val);
Update_Tree ( q , val , 1);
}
else
{
scanf("%d%d", &l, &r);
printf("%lld\n", Query_Tree ( l , r, 1 ));
}
}
return 0 ;
}
线段树的性质:
假设线段树处理的数列长度为N,那么总结点数不超过2*N(满二叉树是最大情况);
线段分解数量级:线段树能把任意一条长度为M的线段分为不超过2Log2(M)条线段(我们知道一个很大的数,Log一下就变小了),这条性质使线段树的查询与修改复杂度都在O(Log2(n))的范围内解决。
由于线段树是一颗二叉树,深度约为Log2(N)左右。
综上,线段树空间消耗O(n),由于它深度性质,使它在解决问题上有较高的效率。
相关文章推荐
- Flask学习总结笔记(11) -- 利用itsdangerous实现用户身份确认
- PHP 设计模式 笔记与总结(11)观察者模式
- Android开发总结笔记 Progress(进度条) 1-1-11
- .Net培训个人总结笔记11
- 数据结构与算法分析笔记与总结(java实现)--链表11:链表按值分化问题
- 数据结构与算法分析笔记与总结(java实现)--数组11:数组中的逆序对(﹡)
- Nginx 笔记与总结(11)Nginx + php-fpm + MySQL 安装 ecshop
- 薛开宇学习笔记二之总结笔记(用一个预训练模型提取特征)--Linux语法总结
- 首届华中区程序设计邀请赛暨第十届武汉大学程序设计大赛训练总结【8/11】
- 黑马程序员——Java学习笔记之11——“Collection集合”总结
- java学习笔记11--集合总结
- 薛开宇学习笔记一之总结笔记(CIFAR-10 在 在 caffe 上进行训练与学习)--Linux语法总结
- torch入门笔记11:如何训练神经网络
- PHP 错误与异常 笔记与总结(11 )register_shutdown_function() 函数的使用
- 数据结构与算法分析笔记与总结(java实现)--排序11:有序矩阵查找练习题
- Android开发总结笔记 Fragment(上) 1-2-11
- 周中训练笔记——线段树模板(建树+更新)(9.7)
- java基础教程学习笔记总结&nbsp;11-18节
- 山东省第八届ACM大学生程序设计竞赛 训练总结 [8/11] 待补
- Bootstrap学习总结笔记(11)-- 基本组件之列表组