操作格子
一、题目分析
刚刚看完题目的时候,我立马想到的是用数组来存储 n 个格子的权值,但是,这样做的话,没有办法在时间限制的 1s 完成操作,出现运行超时。
这是因为题目的数据规模与约定是:对于 100% 的数据 1 <= n <= 100000,m <= 100000,0 <= 格子权值 <= 10000。详细分析如下:
由题目可知:操作 1、2 和 3 的时间复杂度分别为:O(1)、O(n) 和 O(n),当 n=100000=10^5,m=100000=10^5 时,所需要运算的次数为0^5*10^5=10^10(次)。再看看我们手上的和市场上的个人电脑,CPU 的主频一般只有 GHz 的数量级,也就是 1s 一般只能运算 10^9(次),无法再题目的时间限制 (1s) 之内完成 10^10(次) 运算,所以会出现运行超时。
因此,这里我们需要选择线段树这种数据结构代替数组来解决这个问题。
二、线段树介绍
线段树将一个线段(区间,如:[1,10])不断地划分,直到划分成一些单元区间,最后形成一棵树,每个单元区间对应线段树中的一个子叶结点。如下图所示:
对于线段树中的每一个非叶子节点 [a,b],它的左儿子表示的区间为 [a,(a+b)/2],右儿子表示的区间为 [(a+b)/2+1,b],最后的子节点数目为 n,即整个线段区间的长度。
三、算法设计
上图所示的线段树是一棵空树,因为每个结点上面还没有挂值,下面给这颗树挂值,并详细叙述线段树在本题的用处。
由题目可知,要求输入格子数目,然后给格子赋初始权值。这里假设格子的数目 n=10,初始权值分别为 1,2,3,4,5,6,7,8,9,10。
首先,定义线段树的节点结构体如下:
1 //定义结构体:线段树的节点 2 typedef struct SegmentTreeNode 3 { 4 int left,right; //分别用于记录线段区间的左端点和右端点的值 5 int weight_sum,weight_max; //分别用于记录连续一段格子的权值和与连续一段格子的最大值 6 struct SegmentTreeNode *left_child,*right_child; //分别用于指向该线段树节点的左和右子节点 7 }STNode;
然后,逐一给这棵树挂值:
1. 权值 1
- 1 在区间 [1,10] 里,该结点 weight_sum=1,weight_max=1;
- 1 在区间 [1,5] 里,该节点 weight_sum=1,weight_max=1;
- 1 在区间 [1,3] 里,该节点 weight_sum=1,weight_max=1;
- 1 在区间 [1,2] 里,该结点 weight_sum=1,weight_max=1;
- 1 在区间 [1,1] 里,该结点 weight_sum=1,weight_max=1。
2. 权值 2
- 2 在区间 [1,10] 里,因为 2 大于上面的 weight_max=1,所以 weight_sum=1+2=3,weight_max=2;
- 2 在区间 [1,5] 里,同上, weight_sum=1+2=3,weight_max=2;
- 2 在区间 [1,3] 里,同上, weight_sum=1+2=3,weight_max=2;
- 2 在区间 [1,2] 里,同上, weight_sum=1+2=3,weight_max=2;
- 2 在区间 [2,2] 里,同上, weight_sum=1+2=3,weight_max=2。
以此类推,直到最后一个权值 10 挂在线段树上。最后得到的线段树各个线段(区间)节点的状态如下:
线段(区间) | weight_max | weight_sum |
---|---|---|
[1,10] | 10 | 1+2+3+...+10 |
[1,5] | 5 | 1+2+3+4+5 |
[6,10] | 10 | 6+7+8+9+10 |
[1,3] | 3 | 1+2+3 |
[4,5] | 5 | 4+5 |
[6,8] | 8 | 6+7+8 |
[9,10] | 10 | 9+10 |
[1,2] | 2 | 1+2 |
3 | 3 | 3 |
4 | 4 | 4 |
5 | 5 | 5 |
[6,7] | 7 | 6+7 |
8 | 8 | 8 |
9 | 9 | 9 |
10 | 10 | 10 |
1 | 1 | 1 |
2 | 2 | 2 |
6 | 6 | 6 |
7 | 7 | 7 |
根据上面的表格,回看一下题目的要求(操作类型):
- 修改一个格子的权值
- 求连续一段格子权值和
- 求连续一段格子的最大值
显然,对于操作类型 2 和 3,我们可以直接从线段(区间)节点中获得,这就是在本题中使用线段树的好处。
对于操作类型 1,当我们找到要修改权值的格子后,显然不能只对该格子的权值作简单的修改,因为修改了该格子的权值后,它的前驱节点的权值和 (weight_max) 和权值最大值 (weight_sum) 也需要作相应的改变。但是,这个问题我们很容易可以想到使用递归的方法来解决,使函数调用在递归返回的时候,每向上返回一层,对权值和 (weight_max) 和权值最大值 (weight_sum) 也作相应的修改。
四、程序设计
1 #include<stdio.h> 2 #include<stdlib.h> 3 4 //定义结构体:线段树的节点 5 typedef struct SegmentTreeNode 6 { 7 int left,right; //分别用于记录线段区间的左端点和右端点的值 8 int weight_sum,weight_max; //分别用于记录连续一段格子的权值和与连续一段格子的最大值 9 struct SegmentTreeNode *left_child,*right_child; //分别用于指向该线段树节点的左和右子节点 10 }STNode; 11 12 //函数声明 13 STNode *SegmentTreeInit(int left, int right); //线段树初始化 14 int GetMaximum(int a,int b); //得到最大值 15 void SegmentTreeAssignment(STNode *segment_tree_node,int i,int weight); //给线段树中的指定的单元区间节点赋权值 16 void SegmentTreeModification(STNode *segment_tree_node,int i,int weight); //修改指定格子(单元区间节点)的权值 17 int SegmentTreeGetWeightSum(STNode *segment_tree_node,int left,int right); //求连续一段格子的权值和 18 int SegmentTreeGetWeightMaximum(STNode *segment_tree_node,int left,int right); //求连续一段格子的最大值 19 20 //主函数 21 int main() 22 { 23 int i,j; 24 int n,m; //分别用于记录输入的格子的数目和操作的次数 25 STNode *segment_tree_node; //用于指向根据输入的格子数 n 初始化的线段树 [1,n] 26 int weight; //用于记录输入的格子的权值 27 int operation[100000][3]={0}; //用于记录操作类型和参数:operation[][0]:操作序号,operation[][1]:x,operation[][2]:y 28 29 scanf("%d%d",&n,&m); //输入格子的数目 n 和操作的次数 m 30 31 segment_tree_node=SegmentTreeInit(1,n); //线段树初始化 32 33 //初始化权值 34 for(i=1;i<=n;i++) 35 { 36 scanf("%d",&weight); //输入权值 37 SegmentTreeAssignment(segment_tree_node,i,weight); //给线段树中的指定的单元区间节点赋权值 38 } 39 40 //输入 m 次操作并执行 41 for(i=0;i<m;i++) 42 for(j=0;j<3;j++) 43 scanf("%d",&operation[i][j]); //输入操作类型和参数 44 for(i=0;i<m;i++) //执行操作 45 switch(operation[i][0]) 46 { 47 case 1:SegmentTreeModification(segment_tree_node,operation[i][1],operation[i][2]); break; //修改指定格子(单元区间节点)的权值 48 case 2:printf("%d\n",SegmentTreeGetWeightSum(segment_tree_node,operation[i][1],operation[i][2])); break; //求连续一段格子的权值和 49 case 3:printf("%d\n",SegmentTreeGetWeightMaximum(segment_tree_node,operation[i][1],operation[i][2])); break; //求连续一段格子的最大值 50 default:break; 51 } 52 53 return 0; 54 } 55 56 /********************************************************************************************************* 57 ** 函数功能 :线段树初始化 58 ** 函数说明 :无 59 ** 入口参数 :left:线段树当前节点的区间左端点值 60 ** :right:线段树当前节点的区间右端点值 61 ** 出口参数 :指向该线段树的指针(初始化后的线段树的第一个节点的地址) 62 *********************************************************************************************************/ 63 STNode *SegmentTreeInit(int left,int right) 64 { 65 STNode *segment_tree_node=(STNode *)malloc(sizeof(STNode)); //申请 sizeof(STNode) 大小的内存空间,新建一个线段树节点 66 67 segment_tree_node->left=left; //新建节点的区间左端点赋初值 left 68 segment_tree_node->right=right; //新建节点的区间右端点赋初值 right 69 segment_tree_node->weight_sum=0; //新建节点中记录的连续一段格子的权值和赋初值 0 70 segment_tree_node->weight_max=0; //新建节点中记录的连续一段格子的权值最大值赋初值 0 71 segment_tree_node->left_child=NULL; //新建节点中指向其左子节点的指针赋初值 NULL 72 segment_tree_node->right_child=NULL; //新建节点中指向其右子节点的指针赋初值 NULL 73 74 if(right!=left) //如果新建的节点的区间不是单元区间,则线段树未完成初始化,继续分割区间 75 { 76 int middle=(left+right)/2; //获得新建节点区间的中值 77 segment_tree_node->left_child=SegmentTreeInit(left,middle); //继续初始化新建节点的左子树 78 segment_tree_node->right_child=SegmentTreeInit(middle+1,right); //继续初始化新建节点的右子树 79 } 80 81 return segment_tree_node; //返回指向该线段树的指针(初始化后的线段树的第一个节点的地址) 82 } 83 84 /********************************************************************************************************* 85 ** 函数功能 :得到最大值 86 ** 函数说明 :无 87 ** 入口参数 :a、b:进行比较的数 88 ** 出口参数 :比较后的最大值 89 *********************************************************************************************************/ 90 int GetMaximum(int a,int b) 91 { 92 if(a>b) 93 return a; 94 else 95 return b; 96 } 97 98 /********************************************************************************************************* 99 ** 函数功能 :给线段树中的指定的单元区间节点赋权值 100 ** 函数说明 :在给指定单元区间节点赋权值的同时,更新包含该单元区间节点的区间节点的权值和与最大值 101 ** 入口参数 :segment_tree_node:指向线段树的指针 102 :i:要赋权值线段树的单元区间节点 i 103 ** :weight:要赋的权值 104 ** 出口参数 :无 105 *********************************************************************************************************/ 106 void SegmentTreeAssignment(STNode *segment_tree_node,int i,int weight) 107 { 108 segment_tree_node->weight_sum+=weight; //更新包含单元区间节点 i 的区间节点中的权值和 109 segment_tree_node->weight_max=GetMaximum(segment_tree_node->weight_max,weight); //更新包含单元区间节点 i 的区间节点中的权值最大值 110 111 //寻找单元区间节点 i 112 if(segment_tree_node->left==segment_tree_node->right) //搜索到单元区间节点 i 113 return; 114 else //没有,继续搜索 115 if (i<=(segment_tree_node->left+segment_tree_node->right)/2) 116 SegmentTreeAssignment(segment_tree_node->left_child,i,weight); //往左子树搜索 117 else 118 SegmentTreeAssignment(segment_tree_node->right_child,i,weight); //往右子树搜索 119 120 return; 121 } 122 123 /********************************************************************************************************* 124 ** 函数功能 :修改指定格子(单元区间节点)的权值 125 ** 函数说明 :在修改指定格子(单元区间节点)权值的同时,修改包含该单元区间节点的区间节点的权值和与最大值 126 ** 入口参数 :segment_tree_node:指向线段树的指针 127 :i:要修改权值的格子(单元区间节点) i 128 ** :weight:要修改的权值 129 ** 出口参数 :无 130 *********************************************************************************************************/ 131 void SegmentTreeModification(STNode *segment_tree_node,int i,int weight) 132 { 133 if(segment_tree_node->left==i&&segment_tree_node->right==i) //搜索到该格子(单元区间节点) 134 { 135 segment_tree_node->weight_sum=weight; //修改指定格子(单元区间节点)中记录的连续一段格子的权值和 136 segment_tree_node->weight_max=weight; //修改指定格子(单元区间节点)中记录的连续一段格子的权值最大值 137 return; 138 } 139 else //没有,继续搜索 140 { 141 int middle=(segment_tree_node->left+segment_tree_node->right)/2; //获得当前节点区间的中值 142 if(i<=middle) //往左子树搜索 143 SegmentTreeModification(segment_tree_node->left_child,i,weight); 144 else //往右子树搜索 145 SegmentTreeModification(segment_tree_node->right_child,i,weight); 146 147 segment_tree_node->weight_sum=segment_tree_node->left_child->weight_sum+segment_tree_node->right_child->weight_sum; //修改包含单元区间节点 i 的区间节点中的权值和 148 segment_tree_node->weight_max=GetMaximum(segment_tree_node->left_child->weight_max,segment_tree_node->right_child->weight_max); //修改包含单元区间节点 i 的区间节点中的权值最大值 149 } 150 151 return; 152 } 153 154 /********************************************************************************************************* 155 ** 函数功能 :求连续一段格子的权值和 156 ** 函数说明 :无 157 ** 入口参数 :segment_tree_node:指向线段树的指针 158 :left:连续一段格子的左端点值 159 ** :right:连续一段格子的右端点值 160 ** 出口参数 :连续一段格子的权值和 161 *********************************************************************************************************/ 162 int SegmentTreeGetWeightSum(STNode *segment_tree_node,int left,int right) 163 { 164 if(segment_tree_node->left==left&&segment_tree_node->right==right) //搜索到该段格子 165 return segment_tree_node->weight_sum; 166 else 167 { 168 int middle=(segment_tree_node->left+segment_tree_node->right)/2; 169 if (right<=middle) //往左子树搜索 170 return SegmentTreeGetWeightSum(segment_tree_node->left_child,left,right); 171 else if(left>middle) //往右子树搜索 172 return SegmentTreeGetWeightSum(segment_tree_node->right_child,left,right); 173 else //分别往左子树和右子树搜索,最后相加 174 return SegmentTreeGetWeightSum(segment_tree_node->left_child,left,middle)+SegmentTreeGetWeightSum(segment_tree_node->right_child,middle+1,right); 175 } 176 } 177 178 /********************************************************************************************************* 179 ** 函数功能 :求连续一段格子的最大值 180 ** 函数说明 :无 181 ** 入口参数 :segment_tree_node:指向线段树的指针 182 :left:连续一段格子的左端点值 183 ** :right:连续一段格子的右端点值 184 ** 出口参数 :连续一段格子的最大值 185 *********************************************************************************************************/ 186 int SegmentTreeGetWeightMaximum(STNode *segment_tree_node,int left,int right) 187 { 188 if(segment_tree_node->left==left&&segment_tree_node->right==right) //搜索到该段格子 189 return segment_tree_node->weight_max; 190 else 191 { 192 int middle=(segment_tree_node->left+segment_tree_node->right)/2; 193 if(right<=middle) //往左子树搜索 194 return SegmentTreeGetWeightMaximum(segment_tree_node->left_child,left,right); 195 else if (left>middle) //往右子树搜索 196 return SegmentTreeGetWeightMaximum(segment_tree_node->right_child,left,right); 197 else //分叉:返回搜索到的最大值 198 return GetMaximum(SegmentTreeGetWeightMaximum(segment_tree_node->left_child,left,middle),SegmentTreeGetWeightMaximum(segment_tree_node->right_child,middle+1,right)); 199 } 200 }