线段树单点更新

 最近开始接触线段树的内容,我发现线段树非常灵活和强大,很多问题都可以用线段树来做。但是,对于初次接触的人来说,可能比较难。根据大牛的博客,我开始一步步练习。现在把已经做好的单点更新的习题贴出来,讲讲自己的想法。欢迎拍砖~


补充几点说明:

(1)我采用静态方法构建线段树,即用数组来做,左子树为 2*i,右子树为 2*i+1。按照保守估计,数组空间一般开N*4,用到的空间实质上是N*2-1,但是考虑到中间会有一些空间没有使用,特别是最底层的。粗略地想,假设最底层只有最后两个空间有用到,那么这一层前面的空余空间就约为N*2了。所以N*4是足够的。

(2)这部分一般来说涉及到下面几个函数:pushUp(由下往上更新修改后的数据),build(初始化线段树),update(修改叶子结点的数据),query(查询区间数据)

(3)线段树本身并不保存当前区间具体是什么,而是由函数传递两个变量l, r 来记录当前的具体区间,这样做可节省大量空间开销,详细参见代码。

(4)前文已出现过的,后面不再重复,包括函数参数的说明。


题目:

hdu 1166 敌兵布阵

大意:敌人有N个营地,一开始知道每个营地的人数。

            接下来会询问四种命令:(1) Add i j,i 和 j 为正整数,表示第i个营地增加j个人(j不超过30);
                                                        (2) Sub i j ,i 和 j 为正整数,表示第i个营地减少j个人(j不超过30);
                                                        (3) Query i j , i 和 j 为正整数,i <= j,表示询问第i到第j个营地的总人数;
                                                        (4) End 表示结束,这条命令在每组数据最后出现。

            遇到Query则作出回答,遇到End则结束本次数据的询问。其他则修改相关营地的数据。(显然,Sub 可以归并到 Add 操作)


思路

对于每一组数据,用build初始化线段树,每个结点保存区间[i, j]内的总人数。叶子结点即是每个营地的人数,这个由输入给出,对于内结点,则采取pushUp由下往上传递值。

对于Add 操作(包括Sub操作),可以用update来修改叶子结点的值,与build相似,内结点采取pushUp更新其值。

对于Query 操作,用query来查看区间的人数。对于没有在一个存储区间表示出来的空间,则采取同时向两边走,覆盖后返回其值的方式取值,详细见代码。


函数接口

pushUp(int cur)      // cur 表示当前数组的实际下标。

build(int l, int r, int cur)     // [l, r] 表示当前数组存储的值的区间,即前面所说的由函数传入值表示。

update(int i, int val, int l, int r, int cur)     // i 表示需要更新值的数组实际下标,即线段树的相应的叶子结点,val 表示需要增加的值(对于Sub操作,主函数传入-val)。

query(int curL, int curR, int l, int r, int cur)    // [curL, curR] 表示需要查询的区间


代码如下:

