先从的《统计与力量》感受了zkw线段树的优美(在此先ym 3分钟……),但是自己还是不能深入理解;后来又看了的线段树,虽然是用递归形式但从优美程度来讲一点儿也不差(zkw的非递归自己在拓展方面很难伸展,功力还不够……),我的线段树主要就是受这两位大牛的风格影响了~~~然后练习呀什么的基本是跟着HH神(NotOnlySuccess)的来的,然后在其他地方看到的很好的题也自己加了进来。
==========================================================================================================================================
对于各类线段树问题来说,
结点中主要有两种需要维护的数据,一个是标记,一个是统计。
主要有两种维护操作,一种是标记下放(懒惰标记,用于区间修改),一种是统计汇总(用于区间查询)。
可以看出这里我的建树方式采用的是zkw的满二叉树的形式,虽然浪费一些空间(?)但是在点区间对应查找和调试上都非常方便。
==========================================================================================================================================
♥单点更新:最最基础的线段树,只更新叶子节点,然后把信息用PointUpdate(int n, int V) || PointAdd(int n, int V)函数更新上来。
♠ (线段树入入入门题 || 第一个线段树程序)
HDU 1166 #include #include #include #include #include #include #include #include #include #include #include
♠ ★(好题,逆推思路)
问题抽象:求第K小点
思路:分析发现, 第i个人对他后面的人的位置都有可能有影响,但对他前面的人的位置一定没有影响。所以,我们可倒着插队。从后向前,先操作第n个人,当操作第i个人时,我们将他插在第pos[i]+1个空处。因为此时区间内的空处,是正着插队时前面奶牛所占的位置。对于求第K点,线段树时跑不过SBT。(嗯~这题可以SBT的......感觉线段树在这儿就是起了一个维护前缀和还有二分搜索的作用......)
POJ 2828 #include #include #include #include #include #include #include #include #include #include #include
♠ ★()
问题抽象:求特殊区间最大点、最小点。
思路:每次左边右边寻找距离当前位置最近的点,则用线段树求[0,now]区间最大数,求[now,l]区间最小的数维护即可。
HDU 4302 1 #include 2 #include 3 #include 4 #include 5 #include 6 #include 7 #include 8 #include 9 #include 10 #include 11 #include
♠ (离线RMQ,更新都不用,轻松1Y,简单题=。=)
POJ 3264 #include #include #include #include #include #include #include #include #include #include #include
♠ ★()
问题抽象:分组线段树求和。
思路:离线(离散化+排序)维护5颗线段树。sum[rt][5]的每棵树表示区间的数以该区间左端为起点mod 5的余数,cnt[rt]表示区间数的数目。一开始不知道怎么动态地维护插入、删除数据的位置的模5的余数,比如一开始插入1、3、5,5是要求的,但要是再插入个2变成1、2、3、5,那么就变成3了。。。这个让我想了好久,后来经过一些提示终于想到了思路:每个叶节点的值都附在sum[rt][0]里,即上面说的,sum[rt][i]表示以该区间左端点为起点mod 5的余数。那么在向上统计汇总时怎么转化呢?
答案是:sum[结点][i]=sum[左儿子][i]+sum[右儿子][((i+5)-cnt[左儿子]%5)%5]。
什么意思呢?从sum的意义出发,左儿子的区间左端点和父节点是一样的,所以他们的余数等价;然而需要把右儿子的左端点与父节点等价起来。设父区间左端点为a,则右儿子区间左端点即为a+cnt[左儿子]。若右儿子(pos-a)%5==i,则把它放到父区间(pos-a-cnt[])%5== i-cnt[]%5== (保证大于等于0小于5) ((i+5)-cnt[]%5)%5。
另外要注意这里离散化的方法~~~lower_bound()函数可以很方便的找到数在原数组中的位置。(或者自己写一个二分也可以。。。)
HDU 4288 1 #include 2 #include 3 #include 4 #include 5 #include 6 #include 7 #include 8 #include 9 #include
♠ ★()
问题抽象:询问区间内比一个数的权值小的数的个数
思路:将所有的询问离线读入之后,按H从小到大排序。对于所有的节点也按从小到大排序,然后根据查询的H,将比H小的点加入到线段树,然后就是一个区间和。
另一个思路:二分+区间第k大(划分树、函数式线段树……)
HDU 4417 1 #include 2 #include 3 #include 4 #include 5 #include 6 #include 7 #include 8 #include
♠ ★()
问题抽象:询问区间内比一个数的first权值大的数中second权值最大的数
思路:先一遍dfs将树形结构转为线性结构,转化后的每个点对应一个区间,区间内是它所有的子孙。然后问题就转化成了求区间内比一个数的权值大的数中权值最大的数。和上面那题很像了。将所有询问离线读入,按能力值从大到小排序。对于节点也按从大到小排序,然后根据查询点的能力值,将比它能力值大的点都插进线段树中,这样保证了线段树中存的都是满足条件的数(能力比上司高),再在对应区间内找个忠诚值最高的就好了(线段树中维护个max值什么的...)。
悲剧的是我在dfs爆栈了=.=。。。其实我觉得这个栈溢出的挺正常,毕竟50000个点压进栈什么的...=.=,可别人的为什么不会呀T_T,姿势也差不多呢呀,HDU 4358那道题为什么没爆呀=.=。。。算了,先放着吧,有时间了手动模拟栈再重新做一遍。。。
♥区间更新:(通常这对初学者来说是一道坎)需要用到延迟标记(懒惰标记),简单来说就是每次更新的时候不要更新到底,用延迟标记使得更新延迟到下次需要更新or询问到的时候。(这个阶段zkw线段树的精髓吾尚不能参透T_T......所以就结合用懒惰标记了=。=......)
注意 由于是对每个区间的值进行修改,如果我们每次修改都修改到叶子节点,那么修改复杂度就降到了O(n),必然导致TLE,所以要用懒惰标记。{ 注意这里默认的总区间是[1..M]不是HH大牛的[1..n],用错了区间和数组下标会对不上(因为zkw式建树和HH大牛的建树方式不同,zkw式在BuildTree()时建的就是[1,M]的满二叉树,这样建树的好处是下标与区间对应的很工整(详见《统计的力量》),查找会很方便。而HH牛的线段树开的是[1,n]的树)。。。而且要注意初始化时是从M赋值还是M+1,如果从M开始赋值(第0个叶节点开始表示区间1),则总区间为[1..M];如果从M+1开始赋值(用第1个叶节点开始表示区间1->前面还有第0个叶节点),则总区间为[0..M-1]。为了方便且易查错,我的程序就统一从M开始赋值,总区间定为[1,M] }
懒惰标记就是:如果当前的区间被要更新的区间所覆盖,直接就改变当前结点的信息,不用再往下了,所以,每次在更新的时候,只更新一层,把当前结点的信息往下传递一层,以供下一步使用,如果下一步完成了更新,更新也就结束了,没有完成,继续往下传递一层。。。。
♠ (区域修改->懒惰标记 入门题)
POJ 3468 1 POJ 3468 2 3 #include 4 #include 5 #include 6 #include 7 #include 8 #include 9 #include 10 #include 11 #include 12 #include 13 #include
♠ ★()
思路:如果没有防守间隔的限制,那么就是一道裸的线段树区间增减、单点查询问题。防守间隔的限制貌似阻碍了我们直接对区间修改,因为区间中不同点的防守状态不尽相同,就要个个针对,如果这样貌似的话就又退化成了N次单点更新---显然不可取。
一个重要的思路就是分而治之---把攻击和防守分开考虑。截至当前时间,一个点被成功攻击的次数 = 总攻击数 - 这段时间这个点防御的次数。(具体怎么求防守次数,看了代码就会很好理解的~~~)(因为模板的初始化sum add的范围(1, 2<<N)没弄对WA一次T_T……又返回去把前面的模板修正了下……)
HDU 4031 1 #include 2 #include 3 #define lson l,m,rt<<1 4 #define rson m+1,r,rt<<1|1 5 const int N = 20001; 6 int M; 7 int sum[N<<2]; 8 int add[N<<2]; 9 10 struct pp{ 11 int l,r; 12 }att[N]; 13 14 int defen[N]; 15 16 void PushUp(int rt) //统计汇总,rt为当前节点 17 { 18 sum[rt] = sum[rt<<1] + sum[rt<<1|1]; //如果是区间最值则 sum[n]=max(sum[n*2],sum[n*2+1]) 19 } 20 21 void PushDown(int rt,int l) //标记下放,rt为当前节点,l为修改区间长度 22 { 23 if (add[rt]) 24 { 25 add[rt<<1]+=add[rt]; 26 add[rt<<1|1]+=add[rt]; 27 sum[rt<<1]+=add[rt]*(l-(l>>1)); 28 sum[rt<<1|1]+=add[rt]*(l>>1); 29 add[rt]=0; 30 } 31 } 32 33 void BuildTree(int n) 34 { 35 for (M=1;M<=n+2;M<<=1); //求出M值(查询区间[1,M]) 36 for (int i=1;i 0;i--) 42 PushUp(i); 43 } 44 45 46 void Update(int s,int t,int v,int l,int r,int rt) 47 { 48 if (s<=l && r<=t) 49 { 50 add[rt]+=v; 51 sum[rt]+=v*(r-l+1); 52 return ; 53 } 54 PushDown(rt,r-l+1); 55 int m=(l+r)>>1; 56 if (s<=m) Update(s,t,v,l,m,rt<<1); 57 if (m >1; 68 if (s<=m) ans+=Query(s,t,l,m,rt<<1); 69 if (m =att[i].l&&a<=att[i].r){ 97 defen[a]++; 98 i+=t0-1; 99 }100 }101 printf("%d\n",Query(a,a,1,M,1)-defen[a]);102 103 104 }105 }106 }107 return 0;108 }
♠ ★()
思路:这道题的难点在于处理平方根时区间不好处理。一开始把思路集中在和的平方根和平方根的和的关系上,果然还是思维不够宽阔呐~这道题如果能想到一个I64整数也最多开平方8次就成1的话就简单了。在区间上加一个域bol[]判断该区间的数是不是都是1了,如果是则不用修改了。这样下来没个点最多被修改8次,不会超时。
HDU 4027 1 #include 2 #include 3 #include 4 #include 5 #include 6 #include 7 #include 8 #include 9 #include 10 #include 11 #include 12 #include 13 #define MID(x,y) ( ( x + y ) >> 1 ) 14 #define FOR(i,s,t) for(int i=s; i 0;i--) 51 PushUp(i); 52 } 53 54 55 void Update(int s,int t,int l,int r,int rt) 56 { 57 if (s<=l && r<=t) 58 { 59 if (bol[rt]==1) 60 return ; 61 else if (l==r) 62 { 63 sum[rt]=(__int64)sqrt(double(sum[rt])); 64 if (sum[rt]==1) 65 bol[rt]=1; 66 return ; 67 } 68 } 69 70 int m=MID(l,r); 71 if (s<=m) Update(s,t,l,m,rt<<1); 72 if (m b) a^=b, b^=a, a^=b;110 Update(a,b,1,M,1);111 }112 else113 {114 int a,b;115 116 scanf("%d%d",&a,&b);117 if (a>b) a^=b, b^=a, a^=b;118 printf("%I64d\n",Query(a,b,1,M,1));119 }120 }121 122 printf("\n");123 }124 125 126 return 0;127 }
♠ (坐标离散化->数组hash)
问题抽象:线段插入问题。(通常需要把坐标离散化成“线段”)
思路 这题的坐标范围有点儿大(1-10000000),普通线段树肯定会超时+超内存,所以就需要离散化: 离散化简单的来说就是只取我们需要的值来用,比如说区间[1000,2000],[1990,2012] 我们用不到[-∞,999][1001,1989][1991,1999][2001,2011][2013,+∞]这些值,所以我只需要1000,1990,2000,2012就够了,将其分别映射到0,1,2,3,在于复杂度就大大的降下来了 所以离散化要保存所有需要用到的值,排序后,分别映射到1~n,这样复杂度就会小很多很多 而这题的难点在于每个数字其实表示的是一个单位长度(并非一个点),这样普通的离散化会造成许多错误(包括我以前的代码,poj这题数据奇弱) 给出下面两个简单的例子应该能体现普通离散化的缺陷: 例子一:1-10 1-4 5-10 例子二:1-10 1-4 6-10 普通离散化后都变成了[1,4][1,2][3,4] 线段2覆盖了[1,2],线段3覆盖了[3,4],那么线段1是否被完全覆盖掉了呢? 例子一是完全被覆盖掉了,而例子二没有被覆盖 为了解决这种缺陷,我们可以在排序后的数组上加些处理,比如说[1,2,6,10] 如果相邻数字间距大于1的话,在其中加上任意一个数字,比如加成[1,2,3,6,7,10],然后再做线段树就好了. PS:这题我写的程序和一个AC程序对拍了N次没错,但交到POJ就是WA。。。哎,算了,留待以后写吧。。。
♠ ()(坐标离散化->map hash)
HDU 4325 1 #include 2 #include 3 #include 4 #include 5 #include 6 #include 7 #include 8 #include
♠ ★()(分组线段树)
做网络赛的时候没想到(经验太少了T T……),比完赛请教了一个大牛才学会这种处理方法。
思路: 比较容易往线段树上想的。但是由于更新的是一些离散的点,比较麻烦。可以考虑这些点的共性,总是隔几个,更新一个,而且注意这个条件“(i-a)%k==0”可以转化为“i%k == a%k ==mod ”,那么我们就可以把区间内的数关于k的余数分组。这样每次更新的都是其中的一组,而且是连续的。由于 K比较小,这是本题的突破口,那么关于k的余数情况,最多只有55种。即如果k=1,则分为1组,k=2分为2组……(如果直接开10*10的数组会MLE = =……今年卡内存卡的那叫个……)最后查询也一样,枚举点所在的所有分组上的和都加起来就行了。
HDU 4267 1 #include 2 #include 3 #include 4 #include 5 #include 6 #include 7 #include 8 #include 9 #include 10 #include 11 #include 12 #include 13 #define MID(x,y) ( ( x + y ) >> 1 ) 14 15 using namespace std; 16 17 #define N 50002 18 19 int sum[N<<2],add[N<<2][55]; 20 int ha[11][11]; 21 int op[N]; 22 23 void BuildTree(int rt,int l,int r) 24 { 25 sum[rt]=0; 26 memset(add[rt],0,sizeof(add[rt])); 27 if (l==r) return ; 28 29 int mid=MID(l,r); 30 BuildTree(rt<<1,l,mid); 31 BuildTree(rt<<1|1,mid+1,r); 32 } 33 34 void PushDown(int rt) 35 { 36 if (sum[rt]) 37 { 38 sum[rt<<1]+=sum[rt]; 39 sum[rt<<1|1]+=sum[rt]; 40 sum[rt]=0; 41 for (int i=0;i<55;i++) 42 { 43 add[rt<<1][i]+=add[rt][i]; 44 add[rt<<1|1][i]+=add[rt][i]; 45 add[rt][i]=0; 46 } 47 } 48 } 49 50 void Update(int s,int t,int v,int l,int r,int rt,int i,int j) 51 { 52 if (s<=l && r<=t) 53 { 54 sum[rt]+=v; 55 add[rt][ha[i][j]]+=v; 56 return ; 57 } 58 59 PushDown(rt); 60 int mid=MID(l,r); 61 if (s<=mid) Update(s,t,v,l,mid,rt<<1,i,j); 62 if (mid
♠ ★()(分组线段树)
HH神出的一道线段树神题~
思路:题意很简单,成段更新,成段询问,但是更新却和一般的线段树大不一样,每个点虽然接收到相同的信息,但是由于本身不同,最终得到的值也是不同的.用一般的延迟操作就搞不定了.
突破点在K,范围很小,只有10,可以考虑每次有人升级的时候,就递归的找下去,将这个人进行升级操作.由于找到某个人只需要logn的复杂度,每个人最多升k次,所以n个人的复杂度是O(nklogn)。用了两个辅助数组add[maxn]和MAX[maxk][maxn],add用于记录延迟标记,MAX[k]表示该区间等级为k的最大经验值.初始化add,MAX[1]为0,其他为-1,表示无人在这个等级.当MAX[k]的值大于等于Needk时,就对这个区间进行升级操作,和线段树操作一样递归的将这个区间能升级的人全部升级.
单次操作可能会是nlogn(每个人都升级),但是平均下来还是只有nklogn.
HDU 3954 1 #include 2 #include 3 #include 4 #include 5 #include 6 #include 7 #include 8 #include 9 #include 10 #include
♠ ★★()
问题抽象:区间内恰好出现K次的数的个数。
思路: (内容有点儿多,单独放一处了)
♠ ★(区间异或&&分组线段树)
题目大意:序列a有n个数。实现两个操作:①求[l,r]区间和 ②对某个区间[l,r]所有数异或一个数x。
思路:比较容易想到是用线段树,因为每个数都小于10^6,所以把每一位拆成20位储存,这样就用20颗线段树。那么问题就转化为区间01异或问题:0异或任何数不变,不用修改,1异或一个数x结果是1-x。。(详细理解还是看代码吧。。
View Code #include #include #include #include #include #include #include
♥区间合并:这类题目会询问区间中满足条件的连续最长区间,所以PushUp的时候需要对左右儿子的区间进行合并,所以一般情况下除了设置本区间最长连续mmax外,还需要设置从区间左端开始最长连续lmax和从区间右端开始最长连续rmax,以便于和左右两边区间合并。
♠ (区间合并入门)
题目大意:区间内最长连续房间数。
问题出来了,对于二叉树,或许某子树根的左孩子的右边跟右孩子的左边连续着呢,怎么办?
于是,我们开出三个数组 lsum[] rsum[] 和 sum[]。对于区间 [L, R],lsum[rt]表示以 L为开头的最长连续房间数,rsum[rt]表示以R为结尾的最长连续房间数,sum[]表示[L,R]内的最长连续房间。
继续分析:当 lsum[rt<<1]等于左孩子区间总长度时,lsum[rt<<1]和lsum[rt<<1|1] (即左孩子的lsum和右孩子的lsum)是相连的;
同理得, 当 rsum[rt<<1|1]等于右孩子总长度时,rsum[rt<<1|1]和rsum[rt<<1](即右孩子的rsum和左孩子的rsum)是相连的。
而对于一个 sum[rt]= max{ rsum[rt<<1]+lsum[rt<<1|1], sum[rt<<1], sum[rt<<1|1 }。
POJ 3667 1 #include 2 #include 3 #include 4 #include 5 #include 6 #include 7 #include 8 #include
♠ ()
问题抽象:最长连续公共子串
一开始想了个二分的线段树方法,不过nlog2n超时了。。。
62 | Abandon.burning | 2000MS | 10444K | 2333B | C++ | 2012-10-01 08:42:19 |
HDU 4339 1 #include 2 #include 3 #include 4 #include 5 #include 6 #include 7 #include 8 #include
♠ ★★() (线段树值得研究的一道好题)
区间合并->二次合并(pushup)->更新时要合并左右子区间,询问时要合并左右询问子区间(这是不一样的,比如在[1,8]中询问[1,6],那么左右子区间分别是[1,4],[5,8],而左右询问子区间是[1,4],[5,6])。
预备知识:一个数的数字根就是这个数mod 9的余数 (余数为0则取9,当然如果这个数本身是0那就是0了……)
思路:因为题目中所查询的区间,是所有的子区间的结果的并。所以区间合并就很必要了。
每个结点,记录这个区间的和的数根, 记录本区间内从左端点为端点往右的连续区间出现的数根,从右端点为端点往左的连续区间出现的数根,以及整个区间能出现的数根。然后合并什么的。。。
但显然还没有结束,不然我也不会把他视作好题了。。。这道题用线段树会卡时限!需要各种优化------主要是算数字根的打表优化。
做了一天吧T_T。。。上午想好了思路,然后一下午+晚上完成了一个加上打表、输入输出优化还是TLE的代码。。。T_T。。。然后就捉鸡了。。。
HDU 4351(TLE sb代码) 1 #include 2 #include 3 #include 4 #include 5 #include 6 #include 7 #include 8 #include
噗。。。然后第二天上午终于找到哪儿超时了(我说怎么加了打表优化和输入输出优化还超T_T),竟然是开始回答询问时的那四个memset T_T,然后恍然大悟。。。我竟然sb的给ans也建了颗线段树!活该作死啊。。。T_T。。。
然后把ans改为节点,加上打表优化、输入输出外挂后终于A了T_T (速度还不是很快啊。。。网上找了个程序1100MS呜呜。。。):
6851969 | 2012-10-02 15:18:54 | Accepted | 4351 | 1546MS | 32140K | 6320 B | G++ | Abandon.burning |
HDU 4351 1 #include 2 #include 3 #include 4 #include 5 #include 6 #include 7 #include 8 #include
♥扫描线:这类题目需要将一些操作排序,然后从左到右用一根扫描线(当然是在我们脑子里)扫过去。最典型的就是矩形面积并,周长并等题
♠ (矩形面积并入门题)
问题抽象:矩形面积并
其他入门题: (就是这道题,不过POJ的数据比较弱...)、、(所有矩形底边都在一条水平线上)
思路:浮点数先要离散化; 对于一个矩形(x1, y1, x2, y2),只取上边和下边,结构体ss[] 记录一条边的左右点坐标和距坐标轴的高度,再拿一个变量 ss[].s记录这条边是上边还是下边,下边记录为1,上边记录为-1。按高度排序,对横轴建树,从低到高扫描一遍。若下边,则加入线段树;若上边,则从线段树上去掉。用cnt表示该区间下边比上边多几条线段,sum代表该区间内被覆盖的长度总和。
这里线段树的一个结点并非是线段的一个端点,而是该端点和下一个端点间的线段,所以题目中r+1,r-1的地方要好好的琢磨一下。
可以去看看nocLyt的图解:
HDU 1542 1 #include 2 #include 3 #include 4 #include 5 #include 6 #include 7 #include 8 #include
♠ ()
问题抽象:矩形面积并
贴矩形海报,每次贴前都要在海报中剪个小的矩形洞,然后求面积并。思路很简单,把单个海报剪掉一个洞后看成四个小海报就行了,分成四个小矩形时要注意下边界处理问题,然后直接套HDU 1542的模板1Y了 =.=。。。
HDU 3265 1 #include 2 #include 3 #include 4 #include 5 #include 6 #include 7 #include 8 #include
♥二维线段树:此类线段树没什么意思,就是代码量加倍了而已,运用范围也没一维线段树广,会写就行了
二维线段树需要有两个维度,所以实现它的最基本思想就是树中套树。假设有一个矩形横坐标范围1—n,纵坐标范围1—m。我们可以以横坐标为一个维度,建立一棵线段树,假设为tree1,在这棵树的每个节点中以纵坐标建立一棵线段树,设为tree2,假设我们在tree1所处在的节点的的横坐标范围为l,r,那么该节点表示的矩形范围为横坐标为l—r,纵坐标范围为1—m。若我们正处在该节点中tree2的某个节点,该节点的纵坐标范围为d—u,那么tree2中的这个节点所代表的矩形范围,横坐标l—r,纵坐标d—u。所以我们看到,树套树&&二维线段树只要理解其思想就会发现其实并不难。
♠ (二维线段树入门)
因为有两个限制范围,所以需要二维的线段树。也没什么好说的地方。
需要注意几个问题:1.初始化sum = -1,不然所有的返回结果都是>=0的数,查找不到的情况不好判断。
2.坐标左小右大(因为这个白白贡献几次WA擦。。。)
3.有可能存在同一身高、活泼度的人,所以更新时不是直接赋值而是选个缘分值最大的。
HDU 1823 1 #include 2 #include 3 #include 4 #include 5 #include 6 #include 7 #include 8 #include
(未完待续...)