线段树 Interval Tree

一、线段树


线段树既是线段也是树,并且是一棵二叉树,每个结点是一条线段,每条线段的左右儿子线段分别是该线段的左半和右半区间,递归定义之后就是一棵线段树。

例题:给定N条线段,{[2, 5],
[4, 6], [0, 7]}, M个点{2, 4, 7},判断每个点分别在几条线段出现过?

1、构建线段树

2、处理线段

三条线段分割之后

3、查询

对于每一个值我们就可以开始遍历这一颗线段树,加上对于结点的count字段便是在线段中出现的次数

比如对于4,首先遍历[0, 7],次数 = 0+1=1;4在右半区间,遍历[4, 7],次数 = 1+0=0;4在[4, 7]左半区间, 次数
= 1+2=3;4在[4, 5]左半区间,次数 = 3+0 = 4,遍历结束,次数 =
3说明4在三条线段中出现过,同理可求其他的值,这一步的时间复杂度为O(M*log(MAX-MIN))

二、线段树的存储数据结构


储一颗线段树和二叉树有点类似,需要左孩子和右孩子节点,另外,为了存储每条线段出现的次数,所以一般会加上计数的元素。


struct Node         // 线段树
{
int left;
int right;
int counter;
}segTree[4*BORDER];

三、线段树支持的操作


一颗线段树至少支持以下四个操作:

  • void construct(int index, int lef, int rig),构建线段树
    根节点开始构建区间[lef,rig]的线段树

  • void insert(int index, int start, int end),插入线段[start,end]到线段树,
    同时计数区间次数

  • int query(int index, int
    x),查询点x的出现次数,从根节点开始到[x,x]叶子的这条路径中所有点计数相加方为x出现次数

  • void delete_ (int c , int d, int index),从线段树中删除线段[c,d]

四、线段树的特征


1、线段树的深度不超过logL(L是最长区间的长度)

2、线段树把区间上的任意一条线段都分成不超过2logL条线段。

线段树能在O(logL)的时间内完成一条线段的插入、删除、查找等工作。

五、线段树的应用


线段树适用于和区间统计有关的问题。比如某些数据可以按区间进行划分,按区间动态进行修改,而且还需要按区间多次进行查询,那么使用线段树可以达到较快查询速度。

(1):区间最值查询问题
(见模板1)

(2):连续区间修改或者单节点更新的动态查询问题
(见模板2)

(3):多维空间的动态查询
(见模板3)

六、模板代码


模板1:

RMQ,查询区间最值下标---min


#include<iostream>

using namespace std;

#define MAXN 100
#define MAXIND 256 //线段树节点个数

//构建线段树,目的:得到M数组.
void build(int node, int b, int e, int M[], int A[])
{
if (b == e)
M[node] = b; //只有一个元素,只有一个下标
else
{
build(2 * node, b, (b + e) / 2, M, A);
build(2 * node + 1, (b + e) / 2 + 1, e, M, A);

if (A[M[2 * node]] <= A[M[2 * node + 1]])
M[node] = M[2 * node];
else
M[node] = M[2 * node + 1];
}
}

//找出区间 [i, j] 上的最小值的索引
int query(int node, int b, int e, int M[], int A[], int i, int j)
{
int p1, p2;

//查询区间和要求的区间没有交集
if (i > e || j < b)
return -1;

if (b >= i && e <= j)
return M[node];

p1 = query(2 * node, b, (b + e) / 2, M, A, i, j);
p2 = query(2 * node + 1, (b + e) / 2 + 1, e, M, A, i, j);

//return the position where the overall
//minimum is
if (p1 == -1)
return M[node] = p2;
if (p2 == -1)
return M[node] = p1;
if (A[p1] <= A[p2])
return M[node] = p1;
return M[node] = p2;

}

int main()
{
int M[MAXIND]; //下标1起才有意义,否则不是二叉树,保存下标编号节点对应区间最小值的下标.
memset(M,-1,sizeof(M));
int a[]={3,4,5,7,2,1,0,3,4,5};
build(1, 0, sizeof(a)/sizeof(a[0])-1, M, a);
cout<<query(1, 0, sizeof(a)/sizeof(a[0])-1, M, a, 0, 5)<<endl;
return 0;
}

模板2:

连续区间修改或者单节点更新的动态查询问题
(此模板查询区间和)


#include <cstdio>
#include <algorithm>
using namespace std;