[cpp]  view plain copy
  1. #include<cstdio>  
  2. using namespace std;  
  3. const int MAX = 50000 + 10;         //数据范围  
  4. int sum[MAX << 2];          //保存各个范围的sum值  
  5. void pushUp(int cur){  
  6.     //检查下层数据,更新当前的sum值  
  7.     sum[cur] = sum[cur << 1] + sum[cur << 1 | 1];  
  8. }  
  9. void build(int l, int r, int cur){  
  10.     //初始化线段树  
  11.     if(l == r){  
  12.         scanf("%d",&sum[cur]);  
  13.         return;  
  14.     }  
  15.     int mid = (l + r) >> 1;  
  16.     build(l, mid, cur << 1);  
  17.     build(mid + 1, r, cur << 1 | 1);  
  18.     //更新当前的sum  
  19.     pushUp(cur);  
  20. }  
  21. void update(int i, int val, int l, int r, int cur){  
  22.     //更新范围i的值,并向上传递sum  
  23.     if(l == r){  
  24.         sum[cur] += val;  
  25.         return;  
  26.     }  
  27.     int mid = (l + r) >> 1;  
  28.     if(i <= mid){  
  29.         update(i, val, l, mid, cur << 1);  
  30.     }  
  31.     else{  
  32.         update(i, val, mid+1, r, cur << 1 | 1);  
  33.     }  
  34.     //传递sum值  
  35.     pushUp(cur);  
  36. }  
  37. int query(int curL, int curR, int l, int r, int cur){  
  38.     //查询curL 至 curR 的 sum  
  39.     if(curL <= l && curR >= r){  
  40.         return sum[cur];  
  41.     }  
  42.     int mid = (l + r) >> 1;  
  43.     int result = 0;  
  44.     if(curL <= mid){  
  45.         result += query(curL, curR, l, mid, cur << 1);  
  46.     }  
  47.     if(curR > mid){  
  48.         result += query(curL, curR, mid+1, r, cur << 1 | 1);  
  49.     }  
  50.     return result;  
  51. }  
  52. int main(){  
  53.     int k = 1;  //当前例子编号  
  54.     int t;      //样例数  
  55.     int n;      //营地数  
  56.     scanf("%d",&t);  
  57.     while(t--){  
  58.         scanf("%d",&n);  
  59.         build(1,n,1);  
  60.         printf("Case %d:\n",k++);  
  61.         char s[10];  
  62.         int i,j;  
  63.         while(scanf("%s",s) != EOF){  
  64.             if(s[0] == 'E')break;  
  65.             scanf("%d %d",&i, &j);  
  66.             switch(s[0]){  
  67.                 case 'A': update(i, j, 1, n, 1); break;  
  68.                 case 'S': update(i, -j, 1, n, 1); break;  
  69.                 case 'Q': printf("%d\n",query(i, j, 1, n, 1)); break;  
  70.             }  
  71.         }  
  72.     }  
  73.     return 0;  
  74. }  

hdu 1754 I Hate It

大意:给出N个学生的成绩,下面有M个操作。

            接下来有M行。每一行有一个字符 C (只取'Q'或'U') ,和两个正整数A,B。

            当C为'Q'的时候,表示这是一条询问操作,它询问ID从A到B(包括A,B)的学生当中,成绩最高的是多少。

            当C为'U'的时候,表示这是一条更新操作,要求把ID为A的学生的成绩更改为B。


思路:和上题类似,不同的是这次线段树保存的不是总和,而是最大值。

            build 与 update 和上题的一样,只是 pushUp 和 query 不再是累加两个子结点,而是比较两个子结点(如果有的话),然好返回其较大值。

函数接口

基本上和上题相同,只是update不再是累加val,而是直接修改数据为val。


代码如下:

[cpp]  view plain copy
  1. #include<cstdio>  
  2. #include<cstring>  
  3. using namespace std;  
  4. const int MAX = 200000 + 10;  
  5. int highest[MAX << 2];          //构建最高成绩线段树  
  6. void pushUp(int cur){  
  7.     //更新 highest 的值  
  8.     if(highest[cur << 1] > highest[cur << 1 | 1]){  
  9.         highest[cur] = highest[cur << 1];  
  10.     }  
  11.     else{  
  12.         highest[cur] = highest[cur << 1 | 1];  
  13.     }  
  14. }  
  15. void build(int l, int r, int cur){  
  16.     //初始化线段树  
  17.     if(l == r){  
  18.         scanf("%d",&highest[cur]);  
  19.         return;  
  20.     }  
  21.     int mid = (l + r) >> 1;  
  22.     build(l, mid, cur << 1);  
  23.     build(mid+1, r, cur << 1 | 1);  
  24.     //由下往上传递 highest 值  
  25.     pushUp(cur);  
  26. }  
  27. void update(int i, int val, int l, int r, int cur){  
  28.     //更新 i 的值 为 val  
  29.     if(l == r){  
  30.         highest[cur] = val;  
  31.         return;  
  32.     }  
  33.     int mid = (l + r) >> 1;  
  34.     if(i <= mid){  
  35.         update(i, val, l, mid, cur << 1);  
  36.     }  
  37.     else{  
  38.         update(i, val, mid+1, r, cur << 1 | 1);  
  39.     }  
  40.     pushUp(cur);  
  41. }  
  42. int query(int a, int b, int l, int r, int cur){  
  43.     //返回[a, b]的最高成绩  
  44.     if(a <= l && b >= r){  
  45.         return highest[cur];  
  46.     }  
  47.     int mid = (l + r) >> 1;  
  48.     int result = 0,temp;  
  49.     if(a <= mid){  
  50.         temp = query(a, b, l, mid, cur << 1);  
  51.         if(result < temp){  
  52.             result = temp;  
  53.         }  
  54.     }  
  55.     if(b > mid){  
  56.         temp = query(a, b, mid+1, r, cur << 1 | 1);  
  57.         if(result < temp){  
  58.             result = temp;  
  59.         }  
  60.     }  
  61.     return result;  
  62. }  
  63. int main(){  
  64.     int n;      //人数  
  65.     int m;      //操作次数  
  66.     while(scanf("%d %d",&n,&m) != EOF){  
  67.         build(1,n,1);  
  68.         char s;  
  69.         int a,b;  
  70.         for(int i = 0; i < m; i++){  
  71.             getchar();  
  72.             scanf("%c %d %d",&s,&a,&b);  
  73.             switch(s){  
  74.                 case 'U': update(a, b, 1, n, 1); break;  
  75.                 case 'Q': printf("%d\n",query(a, b, 1, n, 1)); break;  
  76.             }  
  77.         }  
  78.     }  
  79.     return 0;  
  80. }  

