这几天反正磕磕绊绊吧这两部分东西整完了,确切的说是基础给整完了。。。。那就意味着出现套模版的题还是能做上,稍微变形那还是白扯。。
首先弄的是树状数组,树状数组,把一个区间利用位的方式分割成多个其他区间的和,然后更新期中一个时候可以用较少的操作次数来更新整体的每个的值,比如区间和
int lowbit(int x) { return x&(-x); } void update(int x,int val) { while(x<MAX) { c[x]+=val; x+=lowbit(x); } } int get(int x) { int sum=0; while(x>0) { sum+=c[x]; x-=lowbit(x); } return sum; }
lowbit函数用来求一个数的二进制中,最右边的那个1所代表的十进制数值是多少...这样就按照分拆1的方式把0-x这个区间的操作转化成了某几个值上的操作
update每次用来更新与当前值有关的数的和
get获取查询所给区间的值
在这里转一段话,帮助理解:
c[k]存储的实际上是从k开始向前数k的二进制表示中右边第一个1所代表的数字个元素的和(这么说可能有点拗口,令lowbit为k的二进制表示中右边第一个1所代表的数字,然后c[k]里存的就是从a[k]开始向前数lowbit个元素之和)这么存有什么好处呢?无论是树状数组还是线段树,都用到了分块的思想,而树状数组采用这样的存储结构我想最主要的还是这样方便计算,我们可以用位运算轻松地算出lowbit.分析一下这样做的复杂度:对于更改元素来说,如果第i个元素被修改了,因为我们最终还是要求和,所以可以直接在c数组里面进行相应的更改,如图中的例子,假设更改的元素是a[2],那么它影响到得c数组中的元素只有c[2],c[4],c[8],我们只需一层一层往上修改就可以了,这个过程的最坏的复杂度也不过O(logN);对于查找来说,如查找s[k],只需查找k的二进制表示中1的个数次就能得到最终结果,比如查找s[7],7的二进制表示中有3个1,也就是要查找3次,到底是不是呢,我们来看上图,s[7]=c[7]+c[6]+c[4],可能你还不知道怎么实现这个过程.
还以7为例,二进制为0111,右边第一个1出现在第0位上,也就是说要从a[7]开始向前数1个元素(只有a[7]),即c[7];
然后将这个1舍掉,得到6,二进制表示为0110,右边第一个1出现在第1位上,也就是说要从a[6]开始向前数2个元素(a[6],a[5]),即c[6];
然后舍掉用过的1,得到4,二进制表示为0100,右边第一个1出现在第2位上,也就是说要从a[4]开始向前数4个元素(a[4],a[3],a[2],a[1]),即c[4].
树状数组用来解决RMQ问题特别有效二维树状数组就是加了个x,y坐标的循环更新,在这里不介绍了
线段树:
线段树其实稍微好理解一点,但是应用太难了。。。它是把一个区间转换成一个平衡树,类似二分查找树,
建树有两种,一种是到叶子节点l==r-1时候结束,一种是l==r结束,我比较喜欢第一种
void build(int t,int l,int r)//第一种建法 { node[t].l = l; node[t].r = r; node[t].sum = 0; if( l == r - 1 ) { node[t].sum = 1;//求和的话在这里给叶子节点赋值 return ; } int mid = MID(l,r); build(L(t),l,mid); build(R(t),mid,r); //node[t].sum=node[L(t)].sum+node[R(t)].sum;//这里计算区间的和 }
第二种的图片,第一种就是把,题目中的所有叶子节点去掉即可
每个叶子节点保留的都是题目中的每个数据节点(比如某一个数字,染色的某个一个起始号),
对某个区间进行操作的时候从根节点开始往下查找
一般来讲线段树的应用比较多
RMQ,线段求长,矩形交,矩形并等……
RMQ问题在这里分几种特殊的,首先是比较基础的套模版就不再说了其次是更新区间求区间和问题,这里需要维护一个变量就是增量,否则会超时见具体题目结题报告
重点介绍的就是矩形并问题
矩形并问题主要用到的思想是离散化+扫描
首先把每个矩形的x和y坐标分别存储,一个矩形分成两部分来存储,其实也就是把矩形左右两条高的位置如图a,b分开来存储rr结构体数组(这个结构体存储高a(x1,y1,y2)),然后更新。。。把y坐标给离散化了
/*结构体存储*/ struct Tnode {int l,r,cover;//cover存储一个状态标识当前这个矩形是否更新完毕,如果只有左边这条高更新完就是1,否则就是0 double length;//当前节点的这个区间内,矩形包含面积的有效高度和是多少 };
根据图可知道,按照x由小到达依次更新,更新一次就可以获得临近两个x区间内的阴影部分的有效高度是多少了

1 void len(int t) 2 { 3 if( node[t].cover > 0 )//如果当前a更新了b还没更新,表明a的高还可以使用 4 node[t].length = y[node[t].r] - y[node[t].l];//区间内的有效高度就等于a的高 5 else 6 if( node[t].l == node[t].r - 1 )//否则如果b更新了并且更新到当前节点了,说明已经扫描到这个矩形结尾,有效高度为0 7 node[t].length = 0.0; 8 else 9 node[t].length = node[R(t)].length + node[L(t)].length;//区间内的有效高度就等于两部分的和 10 } 11 void update(int t,Rec p) 12 { 13 if( y[node[t].l] == p.y1 && y[node[t].r] == p.y2 )//刚好更新某个矩形的区间 14 { 15 node[t].cover += p.flag;//标志 16 len(t); 17 return ; 18 } 19 int mid = MID(node[t].l,node[t].r);//否则按照一般的线段树开始更新求出当前区间的有效高度 20 if( dyd(p.y1,y[mid]) ) 21 update(R(t),p); 22 else 23 if( xyd(p.y2,y[mid]) ) 24 update(L(t),p); 25 else 26 { 27 Rec tmp = p; 28 tmp.y2 = y[mid]; 29 update(L(t),tmp); 30 tmp = p; 31 tmp.y1 = y[mid]; 32 update(R(t),tmp); 33 } 34 len(t); 35 }
有了上面的条件,就可以按照x由小到大开始来更新,但是必须计算一次再把当前的边更新掉。。。
ouble solve(int n,int cnt) { init(); build(1,0,cnt-1); double sum = 0.0; update(1,rr[0]); for(int i=1; i<n; i++) { sum += (rr[i].x - rr[i-1].x)*node[1].length;//node[1].length每次返回当前x小区间内的有效总高度是多少 update(1,rr[i]); } return sum; }
至此,两种树的简单学习也就到这里了
两种的应用,首先如果一般的RMQ问题,对一个数的操作,询问整个区间的话,树状数组都能解决,并且是最简答的办法
如果是对某个区间的操作的话,线段树处理起来有效,因为它对区间的操作比较快,但是简单数据量也可以用树状数组,把区间内的每个数都循环更新一次即可
另外线段树可以用来处理矩形交并面积,矩形周长等等