#define lson l , m , rt << 1
#define rson m + 1 , r , rt << 1 | 1
#define root 1 , N , 1
#define LL long long
const int maxn = 111111;
LL add[maxn<<2];
LL sum[maxn<<2];
void PushUp(int rt) {
sum[rt] = sum[rt<<1] + sum[rt<<1|1];
}
void PushDown(int rt,int m) {
if (add[rt]) {
add[rt<<1] += add[rt];
add[rt<<1|1] += add[rt];
sum[rt<<1] += add[rt] * (m - (m >> 1));
sum[rt<<1|1] += add[rt] * (m >> 1);
add[rt] = 0;
}
}
void build(int l,int r,int rt) {
add[rt] = 0;
if (l == r) {
scanf("%lld",&sum[rt]);
return ;
}
int m = (l + r) >> 1;
build(lson);
build(rson);
PushUp(rt);
}
void update(int L,int R,int c,int l,int r,int rt) {
if (L <= l && r <= R) {
add[rt] += c;
sum[rt] += (LL)c * (r - l + 1);
return ;
}
PushDown(rt , r - l + 1);
int m = (l + r) >> 1;
if (L <= m) update(L , R , c , lson);
if (m < R) update(L , R , c , rson);
PushUp(rt);
}
LL query(int L,int R,int l,int r,int rt) {
if (L <= l && r <= R) {
return sum[rt];
}
PushDown(rt , r - l + 1);
int m = (l + r) >> 1;
LL ret = 0;
if (L <= m) ret += query(L , R , lson);
if (m < R) ret += query(L , R , rson);
return ret;
}
int main() {
int N , Q;
scanf("%d%d",&N,&Q);
build(root);
while (Q --) {
char op[2];
int a , b , c;
scanf("%s",op);
if (op[0] == ‘Q‘) {
scanf("%d%d",&a,&b);
printf("%lld\n",query(a , b ,root));
} else {
scanf("%d%d%d",&a,&b,&c);
update(a , b , c , root);
}
}
return 0;
}

模板3:

多维空间的动态查询

(留待填充)

模板4:用指针构建的线段树


#include <iostream>
using namespace std;
struct Line{
int left, right, count;
Line *leftChild, *rightChild;
Line(int l, int r): left(l), right(r) {}
};

//建立一棵空线段树
void createTree(Line *root) {
int left = root->left;
int right = root->right;
if (left < right) {
int mid = (left + right) / 2;
Line *lc = new Line(left, mid);
Line *rc = new Line(mid + 1, right);
root->leftChild = lc;
root->rightChild = rc;
createTree(lc);
createTree(rc);
}
}

//将线段[l, r]分割
void insertLine(Line *root, int l, int r) {
cout << l << " " << r << endl;
cout << root->left << " " << root->right << endl << endl;
if (l == root->left && r == root->right) {
root->count += 1;
} else if (l <= r) {
int rmid = (root->left + root->right) / 2;
if (r <= rmid) {
insertLine(root->leftChild, l, r);
} else if (l >= rmid + 1) {
insertLine(root->rightChild, l, r);
} else {
int mid = (l + r) / 2;
insertLine(root->leftChild, l, mid);
insertLine(root->rightChild, mid + 1, r);
}
}
}
//树的中序遍历(测试用)
void inOrder(Line* root) {
if (root != NULL) {
inOrder(root->leftChild);
printf("[%d, %d], %d\n", root->left, root->right, root->count);
inOrder(root->rightChild);
}
}

//获取值n在线段上出现的次数
int getCount(Line* root, int n) {
int c = 0;
if (root->left <= n&&n <= root->right)
c += root->count;
if (root->left == root->right)
return c;
int mid = (root->left + root->right) / 2;
if (n <= mid)
c += getCount(root->leftChild, n);
else
c += getCount(root->rightChild, n);
return c;
}
int main() {
int l[3] = {2, 4, 0};
int r[3] = {5, 6, 7};
int MIN = l[0];
int MAX = r[0];
for (int i = 1; i < 3; ++i) {
if (MIN > l[i]) MIN = l[i];
if (MAX < r[i]) MAX = r[i];
}
Line *root = new Line(MIN, MAX);
createTree(root);
for (int i = 0; i < 3; ++i) {
insertLine(root, l[i], r[i]);
}
inOrder(root);
int N;
while (cin >> N) {
cout << getCount(root, N) << endl;
}
return 0;
}

七、ACM题


在代码前先介绍一些线段树风格:

  • maxn是题目给的最大区间,而节点数要开4倍,确切的来说节点数要开大于maxn的最小2x的两倍

  • lson和rson分辨表示结点的左儿子和右儿子,由于每次传参数的时候都固定是这几个变量,所以可以用预定于比较方便的表示

  • 以前的写法是另外开两个个数组记录每个结点所表示的区间,其实这个区间不必保存,一边算一边传下去就行,只需要写函数的时候多两个参数,结合lson和rson的预定义可以很方便

  • PushUP(int rt)是把当前结点的信息更新到父结点

  • PushDown(int rt)是把当前结点的信息更新给儿子结点

  • rt表示当前子树的根(root),也就是当前所在的结点