hdu 1394 Minimun Inversion Number

大意:给定一个N和关于0-N-1的序列,通过不断把首元素放到最后得出变换后的序列和其逆序数,求出最小逆序数(包括一开始那个序列)。


思路

(1)用线段树求初始序列的逆序数。用build构建线段树保存区间[l, r]中出现过的元素个数(pushUp 求子树之和),对于每一个query查询[0, val] 已经出现过的元素个数(累加这个值即逆序数),然后用update 表示 val 已出现过一次。

(2)得到最初的逆序数后,对于后面出现的每一个序列,其逆序数为前一个序列的逆序数count 减去 首元素的值val,再加上 N - val - 1,即 count = count - val + N - val - 1(注意到,序列中的每个元素均小于N,那么首元素val值则表示其后有val 个元素小于它,所以将val 移到最后即逆序数要减去这个值;而移动后,val 前面必有 N - val - 1 个元素大于它,则逆序数需要加上这个值。)


函数接口

和之前的类似,只是update没有了val,因为这次只需要将 i 结点表示出现过1次。


代码如下:

[cpp]  view plain copy
  1. #include<cstdio>  
  2. using namespace std;  
  3. const int MAX = 5000 + 10;  
  4. int sum[MAX << 2];              //保存每个范围的数字出现次数  
  5. void build(int l, int r, int cur){  
  6.     //创建空线段树  
  7.     sum[cur] = 0;  
  8.     if(l == r){  
  9.         return;  
  10.     }  
  11.     int mid = (l + r) >> 1;  
  12.     build(l, mid, cur << 1);  
  13.     build(mid+1, r, cur << 1 | 1);  
  14. }  
  15. void pushUp(int cur){  
  16.     //更新范围出现次数  
  17.     sum[cur] = sum[cur << 1] + sum[cur << 1 | 1];  
  18. }  
  19. void update(int i, int l, int r, int cur){  
  20.     //更新结点i出现一次  
  21.     if(l == r){  
  22.         sum[cur] ++;  
  23.         return;  
  24.     }  
  25.     int mid = (l + r) >> 1;  
  26.     if(i <= mid){  
  27.         update(i, l, mid, cur << 1);  
  28.     }  
  29.     else{  
  30.         update(i, mid+1, r, cur << 1 | 1);  
  31.     }  
  32.     pushUp(cur);  
  33. }  
  34. int query(int curL, int curR, int l, int r, int cur){  
  35.     //查询[curL, curR]出现的元素个数  
  36.     if(curL <= l && curR >= r){  
  37.         return sum[cur];  
  38.     }  
  39.     int mid = (l + r) >> 1;  
  40.     int result = 0;  
  41.     if(curL <= mid){  
  42.         result += query(curL, curR, l, mid, cur << 1);  
  43.     }  
  44.     if(curR > mid){  
  45.         result += query(curL, curR, mid+1, r, cur << 1 | 1);  
  46.     }  
  47.     return result;  
  48. }  
  49. int main(){  
  50.     int n;          //元素个数  
  51.     int A[MAX];     //保存元素  
  52.     int count;      //保存当前结果  
  53.     int result;     //保存结果  
  54.     while(~scanf("%d",&n)){  
  55.         count = 0;  
  56.         build(0, n-1, 1);  
  57.         for(int i = 0; i < n; i++){  
  58.             scanf("%d",&A[i]);  
  59.             count += query(A[i], n-1, 0, n-1, 1);  
  60.             update(A[i], 0, n-1, 1);  
  61.         }  
  62.         result = count;  
  63.         for(int i = 0; i < n; i++){  
  64.             count += n - A[i] - A[i] - 1;  
  65.             if(result > count){  
  66.                 result = count;  
  67.             }  
  68.         }  
  69.         printf("%d\n",result);  
  70.     }  
  71.     return 0;  
  72. }  

