hdu 1698 Just a Hook
线段树:一整段区间修改数值,并询问一段区间的和(不过这里询问的整个区间的和,固定的,当然原理是一样)
这题要用到LAZY标记,决定自己写一下LAZY标记
先说题意:一个连续的线段,材料可能为金银铜,数值对应3,2,1,一开始所有单元都是铜,所以整段的和就是n。然后多个修改操作,每次为x,y,z把区间[x,y]的每个单元都变为数值z。z的值为1,2,3。所有的修改操作做完后,输出整个线段的和
然后说一下LAZY思想的本质,就是“只做表面功夫,内部其实不合格,当某一次需要访问到内部的时候,再对内部处理”,这种偷懒的思想就能减少操作的次数,因为每次访问到线段树的深度不一定一样,不需要全部都处理掉。
这个思想是基于线段树的性质的,我们知道线段树的一个结点代表的区间其实包含了它的所有子孙后代结点的区间,所以按照普通的做法,更改了该结点整段区间的信息,其子孙后代的信息要全部改变。下面举一个例子说明这个简单的道理
一个线段树[1,2],那么它有两个孩子[1,1],[2,2]
初始建树的时候,每个单元的数值为1。第一次修改,使[1,2]区间内每个单元的数值变为2,第二次修改,使[2,2]区间内每个单元的数值为3,最后问[1,2]区间和
普通的思路(全部处理)
1.第一次修改,使[1,2]区间内每个单元的数值变为2------->[1,2]=4,[1,1]=2,[2,2]=2;
2.第二次修改,使[2,2]区间内每个单元的数值为3--------->[1,2]=5,[1,1]=2,[2,2]=3;
3.可以直接输出[1,2]区间和为5
可以看到,每个结点的信息都是准确无误的,这当然好,但是需要付出很多时间(当结点数很大时),下面看LAZY思想处理的结果
1.第一次修改,使[1,2]区间内每个单元的数值变为2------->[1,2]=4,[1,1]=1,[2,2]=1;
(这里就可以看到,其子孙后代的信息并没有修改,这个就是做“表面功夫”,因为[1,2]在两个孩子上面,一旦修改了[1,2]就不再往下走,而实际上下面的信息并不是准确的)
2.第二次修改,使[2,2]区间内每个单元的数值为3--------->[1,2]=x,[1,1]=2,[2,2]=3;
(这里为什么[1,2]=x,意思是我们现在并不能知道[1,2]的值,为什么?而[1,1]=2居然变准确了,为什么?而[2,2]=3也变准确了,为什么?)
3.需要处理一次才能得到[1,2]的和(看上面那点,我们并不知道[1,2]的值,就是要经过处理才能知道的)
我们就以本题为例,说一下怎么实现,看看怎么设置线段树的结点
struct node
{
int a,b; //表示该结点覆盖的区间
int sum; //该区间的和,可以省略
int col; //最重要的量,表示该结点(对应的区间)的数值类型,本题就是1,2,3,还有0,0的意义后面说
}
col的意义很重要,本题中数值类型只有1,2,3,col=1/2/3,表示该结点对应的区间全部是该种类型(及区间内所有单元都是),若col=0表示该区间内并不是清一色的全部是一种颜色,而是有两种或者多种颜色,至于具体的信息我们不知道,我们只知道不止一种颜色。
我们怎么计算一个区间的和?如果该区间都是一种颜色,sum=(b-a+1)*col,因为每个单元都是这个数值。但是如果该区间不止一个颜色,我们不能直接计算,只能依靠左右孩子的值,亲(sum)=左孩子(sum)+右孩子(sum),事实上即便是第一种情况我们也是可以这样计算的。
LAZY用于处理“由浅至深”的修改。例子说明问题
当前结点[2,4]的col是3,要进行一个修改[3,3]变为1。根据线段树的性质,我们知道要到达[3,3]是必须经过[2,4]的。
到达[2,4]时我们发现它的类型是3,我们的类型是1,说明[2,4]的纯洁性将被破坏(该区间内将不止一种颜色)。[2,4]的左右孩子分别为[2,3],[4,4],显然我们应该去左孩子那边,不用去右孩子那边。这时候有一个关键的处理,本来[2,4]是纯洁的为3,那么我们知道[4,4]也应该是3,当然[2,3]也应该是3,所以我们记录[2,3],[4,4]的类型为3,并且计算它们的区间和(其实记录类型已经足够了,不一定需要计算区间和,所以我们在上面结构体定义中就写到,sum这个域是可以省略的)
做完这个操作,可以往左孩子方向走了吗?还不行,记得把[2,3]的类型修改为0,表示[2,3]已经不纯洁了。
接着,我们来到了[2,3],它的左右孩子为[2,2],[3,3],我们显然是往右孩子走。由于上面说了[2,3]的类型为3,可以知道[2,2],[3,3]的类型也必定是3,所以同样的我们要记录[2,2],[3,3]的类型为3,并计算它的区间和,然后把[2,3]类型变为0,表示它不纯洁了
最后我们呢来到了[3,3],是我们的目标区间了,我们当然把[3,3]的类型改为1,然后计算它的区间和。接下来不需要再往下走了,也就是不需要再理会它的子孙后代,直接结束整个修改算法(虽然在这里[3,3]并没有孩子,但是其他有孩子的情况下也不要再往走了,这个就是LAZY)
上面的例子说明了很多问题和具体的操作,但是还有一些情况我们没有讨论,接下来,我们就在上面的修改基础上,继续修改一次,以便说明这种情况。这次我们要把[2,2]变为类型2
同样的,我们必定经过[2,4],但这次情况有所不同了,我们看看[2,4]的类型是什么,是0!!!!!说明[2,4]并不纯洁,这个区间内不止一种颜色,那我们就没办法确定它的孩子的信息。所以这个时候我们什么都不用做,直接往左边走来到[2,3]。来到[2,3]发现[2,3]的类型也是0,说明它也不纯洁,也没办法确定它的孩子的信息,直接来到它的左孩子[2,2]。来到目标区间[2,2]就直接修改类型为2,并计算区间和
所以我们已经可以总结实现了
1.什么时候能计算区间和?只有当这个区间是纯洁的,我们就可以用公式sum=(b-a+1)*col。而双亲不纯洁也就是类型为0时,只能亲(sum)=左孩子(sum)+右孩子(sum),这个公式也不是随便用的,必须满足条件就是左孩子是纯洁的,右孩子是纯洁的(但是它们的类型可以不同,其实也可以知道必定不同),如果有一方不纯洁,那么就递归处理这一方,直达返回它的sum。递归一定有尽头,因为至少元结点[i,i]的类型是确定的,一定是纯洁的,非0的。
2.每次我们要修改一些区间,都从树根出发并去到目标区间,路径时唯一的(这是树最基本的性质),所以我们经过哪些结点也是确定的。当我们沿着路径走(也就是经过这个目标区间的所有直属祖先),每到一个结点要看它是什么类型,如果是纯洁的,记得修改它的左右孩子的类型,为什么?因为它的左右孩子的类型不一定跟它的双亲结点的类型相同(而我们知道,它们应该是一样的),这个是为LAZY付出的代价,也就是终于检查到这个结点了,所以不得不修改它的左右孩子的信息。
3.有一个小剪枝:比如我们要修改[10,20]的类型为1,那么我们呢必定经过[1,30],而且[1,30]的类型为1,也就是说其子孙后代都是类型1,那么我们没必要往下走,因为这个修改是重复的。
这题由于最后输出的是整个区间的和比较特殊,不需要另外的询问,因为每次修改都必定经过树根,在updata函数中维护好树根的sum值即可,最后直接输出
#include <cstdio> #define N 100010 struct node { int a,b; int sum; int col; }tree[4*N]; void updata(int a ,int b ,int col ,int root) { if(tree[root].col==col) return ; //如果当前结点的类型和要修改的类型的类型相同那么没必要继续下去了 if(tree[root].a==a && tree[root].b==b) //找到目标区间 { tree[root].col=col; //修改目标区间的类型 tree[root].sum=(b-a+1)*col; //计算目标区间的区间和 return ; } int mid=(tree[root].a+tree[root].b)>>1; if(tree[root].col) //当前结点的类型非0,即一整段区间是纯洁的单色的 { tree[root<<1].col=tree[root<<1|1].col=tree[root].col; //那么可知其左右孩子的类型和双亲是相同的 tree[root<<1].sum=(tree[root<<1].b-tree[root<<1].a+1)*tree[root<<1].col; //计算左孩子的区间和 tree[root<<1|1].sum=(tree[root<<1|1].b-tree[root<<1|1].a+1)*tree[root<<1|1].col; //计算右孩子的区间和 tree[root].col=0; //此时双亲的类型为0,表示不是单色不纯洁 } if(a>mid) //去右孩子结点 updata(a,b,col,root<<1|1); else if(b<=mid) //去左孩子结点 updata(a,b,col,root<<1); else //横跨两边 { updata(a,mid,col,root<<1); updata(mid+1,b,col,root<<1|1); } tree[root].sum=tree[root<<1].sum+tree[root<<1|1].sum; } void build(int a ,int b ,int root) { tree[root].a=a; tree[root].b=b; tree[root].col=1; if(a==b) { tree[root].sum=1; return ; } int mid=(a+b)>>1; build(a,mid,root<<1); build(mid+1,b,root<<1|1); tree[root].sum=tree[root<<1].sum+tree[root<<1|1].sum; return ; } int main() { int n,m,x,y,col,Case,T; scanf("%d",&T); for(Case=1; Case<=T; Case++) { scanf("%d",&n); build(1,n,1); scanf("%d",&m); while(m--) { scanf("%d%d%d",&x,&y,&col); updata(x,y,col,1); } printf("Case %d: The total value of the hook is %d.\n",Case,tree[1].sum); } return 0; }
节省一个域sum,重新写一个代码,包含了query函数,代码量更好了,空间更少了,时间更快了,更通俗易懂了
#include <cstdio> #define N 100010 struct node { int a,b,col; }t[4*N]; int query(int a ,int b ,int rt) { if(t[rt].col) //当前区间单色 return (t[rt].b-t[rt].a+1)*t[rt].col; int mid=(t[rt].a+t[rt].b)>>1; return query(a,mid,rt<<1)+query(mid+1,b,rt<<1|1); } void updata(int a ,int b , int col ,int rt) { if(t[rt].col==col) return ; //剪枝 if(t[rt].a==a && t[rt].b==b) //目标区间 { t[rt].col=col; return ; } if(t[rt].col) //单色 { t[rt<<1].col=t[rt<<1|1].col=t[rt].col; t[rt].col=0; //非单色的 } int mid=(t[rt].a+t[rt].b)>>1; if(a>mid) //去右孩子结点 updata(a,b,col,rt<<1|1); else if(b<=mid) //去左孩子结点 updata(a,b,col,rt<<1); else //横跨两边 { updata(a,mid,col,rt<<1); updata(mid+1,b,col,rt<<1|1); } return ; } void build(int a ,int b ,int rt) { t[rt].a=a; t[rt].b=b; t[rt].col=1; if(a==b) return ; int mid=(a+b)>>1; build(a,mid,rt<<1); build(mid+1,b,rt<<1|1); return ; } int main() { int Case,T; scanf("%d",&T); for(Case=1; Case<=T; Case++) { int n,m,x,y,col; scanf("%d",&n); build(1,n,1); scanf("%d",&m); while(m--) { scanf("%d%d%d",&x,&y,&col); updata(x,y,col,1); } printf("Case %d: The total value of the hook is %d.\n",Case,query(1,n,1)); } return 0; }