整理这些题目后我觉得线段树的题目整体上可以分成以下四个部分:

(1)单点更新:

最最基础的线段树,只更新叶子节点,然后把信息用PushUP(int r)这个函数更新上来

hdu1166 敌兵布阵

题意:O(-1)

思路:O(-1)

线段树功能:update:单点增减 query:区间求和

code 1:


#include<cstring>
#include<iostream>

#define M 50005
#define lson l,m,rt<<1
#define rson m+1,r,rt<<1|1
/*left,right,root,middle*/

int sum[M<<2];

inline void PushPlus(int rt)
{
sum[rt] = sum[rt<<1] + sum[rt<<1|1];
}

void Build(int l, int r, int rt)
{
if(l == r)
{
scanf("%d", &sum[rt]);
return ;
}
int m = ( l + r )>>1;

Build(lson);
Build(rson);
PushPlus(rt);
}

void Updata(int p, int add, int l, int r, int rt)
{

if( l == r )
{
sum[rt] += add;
return ;
}
int m = ( l + r ) >> 1;
if(p <= m)
Updata(p, add, lson);
else
Updata(p, add, rson);

PushPlus(rt);
}

int Query(int L,int R,int l,int r,int rt)
{
if( L <= l && r <= R )
{
return sum[rt];
}
int m = ( l + r ) >> 1;
int ans=0;
if(L<=m )
ans+=Query(L,R,lson);
if(R>m)
ans+=Query(L,R,rson);

return ans;
}
int main()
{
int T, n, a, b;
scanf("%d",&T);
for( int i = 1; i <= T; ++i )
{
printf("Case %d:\n",i);
scanf("%d",&n);
Build(1,n,1);

char op[10];

while( scanf("%s",op) &&op[0]!=‘E‘ )
{

scanf("%d %d", &a, &b);
if(op[0] == ‘Q‘)
printf("%d\n",Query(a,b,1,n,1));
else if(op[0] == ‘S‘)
Updata(a,-b,1,n,1);
else
Updata(a,b,1,n,1);

}
}
return 0;
}

code 2:


#include <iostream>
#include <cstdio>

using namespace std;

const int NMAX = 50005;
int n;
int a[NMAX];
struct Node {
int left, right, sum;
};
Node node[4*NMAX];

void push_up(int pos) {
node[pos].sum = node[pos<<1].sum + node[(pos<<1)+1].sum;
}

void build(int left, int right, int pos) {
node[pos].left = left;
node[pos].right = right;
if (left == right) {
node[pos].sum = a[left];
return;
}
int mid = (left + right) >> 1;
build(left, mid, pos<<1);
build(mid+1, right, (pos<<1)+1);
push_up(pos);
}

void update(int index, int val, int pos) {
if (node[pos].left == node[pos].right) {
node[pos].sum += val;
return;
}
int mid = (node[pos].left + node[pos].right) >> 1;
if (index <= mid) update(index, val, pos<<1);
else update(index, val, (pos<<1)+1);
push_up(pos);
}

int query(int left, int right, int pos) {
if (node[pos].left == left && node[pos].right == right) return node[pos].sum;
int mid = (node[pos].left + node[pos].right) >> 1;
if (left > mid) return query(left, right, (pos<<1)+1);
else if (right <= mid) return query(left, right, pos<<1);
else return query(left, mid, pos<<1) + query(mid+1, right, (pos<<1)+1);
}

int main()
{
int T;
scanf("%d", &T);
for (int i=0; i<T; i++) {
scanf("%d", &n);
for (int j=1; j<=n; j++) {
scanf("%d", a+j);
}

printf("Case %d:\n", i+1);
build(1, n, 1);
getchar();
int x, y;
char cs[10];
while (scanf("%s",cs) && cs[0] != ‘E‘) {
scanf("%d%d%*c", &x, &y);
if (cs[0] == ‘Q‘) printf("%d\n", query(x, y, 1));
else if (cs[0] == ‘A‘) update(x, y, 1);
else update(x, -y, 1);
}
}
return 0;
}

 

八、Reference:


1、菜鸟都能理解的线段树入门经典

2、【完全版】线段树 http://www.notonlysuccess.com/index.php/segment-tree-complete/

3、线段树(segment
tree)

4、数据结构:线段树

5、数据结构专题——线段树

6、线段树专题【暂停更新中】

时间: 2024-10-23 11:20:22

线段树 Interval Tree的相关文章

【数据结构】线段树(interval tree)