hdu 2795 Billboard

大意:已知一块黑板高为h(有h行),宽为w。下面有n张通知需要张贴。张贴规则为尽量高,尽量靠左边,即当前最高行,和那行的最左边,可以理解为从左上角开始张贴。给出每张通知的宽度(默认一行至少可以容纳一张通知),求它张贴的行号(由上至下1到h),如果贴不下,则返回-1.


思路

(1)用黑板的高度作为区间下标构建线段树,保存当前区间所能容纳的最大宽度。build的时候每个区间保存的都是w. 为了节省时间,这里的update操作归并到query里,因为每次查询都需要查询到特定的叶结点并返回其区间下标,这时就可以修改其值并在过程中传递上去。pushUp更新当前区间能容纳的最大宽度。

(2)每次传入当前通知宽度前,都需要查看根结点的数值是否大于当前宽度,因为若根结点都存不下,那这个通知就不能张贴了。


函数接口

和之前的类似,只是query 传递 curW,查看当前区间是否能存放下curW宽度的通知,若能则找到其叶子结点返回其值。


代码如下:

[cpp]  view plain copy
  1. #include<cstdio>  
  2. using namespace std;  
  3. const int MAX = 2000000 + 10;  
  4. int maxWidth[MAX << 2];         //保存范围内的最大宽度,用高度构建线段树  
  5. int h, w, n;            //输入  
  6. void pushUp(int cur){  
  7.     //更新宽度  
  8.     if(maxWidth[cur << 1] > maxWidth[cur << 1 | 1]){  
  9.         maxWidth[cur] = maxWidth[cur << 1];  
  10.     }  
  11.     else{  
  12.         maxWidth[cur] = maxWidth[cur << 1 | 1];  
  13.     }  
  14. }  
  15. void build(int l, int r, int cur){  
  16.     //初始化线段树,保存范围内可容纳最大宽度  
  17.     maxWidth[cur] = w;  
  18.     if(l == r)return;  
  19.     int mid = (l + r) >> 1;  
  20.     build(l, mid, cur << 1);  
  21.     build(mid+1, r, cur << 1 | 1);  
  22. }  
  23. int query(int curW,int l, int r, int cur){  
  24.     //查询[l, r]是否能容纳 curW 宽度, 若能返回行号  
  25.     if(l == r){  
  26.         maxWidth[cur] -= curW;  
  27.         return l;  
  28.     }  
  29.     int mid = (l + r) >> 1;  
  30.     int result = (maxWidth[cur << 1] >= curW) ? query(curW, l, mid, cur << 1) : query(curW, mid+1, r, cur << 1 | 1);  
  31.     pushUp(cur);  
  32.     return result;  
  33. }  
  34. int main(){  
  35.     while(~scanf("%d %d %d", &h, &w, &n)){  
  36.         if(h > n){  
  37.             //因为张贴的宽度不可能大于w, 即不可能超过n行  
  38.             h = n;  
  39.         }  
  40.         build(1, h, 1);  
  41.         int temp;  
  42.         for(int i = 0; i < n; i++){  
  43.             scanf("%d",&temp);  
  44.             if(maxWidth[1] < temp){  
  45.                 printf("-1\n");  
  46.             }  
  47.             else{  
  48.                 printf("%d\n",query(temp, 1, h, 1));  
  49.             }  
  50.         }  
  51.     }  
  52.     return 0;  
  53. }  

