线段树和树状数组在很多时候都可以用来处理相同的问题,特别是在用来进行RMQ离线处理时候两者各有所长,故放在一起整理。
首先是线段树,线段树除了最后一层子节点整体是一颗标准的完全树,所以有着许多很有趣的特点,在区域搜索、区域数值增改中有着很大的优势,先上一道水题
poj3264 线段树
题意是对给出的Q次访问求出访问区间中数值的最大差值,正好与线段树的区间搜索相符。
本意是想用RMQ离线处理一下,但是因为本身不存在增改,离线处理也不会增加多少效率,所以就直接写了
#include <iostream> #include <cstdio> #include <cstring> #include <cmath> #include <string> #include <algorithm> #include <queue> #include <vector> #include <map> #include <list> using namespace std; #define N 50005 #define Q 200005 int n,q; int mi,ma; struct tree { int l,r; int mi,ma; }s[Q * 2]; int c[N]; void update(int wei) { int l = s[wei].l; int r = s[wei].r; if(l == r) { s[wei].ma = s[wei].mi = c[l]; return; } int ls = wei * 2; int rs = ls + 1; int mid = (l + r) / 2; s[ls].l = l; s[ls].r = mid; update(ls); s[rs].l = mid + 1; s[rs].r = r; update(rs); s[wei].ma = max(s[ls].ma,s[rs].ma); s[wei].mi = min(s[ls].mi,s[rs].mi); } void cal(int wei,int l,int r) { if(l == s[wei].l && r == s[wei].r) { mi = min(mi,s[wei].mi); ma = max(ma,s[wei].ma); return; } int mid = (s[wei].l + s[wei].r) / 2; if(mid >= r) cal(wei * 2,l,r); else if(mid < l) cal(wei*2+1,l,r); else { cal(wei*2,l,mid); cal(wei*2+1,mid+1,r); } } void out(int a,int b) { mi = 1000005; ma = 0; cal(1,a,b); printf("%d\n",ma - mi); } int main() { scanf("%d%d",&n,&q); for(int i=1;i<=n;i++) { scanf("%d",&c[i]); } s[1].l = 1; s[1].r = n; update(1); int a,b; for(int i=0;i<q;i++) { scanf("%d%d",&a,&b); out(a,b); } return 0; }
多校第四场hdu4638Group 线段树 + RMQ
题意是对给出访问求区域内能成立的组的个数(相邻点数之差为1可以加进一个组、一个数与相邻组存在差为1数的时候可以加入这个组)
因为组的情况不定,每次询问都要对线段树内的内容更新,这时就需要RMQ离线处理了,按照左位置或者右位置排序后按序处理整体输出
因为这样子写出出来好多变量与函数(有建树用的和离线运算一起更新所需要的变量与函数,按我的写法至少需要四个函数与十多个变量)写到后来很容易就乱了,以后需要好好研究怎么定义变量名以实现程序内容的清晰化。
#include <iostream> #include <cstdio> #include <cstring> #include <cmath> #include <algorithm> #include <string> #include <vector> #include <queue> #include <list> #include <map> using namespace std; #define N 100005 struct tree { int l,r; int ans; }s[N * 4]; int c[N]; int wei[N]; int n,m; struct node { int l,r; int wei; }sum[N]; int ans[N]; void update(int w) { s[w].ans = 0; int l = s[w].l; int r = s[w].r; if(l == r) return; int ls = w * 2; int rs = ls + 1; int mid = (l + r) / 2; s[ls].l = l; s[ls].r = mid; update(ls); s[rs].l = mid + 1; s[rs].r = r; update(rs); } bool cmp(node x,node y) { return x.r < y.r; } void cal(int w,int l,int r,int shu) { if(l > r) return; if(s[w].l == l&&s[w].r == r) { s[w].ans += shu; return; } int ls = w * 2; int rs = ls + 1; int mid = (s[w].l + s[w].r) / 2; s[ls].ans += s[w].ans; s[rs].ans += s[w].ans; s[w].ans = 0; if(l > mid) cal(rs,l,r,shu); else if(r <= mid) cal(ls,l,r,shu); else { cal(ls,l,mid,shu); cal(rs,mid+1,r,shu); } } int add(int w,int l) { if(s[w].l == s[w].r) return s[w].ans; int ls = w * 2; int rs = ls + 1; int mid = (s[w].l + s[w].r) / 2; s[ls].ans += s[w].ans; s[rs].ans += s[w].ans; s[w].ans = 0; if(l > mid) return add(rs,l); else return add(ls,l); } int main() { int t; scanf("%d",&t); while(t--) { scanf("%d%d",&n,&m); memset(wei,0,sizeof(wei)); for(int i=1;i<=n;i++) { scanf("%d",&c[i]); } s[1].l = 1; s[1].r = m; update(1); for(int i=0;i<m;i++) { scanf("%d%d",&sum[i].l,&sum[i].r); sum[i].wei = i; } sort(sum,sum + m,cmp); int i = 1; int j = 0; while(j < m&&i <= n) { int x = c[i]; cal(1,1,i-1,1); if(wei[x - 1]) cal(1,1,wei[x - 1],-1); if(wei[x + 1]) cal(1,1,wei[x + 1],-1); wei[x] = i; cal(1,i,i,1); while(j < m && sum[j].r <= i) { ans[sum[j].wei] = add(1,sum[j].l); j++; } i++; } for(int i=0;i<m;i++) { printf("%d\n",ans[i]); } } return 0; }
除去二维的,上面的就是线段树大多数的基本用法了,之后说说树状数组。
树状数组是基于二进制数做出来的数据访问的优化方法,所以每次定义的时候必须从1开始(线段树如果想用齐满树的特征来省变量也需要从1开始)
如果说线段树最大的优势是区间访问与更新,那树状数组最大的优势就是对单点的访问与更新。
同样以hdu4638Group为例,只要把中间的线段树部分改成了树状数组就可以了。
并且因为实际上这题都是对点更新所以时间效率还要高一点,但是和线段树有着同一个问题(树状数组有固有的lowbit函数与更新的update函数,与线段树的的建树update/maketree函数以及更新的cal/update正好凑成一对)函数变量很多,写的很晕。。
#include <iostream> #include <cstdio> #include <cstring> #include <cmath> #include <string> #include <algorithm> #include <queue> #include <vector> #include <map> #include <list> using namespace std; #define N 100005 int n,m; int a[N]; int s[N]; int num[N]; int ans[N]; struct node { int l,r; int wei; }c[N]; int lowbit(int x) { return x&(-x); } void update(int wei,int shu) { while(wei <= n) { s[wei] += shu; wei += lowbit(wei); } } int sum(int wei) { int ans = 0; int i = wei; while(i > 0) { ans += s[i]; i -= lowbit(i); } return ans; } bool cmp(node x,node y) { return x.l < y.l; } int main() { int t; scanf("%d",&t); while(t--) { scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) { scanf("%d",&a[i]); num[a[i]] = i; } num[0] = num[n+1] = N; memset(s,0,sizeof(s)); for(int i=1;i<=n;i++) { if(i < num[a[i] - 1] && i < num[a[i] + 1]) update(i,1); if(i > num[a[i] - 1] && i > num[a[i] + 1]) update(i,-1); } int l,r; for(int i=0;i<m;i++) { scanf("%d%d",&l,&r); c[i].l = l; c[i].r = r; c[i].wei = i; } sort(c,c+m,cmp); int i = 1; int j = 0; while(j < m) { //cout<<j<<' '<<i<<endl; while(i <= n && i < c[j].l) { //cout<<i<<'i'<<endl; if(i > num[a[i]-1] && i > num[a[i]+1]) update(i,-1); else if(i < num[a[i]-1] && i < num[a[i]+1]) { l = min(num[a[i]-1],num[a[i]+1]); r = max(num[a[i]-1],num[a[i]+1]); update(i,-1); update(l,1); update(r,1); } else if(i < num[a[i]-1]) { update(i,-1); update(num[a[i]-1],1); } else { update(i,-1); update(num[a[i]+1],1); } i++; } while( j < m && c[j].l <= i) { //cout<<j<<'j'; ans[c[j].wei]= sum(c[j].r); j++; } } for(int i=0;i<m;i++) { printf("%d\n",ans[i]); } } return 0; }
总的来说,树状数组能处理的问题基本线段树都能处理,而线段树能处理的问题范围要比树状数组广,很多时候两者时间仅仅只是效率差而已,择优而用很重要~~^_^~~