线段树(interval tree),也叫区间树.也是一种二叉搜索树,同一般的BST不同之处在于:线段树的每一个结点包含的是一个区间而不是一个数.具体的描述如下: 从图上可以看出,线段树的每一个结点都是一个线段(区间),子节点是对父结点的进一步分划,每个子节点的长度都是父节点的二分,每个叶子结点就是一个元素. 每个节点可以用一个变量hit_count来计算在每一段的命中率,这样可以用来统计此线段线段或者区间内的命中率. 区间树主要用在一些跟统计和分部相关的计算中,可以快速找到相应的数据.

线段树(segment tree)

1.概述 线段树,也叫区间树,是一个完全二叉树,它在各个节点保存一条线段(即"子数组"),因而常用于解决数列维护问题,基本能保证每个操作的复杂度为O(lgN). 线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点. 对于线段树中的每一个非叶子节点[a,b],它的左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b].因此线段树是平衡二叉树,最后的子节点数目为N,即整个线段区间的长度. 使用线段树可以

线段树(Segment Tree)(转)

原文链接:线段树(Segment Tree) 1.概述 线段树,也叫区间树,是一个完全二叉树,它在各个节点保存一条线段(即“子数组”),因而常用于解决数列维护问题,基本能保证每个操作的复杂度为O(lgN). 线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点. 对于线段树中的每一个非叶子节点[a,b],它的左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b].因此线段树是平衡二叉树,最后的子节点数目为N,即

线段树(I tree)

Codeforces Round #254 (Div. 2)E题这题说的是给了一个一段连续的区间每个区间有一种颜色然后一个彩笔从L画到R每个区间的颜色都发生了 改变然后 在L和R这部分区间里所用的颜色变成了x 然后每个区间的 色度加上abs(x-Yi) Yi 为原位置的颜色,还有一个操作就是求 L 到 R 的距离之内的所有的点和,数据 有 n<=100000 m<100000 次操作 对于每次第二种操作输出, 自然我们将一个区间的颜色如果相同自然将他们 用延迟标记 但是 会有一个问题就是在一个

线段树之入门篇

线段树(interval tree) 是把区间逐次二分得到的一树状结构,它反映了包括归并排序在内的很多分治算法的问题求解方式. 上图是一棵典型的线段树,它对区间[1,10]进行分割,直到单个点.这棵树的特点 是: 1. 每一层都是区间[a, b]的一个划分,记 L = b - a 2. 一共有log2L层 3. 给定一个点p,从根到叶子p上的所有区间都包含点p,且其他区间都不包含点p. 4. 给定一个区间[l; r],可以把它分解为不超过2log2 L条不相交线段的并. 其中第四点并不是很显然,

线段树入门整理、

线段树(interval tree) 是把区间逐次二分得到的一树状结构,它反映了包括归并排序在内的很多分治算法的问题求解方式. [声明] 1 #include<cstdio> 2 #include<cmath> 3 const int MAXNODE = 2097152; 4 const int MAX = 1000003; 5 struct NODE{ 6 int left,right; // 区间[left,right] . 7 int value; // 节点区间对应的权值.

线段树(一)

问题: 先抛出一个问题,坐标轴上有若干线段,现在给定若干个点,对于每个点,求出包含点的线段的数量 如果用常规的解法,时间复杂度是O(mn),空间复杂度是O(m + n) 能不能降低一下时间复杂度呢?答案是肯定的,这些线段里有大量相交或者覆盖的线段,而上面的解法显然没有利用这些信息,导致时间复杂度较高,现在我们引入 一个数据结构,线段树(Segment Tree),从Wikipedia抄过来定义如下: In computer science, a segment tree is a tree da

hdu4521 小明系列问题——小明序列(LIS变种 (线段树+单点更新解法))

链接: huangjing 题目:中文题目 思路: 这个题目如果去掉那个距离大于d的条件,那么必然是一个普通的LIS,但是加上那个条件后就变得复杂了.用dp的解法没有看懂,我用的线段树的解法...就是采用延迟更新的做法,用为距离要大于d啊,所以我们在循环到第i的时候,就对(i-d-1)这个点进行更新,因为如果在(i-d-1)这个点更新了,会对后面的造成影响,然后线段树的tree[]数组存的是以i结尾的最长lis,那么每次询问的时候就找最大的tree[]就可以了... 代码: 小明系列问题--小明

【POJ 2528】Mayor’s posters(线段树+离散化)

题目 给定每张海报的覆盖区间,按顺序覆盖后,最后有几张海报没有被其他海报完全覆盖.离散化处理完区间端点,排序后再给相差大于1的相邻端点之间再加一个点,再排序.线段树,tree[i]表示节点i对应区间是哪张海报,如果是-1代表对应区间不是一张海报(0或多张).每贴一张海报,就用二分查找出覆盖的起点和终点对应的离散后的下标,然后更新区间.线段树的区间更新可以加上懒惰标记(或延迟标记,但是这题可以不用另外标记. #include<cstdio> #include<cstring> #in