pku 2828 Buy Tickets

大意:插队问题。已知有N个人,一开始队列为空,编号从1到N,可以看作所有人都排在0号的后面(0号不存在)。分别读入N个人的pos 和 val,pos 表示插队在 pos号人的后面(如pos = 0, 即插入到第1位),val 可以看作是读入的人的一个标识。求经过N次插队以后(插队一定成功),最终的队列(输出这个队列每个人的标识值)。


思路

(1)如果按输入顺序模拟插队,每次都要做很多的移动,效率低下。如果从最后一个人往前看,他的pos 表示的就是前面有 pos 个空位。根据这个他们每次插队后的位置就是最终的位置,因为没有后面的人会让他们后移(因为是从后往前看嘛~)。可以用build 构建线段树保存区间的空位数,区间下标为队列的编号。pushUp 由下往上传递求和信息。query 查询前i 个空位,并返回第i + 1 个空位的区间下标(这个可以从函数代码里那个if 语句看出,左区间是包括mid 的,但比较的时候没有包括进去,其实每次返回的就是上一次的 mid)。和上题相似,query 找到下标后直接修改区间的值,避免重复计算,然好用pushUp 传递其值。

(2)对于每次query 返回的值,我采用多开一个数组来保存每次的结果,最后再顺序输出即可。


函数接口

基本和前面出现的类似,需要注意的是query 传递的 i 的含义。每次返回的是第i + 1 个空位的下标。


代码如下:

[cpp]  view plain copy
  1. #include<cstdio>  
  2. using namespace std;  
  3. const int MAX = 200000 + 10;  
  4. struct person{  
  5.     int pos;  
  6.     int val;  
  7. };  
  8. int sum[MAX << 2];      //保存线段树,空位数  
  9. void pushUp(int cur){  
  10.     //更新空位数  
  11.     sum[cur] = sum[cur << 1] + sum[cur << 1 | 1];  
  12. }  
  13. void build(int l, int r, int cur){  
  14.     //构建线段树  
  15.     if(l == r){  
  16.         sum[cur] = 1;  
  17.         return;  
  18.     }  
  19.     int mid = (l + r) >> 1;  
  20.     build(l, mid, cur << 1);  
  21.     build(mid+1, r, cur << 1 | 1);  
  22.     pushUp(cur);  
  23. }  
  24. int query(int i, int l, int r, int cur){  
  25.     //找到前i个空位,并返回第i+1个空位的下标  
  26.     if(l == r){  
  27.         sum[cur] = 0;  
  28.         return l;  
  29.     }  
  30.     int mid = (l + r) >> 1;  
  31.     int result;  
  32.     if(i < sum[cur << 1]){  
  33.         result = query(i, l, mid, cur << 1);  
  34.     }  
  35.     else{  
  36.         result = query(i-sum[cur << 1], mid+1, r, cur << 1 | 1);  
  37.     }  
  38.     pushUp(cur);  
  39.     return result;  
  40. }  
  41. int main(){  
  42.     int n;              //人数  
  43.     person A[MAX];      //保存每个人的值  
  44.     int R[MAX];         //结果序列  
  45.     while(~scanf("%d",&n)){  
  46.         for(int i = 0; i < n; i++){  
  47.             scanf("%d%d",&A[i].pos,&A[i].val);  
  48.         }  
  49.         build(1, n, 1);  
  50.         for(int i = n-1; i >= 0; i--){  
  51.             int index = query(A[i].pos, 1, n, 1);  
  52.             R[index] = A[i].val;  
  53.         }  
  54.         printf("%d",R[1]);  
  55.         for(int i = 2; i <= n; i++){  
  56.             printf(" %d",R[i]);  
  57.         }  
  58.         printf("\n");  
  59.     }  
  60.     return 0;  
  61. }  

pku 2886 Who Gets the Most Candies?

