一、线段树
线段树既是线段也是树,并且是一棵二叉树,每个结点是一条线段,每条线段的左右儿子线段分别是该线段的左半和右半区间,递归定义之后就是一棵线段树。
例题:给定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 ",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 ", 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)这个函数更新上来
题意: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: ",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 ",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: ", 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 ", query(x, y, 1)); else if (cs[0] == 'A') update(x, y, 1); else update(x, -y, 1); } } return 0; }
八、Reference:
2、【完全版】线段树 http://www.notonlysuccess.com/index.php/segment-tree-complete/
4、数据结构:线段树