[PKU 2777] 线段树(一) {概述 基本操作}
2010-10-09 22:04
225 查看
{
以前写的线段树都是零碎
而且描述的也不清楚
最近打算整理一下
就从我的第一个线段树程序开始吧
}
线段树 Segment_tree
网上有人把线段树翻译成 Interval_Tree
Interval_Tree 是另外一种数据结构 而且并非二叉树
这个是线段树的标准E文翻译
可以看wikipedia的原文 http://en.wikipedia.org/wiki/Segment_tree
顾名思义 线段树存储的是连续的线段而非离散的节点
先看一张经典的线段树图解
这个就是标准的线段树
既然是树形结构 我们就得先考虑怎么存储这棵树
分析线段树的定义
*线段树是一棵二叉树 记为T(a, b)
*参数a,b表示区间[a,b] 其中b-a称为区间的长度 记为L
*线段树T(a,b)也可递归定义为
-若L>1 [a, (a+b) div 2]为T的左儿子
[(a+b) div 2,b]为T的右儿子
-若L=1 T为叶子节点
可以得到一些基本性质
*线段树除最后一层外是满二叉树
*线段树是平衡的 高度是Log2L左右
如此我们有2种存储方法
*直接用指针
定义节点
type node=record
ls,rs:^node;
l,r:longint;
end;
其中ls rs分别为左右儿子 l,r是区间的范围
真正实现时一般用数组模拟指针
我们只需定义longint数组ls[] rs[] l[] r[]
*用*2和*2+1代替左右儿子指针
由于是除最后一层外是满二叉树
我们可以向存储堆一样存储线段树
用l[] r[]来存储节点区间范围
x的左右儿子分别就是x*2和x*2+1
具体实现用位移代替乘2
这样乘法指针运算和上述数组调用一样 几乎不需要时间
具体用哪种纯粹是个人喜好 没什么区别
(下文中我的程序都是用的数组模拟 直接存储儿子指针)
接下来讨论线段树的具体操作
也就是维护这种数据结构的算法 (srO 数据结构+算法=程序 Orz)
总结起来就两个词 递归 & 分治
结合一个具体问题吧 PKU 2777
http://poj.org/problem?id=2777
这是线段树的入门题 相当经典
要求程序实现一个涂色的程序
支持对区间[A,B]涂C的颜色和统计区间[A,B]的颜色种类
朴素的做法是用数组a[]存储下整个区间[0,100000]
然后循环涂色 循环查询 这样的复杂度是N*N 大大地TLE
我们考虑用线段树处理这个区间问题
首先我们得建树
先看程序
*第一行build函数是一个递归的过程 参数a,b表示当前建立区间[a,b]的节点
*第四行 inc(tt) 就相当于新建一个节点 x纪录当前节点
*第六行 b-a=1 是递归的边界条件 即建立到叶子节点了
*第八 九 十行 根据线段树定义 分别递归建立左右儿子区间
-注意儿子指针的赋值 由于递归下去第一层就是建立当前节点的儿子 直接赋值为tt+1即可
-注意使用shr运算提高效率 还需注意a b mid 皆为区间端点
其实上文中建好的线段树其实是一个骨架
就相当于朴素做法中我们还未操作的空数组 等待我们给它刷颜色
既然要刷颜色 我们就得存储各区间的颜色 给每个节点新开一个域n来记录颜色
表现在数组模拟上就是新建数组n[]
n数组代表当前节点所代表区间的颜色
因为这个问题的染色是覆盖类型的染色
对一个区间染色自然把为当前区间的子区间也染色了
所以是对子树染色而非区间染色
接着这样的思路 我们可以写出如下程序
*第四 五行 判断当前区间是否在需要覆盖的区间内 是就修改颜色
*第六 七 八行 根据线段树定义判断是否递归左右子树染色
这里需要说明一下这种写法的正确性
即不会出现[a,b]在[l[x],r[x]]外与当前区间没有交集的情况
首先在根节点处[a,b]和区间显然有交集
然后运用数学归纳法的思路 说明当前节点区间和[a,b]有交集的时候 递归插入儿子也是保证和儿子区间有交集的
这样只要执行插入函数就有交集 就能保证程序正确性
给出所有和当前区间有交集的情况图 可以发现经过if语句判断 递归插入都保证还是和儿子区间有交集
(黑色为当前区间 红色为欲染色区间 一共6种情况)
不难分析出这个插入函数的复杂度是O(N)级别的(需要遍历子树) 从常数上看比朴素还慢
但是不覆盖子树上的区间又会产生错误 我们需要对插入进行改进
改进后 我们的n[]数组不单记录一个节点的颜色 而是记录的子树的颜色
我们看具体操作
*如果当前区间已经染色且颜色和欲染色一致 则直接退出(这句话可以不要)
*如果当前区间被完全覆盖 就说明子树也被完全覆盖了 直接给当前节点染色退出
*如果没有被完全覆盖
-就给先给左右儿子染色成当前节点的颜色 然后当前节点赋值为混合颜色=-1
-然后再递归染色左右子树
这样修改完全覆盖的区间时就可以直接修改然后退出 不用遍历子树了
而没有完全覆盖时 需要把颜色先下传给左右子树 再递归修改 保证子树颜色的正确性
这样我们访问的区间总数就降到了O(LogN)级别个 比O(N)好了不少
这个其实是一种最原始的Lazy-Tag思想
这种思想很重要 也比较难掌握 我们以后详细讨论
给出改进后的代码
最后就是统计了
统计相对很简单 一共30种颜色 用个Simple Hash即可
这时候我们记录的混合颜色就有用了 用于判断
结构和插入差不多 不过递归的条件不再是是否有交集而是是否为空节点了
最后是我的AC代码
这就是线段树的入门题
接下来会具体讨论线段树的另一个经典问题
求矩形并面积和周长
需要用到线段删除和维护更多的域
BOB HAN原创 转载请注明出处 http://www.cnblogs.com/Booble/
以前写的线段树都是零碎
而且描述的也不清楚
最近打算整理一下
就从我的第一个线段树程序开始吧
}
线段树 Segment_tree
网上有人把线段树翻译成 Interval_Tree
Interval_Tree 是另外一种数据结构 而且并非二叉树
这个是线段树的标准E文翻译
可以看wikipedia的原文 http://en.wikipedia.org/wiki/Segment_tree
顾名思义 线段树存储的是连续的线段而非离散的节点
先看一张经典的线段树图解
这个就是标准的线段树
既然是树形结构 我们就得先考虑怎么存储这棵树
分析线段树的定义
*线段树是一棵二叉树 记为T(a, b)
*参数a,b表示区间[a,b] 其中b-a称为区间的长度 记为L
*线段树T(a,b)也可递归定义为
-若L>1 [a, (a+b) div 2]为T的左儿子
[(a+b) div 2,b]为T的右儿子
-若L=1 T为叶子节点
可以得到一些基本性质
*线段树除最后一层外是满二叉树
*线段树是平衡的 高度是Log2L左右
如此我们有2种存储方法
*直接用指针
定义节点
type node=record
ls,rs:^node;
l,r:longint;
end;
其中ls rs分别为左右儿子 l,r是区间的范围
真正实现时一般用数组模拟指针
我们只需定义longint数组ls[] rs[] l[] r[]
*用*2和*2+1代替左右儿子指针
由于是除最后一层外是满二叉树
我们可以向存储堆一样存储线段树
用l[] r[]来存储节点区间范围
x的左右儿子分别就是x*2和x*2+1
具体实现用位移代替乘2
这样乘法指针运算和上述数组调用一样 几乎不需要时间
具体用哪种纯粹是个人喜好 没什么区别
(下文中我的程序都是用的数组模拟 直接存储儿子指针)
接下来讨论线段树的具体操作
也就是维护这种数据结构的算法 (srO 数据结构+算法=程序 Orz)
总结起来就两个词 递归 & 分治
结合一个具体问题吧 PKU 2777
http://poj.org/problem?id=2777
这是线段树的入门题 相当经典
要求程序实现一个涂色的程序
支持对区间[A,B]涂C的颜色和统计区间[A,B]的颜色种类
朴素的做法是用数组a[]存储下整个区间[0,100000]
然后循环涂色 循环查询 这样的复杂度是N*N 大大地TLE
我们考虑用线段树处理这个区间问题
首先我们得建树
先看程序
procedure build(a,b:longint); var x,mid:longint; begin inc(tt); x:=tt; l[x]:=a; r[x]:=b; if b-a>1 then begin mid:=(a+b)shr 1; ls[x]:=tt+1; build(a,mid); rs[x]:=tt+1; build(mid,b); end; end;
*第一行build函数是一个递归的过程 参数a,b表示当前建立区间[a,b]的节点
*第四行 inc(tt) 就相当于新建一个节点 x纪录当前节点
*第六行 b-a=1 是递归的边界条件 即建立到叶子节点了
*第八 九 十行 根据线段树定义 分别递归建立左右儿子区间
-注意儿子指针的赋值 由于递归下去第一层就是建立当前节点的儿子 直接赋值为tt+1即可
-注意使用shr运算提高效率 还需注意a b mid 皆为区间端点
其实上文中建好的线段树其实是一个骨架
就相当于朴素做法中我们还未操作的空数组 等待我们给它刷颜色
既然要刷颜色 我们就得存储各区间的颜色 给每个节点新开一个域n来记录颜色
表现在数组模拟上就是新建数组n[]
n数组代表当前节点所代表区间的颜色
因为这个问题的染色是覆盖类型的染色
对一个区间染色自然把为当前区间的子区间也染色了
所以是对子树染色而非区间染色
接着这样的思路 我们可以写出如下程序
procedure insert(x,a,b,c:longint); var mid:longint; begin if (a<=l[x])and(r[x]<=b) then n[x]:=c; 6 mid:=(l[x]+r[x])shr 1; if a<mid then insert(ls[x],a,b,c); if b>mid then insert(rs[x],a,b,c); 9 end;
*第四 五行 判断当前区间是否在需要覆盖的区间内 是就修改颜色
*第六 七 八行 根据线段树定义判断是否递归左右子树染色
这里需要说明一下这种写法的正确性
即不会出现[a,b]在[l[x],r[x]]外与当前区间没有交集的情况
首先在根节点处[a,b]和区间显然有交集
然后运用数学归纳法的思路 说明当前节点区间和[a,b]有交集的时候 递归插入儿子也是保证和儿子区间有交集的
这样只要执行插入函数就有交集 就能保证程序正确性
给出所有和当前区间有交集的情况图 可以发现经过if语句判断 递归插入都保证还是和儿子区间有交集
(黑色为当前区间 红色为欲染色区间 一共6种情况)
不难分析出这个插入函数的复杂度是O(N)级别的(需要遍历子树) 从常数上看比朴素还慢
但是不覆盖子树上的区间又会产生错误 我们需要对插入进行改进
改进后 我们的n[]数组不单记录一个节点的颜色 而是记录的子树的颜色
我们看具体操作
*如果当前区间已经染色且颜色和欲染色一致 则直接退出(这句话可以不要)
*如果当前区间被完全覆盖 就说明子树也被完全覆盖了 直接给当前节点染色退出
*如果没有被完全覆盖
-就给先给左右儿子染色成当前节点的颜色 然后当前节点赋值为混合颜色=-1
-然后再递归染色左右子树
这样修改完全覆盖的区间时就可以直接修改然后退出 不用遍历子树了
而没有完全覆盖时 需要把颜色先下传给左右子树 再递归修改 保证子树颜色的正确性
这样我们访问的区间总数就降到了O(LogN)级别个 比O(N)好了不少
这个其实是一种最原始的Lazy-Tag思想
这种思想很重要 也比较难掌握 我们以后详细讨论
给出改进后的代码
procedure down(x:longint); begin if n[x]>0 then begin n[ls[x]]:=n[x]; n[rs[x]]:=n[x]; n[x]:=-1; end; end;
procedure insert(x,a,b,c:longint); var mid:longint; begin if (a<=l[x])and(r[x]<=b) then n[x]:=c else begin down(x); mid:=(l[x]+r[x])shr 1; if a<mid then insert(ls[x],a,b,c); if b>mid then insert(rs[x],a,b,c); end; end;
最后就是统计了
统计相对很简单 一共30种颜色 用个Simple Hash即可
这时候我们记录的混合颜色就有用了 用于判断
结构和插入差不多 不过递归的条件不再是是否有交集而是是否为空节点了
procedure count(x,a,b:longint); var mid:longint; begin if x=0 then exit; if n[x]>0 then inc(h[n[x]]) else begin mid:=(l[x]+r[x])shr 1; if a<mid then count(ls[x],a,b); if b>mid then count(rs[x],a,b); end; end;
最后是我的AC代码
const max=200000; var l,r,ls,rs,n:array[1..max]of longint; m,t,z,tt,k,i,x,y,ans:longint; h:array[1..30]of longint; ch:char; procedure build(a,b:longint); var x,mid:longint; begin inc(tt); x:=tt; l[x]:=a; r[x]:=b; if b-a>1 then begin mid:=(a+b)shr 1; ls[x]:=tt+1; build(a,mid); rs[x]:=tt+1; build(mid,b); end; end; procedure down(x:longint); begin if n[x]>0 then begin n[ls[x]]:=n[x]; n[rs[x]]:=n[x]; n[x]:=-1; end; end; procedure insert(x,a,b,c:longint); var mid:longint; begin if (a<=l[x])and(r[x]<=b) then n[x]:=c else begin down(x); mid:=(l[x]+r[x])shr 1; if a<mid then insert(ls[x],a,b,c); if b>mid then insert(rs[x],a,b,c); end; end; procedure count(x,a,b:longint); var mid:longint; begin if x=0 then exit; if n[x]>0 then inc(h[n[x]]) else begin mid:=(l[x]+r[x])shr 1; if a<mid then count(ls[x],a,b); if b>mid then count(rs[x],a,b); end; end; begin assign(input,'color.in'); reset(input); assign(output,'color.out'); rewrite(output); readln(m,t,k); tt:=0; build(0,m); n[1]:=1; while k>0 do begin dec(k); read(ch); read(x,y); if ch='C' then read(z); readln; case ch of 'C': insert(1,x-1,y,z); 'P': begin fillchar(h,sizeof(h),0); count(1,x-1,y); ans:=0; for i:=1 to t do if h[i]>0 then inc(ans); writeln(ans); end; end; end; close(input); close(output); end.
这就是线段树的入门题
接下来会具体讨论线段树的另一个经典问题
求矩形并面积和周长
需要用到线段删除和维护更多的域
BOB HAN原创 转载请注明出处 http://www.cnblogs.com/Booble/
相关文章推荐
- 线段树(一) _概述 基本操作
- 线段树(一) _概述 基本操作
- C++容器概述和序列式容器基本操作
- SQL Server基础之《视图的概述和基本操作》
- TYVJ 1427 线段树的基本操作
- 数据结构--树状数组&&线段树--基本操作
- Json概述以及python对json的基本操作
- SQL Server基础之《视图的概述和基本操作》
- HDU 1754线段树基本操作,建树,更新,查询
- TYVJ 1427 线段树的基本操作
- pku2777 线段树 (位运算加速)
- PKU 2777 - Count Color(线段树 + 区间修改)
- 线段树专辑——pku 2777 Count Color
- 线段树专题一:基本操作
- 【线段树】PKU2777 区间更新区间询问(位优化)
- 操作系统的基本概述
- 数据结构--线段树--基本操作
- 线段树基本操作(单点更新,区间更新,区间最大最小值,区间和)poj3468&poj3264&hdu1698&&hdu1166&hdu1754
- pku 2777Count Color 线段树 懒惰标记 位运算(位或)
- 有关线段树的基本操作模板