大意:N个小朋友围成一圈玩游戏,每个人手上拿着一张有数字的卡片。输入序列给出每个小朋友的编号,一开始由第k个小朋友开始玩。被点到的小朋友跳出圈,手上卡片的数字val 若是正数,则顺时针数val 后点到小朋友;若为负值,则逆时针数-val 后点到小朋友。一直这样下去直到所有小朋友跳出圈。另外,每个跳出圈的小朋友都可以得到一定的糖果,糖果数就是 m(他是第 m 个跳出圈的) 的划分,也就是m 的因子数。问可以得到最多糖果的小朋友的名字和他得到的糖果数(输入给出小朋友的名字和他手上的卡片数字)。如果有多个获得最多糖果的孩子,则输出最早跳出圈那个。


思路

(1)如果每次游戏都玩到最后,那么需要保存所有人的糖果数,最后再比较输出。网上有人提到可以利用反素数的概念(百度百科:对于任何正整数x,其约数的个数记做g(x).例如g(1)=1,g(6)=4.如果某个正整数x满足:对于任意i(0<i<x),都有g(i)<g(x),则称x为反素数.),那么对于每个给定的N,我们进行到1-N 的最小反素数时就可以退出,这个孩子得到的就是最多糖果。所以我首先通过下面代码打表获得500000以内的所有最小反素数和获得的最大糖果,只有35个~

[cpp]  view plain copy
  1. #include<cstdio>  
  2. #include<fstream>  
  3. using namespace std;  
  4. int A[500010];  
  5. int R[500010];  
  6. int main(){  
  7.     for(int i = 1; i <= 500000; i++){  
  8.         for(int j = 1; i * j <= 500000; j++){  
  9.             A[i * j]++;  
  10.         }  
  11.     }  
  12.     int n = 0;  
  13.     for(int i = 1; i <= 500000; i++){  
  14.         int max = 1;  
  15.         for(int j = 1; j <= i; j++){  
  16.             if(A[max] < A[j]){  
  17.                 max = j;  
  18.             }  
  19.         }  
  20.         int k;  
  21.         for(k = 0; k < n; k++){  
  22.             if(R[k] == max){  
  23.                 break;  
  24.             }  
  25.         }  
  26.         if(k == n){  
  27.             R[n++] = max;  
  28.         }  
  29.     }  
  30.     ofstream write("1.txt");  
  31.     for(int i = 0; i < n; i++){  
  32.         write<<R[i]<<' '<<A[R[i]]<<endl;  
  33.     }  
  34.     return 0;  
  35. }  

(2)下面就需要用到线段树来快速求解类约瑟夫问题。因为普通方法往往需要遍历整个序列,当数据量很大时,必然超时。用build 构建线段树保存当前区间的未跳出人数,pushUp 求和向上传递,query 返回区间下标并删除该元素即数量减1。这个其实并不困难,只要想到了,和上面的也是差不多的。

(3)关键在于相对下标的求解。相对下标是指删除元素后,某元素在删除后序列的下标。query 每次返回的是绝对下标即原序列的下标(这是因为每次都需要取到该孩子手上的数字),我们的目的在于已知当前的相对下标(一开始给出的k就是相对下标),如何根据卡片的数字求出下一个相对下标。这个我弄了很久还是有错,最后参考了网上的思路,得到下面的递推公式。

if( k > 0){

   next相对下标 = (cur相对下标 + 偏移量 - 1)% 当前人数

   if next相对下标 <= 0{

      next相对下标 += 当前人数

   }

}

else{

   next相对下标 = (cur相对下标 + 偏移量)% 当前人数

   if next相对下标 <= 0{

      next相对下标 += 当前人数

   }

}

需要注意到,当k 大于0 时,因为当前小孩已经跳出圈,那么现在的下标其实是当前小孩的下一个小孩的下标,所以偏移量要 -1. 当k 小于0时,则不需要作此变化。取模是为了将编号控制在人数内,上面的当前人数是不包括这个卡片的持有人,因为他已经跳出圈了。另外,因为结果有可能为非正数(数组下标是1-N),这时就需要加上模数。(举个例子,若1的前1人即0,这时加上模数就是N了,也就满足围成圈。而C++的负数取模和正数取模类似,只是加多个负号。如 -7 % 5 = -2)


代码如下:

[cpp]  view plain copy
  1. #include<cstdio>  
  2. using namespace std;  
  3. //(最小)反素数表  
  4. const int maxTurn[35]={1,2,4,6,12,24,36,48,60,120,180,240,360,720,840,1260,1680,2520,5040,7560,10080,15120,20160,25200,27720,45360,50400,55440,83160,110880,166320,221760,277200,332640,498960};  
  5. //最大糖果表  
  6. const int maxCandy[35]={1,2,3,4,6,8,9,10,12,16,18,20,24,30,32,36,40,48,60,64,72,80,84,90,96,100,108,120,128,144,160,168,180,192,200};  
  7. const int MAX = 500000 + 10;  
  8. struct child{  
  9.     char name[11];  
  10.     int k;  
  11. };  
  12. child A[MAX];             //保存输入数据  
  13. int leftSum[MAX << 2];  //保存区间内剩余人数  
  14. void pushUp(int cur){  
  15.     //更新区间剩余人数  
  16.     leftSum[cur] = leftSum[cur << 1] + leftSum[cur << 1 | 1];  
  17. }  
  18. void build(int l, int r, int cur){  
  19.     //初始化线段树  
  20.     if(l == r){  
  21.         leftSum[cur] = 1;  
  22.         return;  
  23.     }  
  24.     int mid = (l + r) >> 1;  
  25.     build(l, mid, cur << 1);  
  26.     build(mid+1, r, cur << 1 | 1);  
  27.     pushUp(cur);  
  28. }  
  29. int query(int i, int l, int r, int cur){  
  30.     //删除第i个元素并返回原下标  
  31.     if(l == r){  
  32.         leftSum[cur] = 0;  
  33.         return l;  
  34.     }  
  35.     int mid = (l + r) >> 1;  
  36.     int result;  
  37.     if(i <= leftSum[cur << 1]){  
  38.         result = query(i, l, mid, cur << 1);  
  39.     }  
  40.     else{  
  41.         result = query(i - leftSum[cur << 1], mid+1, r, cur << 1 | 1);  
  42.     }  
  43.     pushUp(cur);  
  44.     return result;  
  45. }  
  46. int main(){  
  47.     int n,m;  
  48.     while(~scanf("%d%d",&n,&m)){  
  49.         for(int i = 1; i <= n; i ++){  
  50.             scanf("%s%d",A[i].name,&A[i].k);  
  51.         }  
  52.         build(1, n, 1);  
  53.         int luckyNumber = 0;  
  54.         while(luckyNumber < 35 && maxTurn[luckyNumber] <= n){  
  55.             luckyNumber++;  
  56.         }  
  57.         luckyNumber--;  
  58.         int pos = 0;  
  59.         A[pos].k = 0;  
  60.         int mod = n;  
  61.         for(int i = 0; i < maxTurn[luckyNumber]; i++,mod--){  
  62.             if(A[pos].k > 0){  
  63.                 m = (m + A[pos].k - 1) % mod;  
  64.             }  
  65.             else{  
  66.                 m = (m + A[pos].k) % mod;  
  67.             }  
  68.             if(m <= 0){  
  69.                 m += mod;  
  70.             }  
  71.             pos = query(m, 1, n, 1);  
  72.         }  
  73.         printf("%s %d\n",A[pos].name, maxCandy[luckyNumber]);  
  74.     }  
  75.     return 0;  
  76. }  

总结:

对于一些不是很明显可以用线段树来做的题目,我们要提取它们的区间特征,也就是用什么来做区间下标,需要每个区间保存什么内容,每次修改什么值,怎样更新内容。我认为用什么来做区间下标和每个区间保存什么内容尤为重要,只要说清楚这个两个问题,基本思路就出来了。

采用线段树而不是其他方法,主要是因为线段树的高效,并不是说一般方法做不到。所以很难说什么样的问题归到线段树的类型。线段树的另一个特点就是空间开销非常大,因为需要保存每一块区间的数据。典型的空间换时间。

单点更新是线段树的最基本内容,因为还不需要用到延迟标识,每次更新都是更新到底。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值