题意:
你有N个整数,A1,A2,...,AN。您需要处理两种操作。
一种类型的操作是在给定的 区间范围 内向每个数字添加一些给定的数字。
另一种方法是询问给定间隔内的数字总和。
输入样例:
10 5
1 2 3 4 5 6 7 8 9 10
Q 4 4
Q 1 10
Q 2 4
C 3 6 3
Q 2 4
第一行包含两个数字 N 和 Q。1 ≤ N,Q ≤ 100000。
第二行包含 N 个数字,即 A1、A2、...、AN 的初始值。-1000000000 ≤ Ai ≤ 1000000000。
接下来的每条 Q 行表示一个操作。
“C a b c”表示将 c 添加到 Aa、Aa+1、...、Ab 的每个位置。-10000 ≤ c ≤ 10000。
“Q a b”表示查询 Aa、A a+1、...、Ab 的总和。
输出样例
4
55
9
15
您需要按顺序回答所有 Q 命令。一行一个答案。
题目分析:
很明显从逻辑上来说是一道关于线段树或者树状数组的模板题,需要我们去维护一个区间和,并且随时准备修改区间值并且对修改后的数组进行区间查询,查询区间元素总和!
我们使用线段树来写,也更好理解
线段树构造结果如下,不过实际上线段树是在一维数组上实现的,我们通过数学逻辑将每个元素都与他的儿子连接起来,实际上是一个不断跳跃检索的一维数组
话不多说——详细代码注释在下:
#include<iostream>
#include<algorithm>
#include<cstring>
#include<string>
#include<cstdio>
#define llint long long
//不开long long 就会见到你喜欢的 wa
using namespace std;
llint date[100006];
//储存第一手数据,这里面的玩意除了赋值没啥用
struct Node{
llint l,r,sum,lazy;
}tree[500024];
//一个结构体然后再开个结构体类型数组tree数组
//就是将一大堆相关的、可以同步的数组直接整合成一个数组
//你也可以理解为一个单元就可以装好几个元素的数组,每个元素互相有联系,有各自的作用
//里面储存区间左区间:l 和右区间:r 区间维护值:sum(在这里是表示区间和)
//lazy下面再说
void build(llint p,llint L,llint R){
//建立基础线段树
tree[p].l=L;
tree[p].r=R;
tree[p].sum=date[L];
//为什么每个区间的sum都要初始为左边界上的值呢?
//我们第一次进入函数是是包含了整个数组数据的,也就是l=1 、r=n
//随着下面的不断分段最终l==r,此时整个区间就精确到了一个数
//所以,其实我们想要的只是最下面那些没有子节点的叶子节点,他们的sum就是自己
//而上面的我们可以通过从底层回溯时两端相加就能得到所有的初始sum
//可以判断l==r从而单独赋值,不过没必要,时间差不多的,图个方便
tree[p].lazy=0;
//初始化懒惰标记,没错,我将整个lazy成为懒惰标记,因为他能帮助我们偷懒
if(L==R){
return;
}
//到达最底层自然应该向上回溯了
llint mid=(L+R) >> 1;
//这里是取当前区间的中间下标,不明白可以试试看L和R分别代表当前区间起始位置和终止位置
//至于 “>>” 符号是位运算符:右移 。就是除2的意思,只不过比写“/2”运算更快
build(p*2,L,mid);
//向左分段建树
build(p*2+1,mid+1,R);
//向右分段建树
//为什么是p*2和p*2+1呢?
//虽然一颗线段树画出来看起来是有层次的,但是其实是再一维数组上实现的,实际结构是一条线
//当根节点p初始为1时,p*2和p*2+1刚好能够引索到对应的儿子结点,往下不断递推就得到一颗树
tree[p].sum=tree[p*2].sum+tree[p*2+1].sum;
//这就是回溯过程了在回溯过程才是真正得到sum值的时候
//上一层的sum等于他两个儿子区间的sum值之和
}
void lazy_down(llint p){
//标记下放函数
if(tree[p].lazy){
//如果当前区间有标记,就进行下放
tree[p*2].sum+=tree[p].lazy*(tree[p*2].r-tree[p*2].l+1);
//改变左儿子的sum值,式子与区间修改函数里面那个一样的。
//因为lazy记录的加的值,所以用lazy代替x就行了
tree[p*2+1].sum+=tree[p].lazy*(tree[p*2+1].r-tree[p*2+1].l+1);
//改变右儿子嘛
tree[p*2].lazy+=tree[p].lazy;
//儿子区间修改完毕,那儿子区间修改了,他就应该继承父亲lazy标记
//毕竟还有可能下次还要往下,lazy他不能丢了呀,帐得记上
tree[p*2+1].lazy+=tree[p].lazy;
//两个儿子都要嘛
tree[p].lazy=0;
//父亲的标记下放完了,他的lazy传给了儿子了,lazy清0
//因为万一下次又到这里往下,不清0就又会对儿子修改,就多修改了
}
}
void change(llint p,llint L,llint R,llint x){
//区间修改(将L和R初始为两个相同的位置,就能用于单点修改)
//p:根节点 L:修改区间的左边界 R:修改区间的右边界 x:区间内每个元素需要加的值
if(L<=tree[p].l&&R>=tree[p].r){
//如果我当前区间整个都被要修改的区间包住了,那么是不是说明整个区间所有元素都要加x
tree[p].sum+=(tree[p].r-tree[p].l+1)*x;
//那么直接对当前区间的sum一步到位
//新sum=老sum+区间内元素个数*x
tree[p].lazy+=x;
//用懒惰标记lazy记录下这个区间我曾经改变过x
return;
//因为下面也许我们根本就不用查了,所以我们偷懒,只要他不查,我们就不改,直接返回。
}
llint mid=(tree[p].l+tree[p].r) >> 1;
//上面都是临界条件,下面才开始跟踪修改
//首先得到一个区间中间值
lazy_down(p);
//由于我们要往下走,那么如果当前有改变,我们就应该吧改变提前改到下面去,并且删掉当前改变标记,以防多次改变
//我们称之为标记下放:函数里详细解释
if(L<=mid)
//如果要改的区间有在左边
//那么我们去左边找那部分
change(p*2,L,R,x);
//向左找
if(R>mid)
//如果有在右边
//那么我们去右边找那部分
change(p*2+1,L,R,x);
//向右找
tree[p].sum=tree[p*2].sum+tree[p*2+1].sum;
//最后,我们既然改变了区间,那么包含他的区间们,自然也应该改变了,所以回溯过程中更新上面的sum
}
llint Find(llint p,llint L,llint R){
//查询函数
//各个参数意义与上面没区别
if(tree[p].l>=L&&tree[p].r<=R){
return tree[p].sum;
}
//临界条件时:当前区间被完全包裹在查询区间内,证明这个区间的sum是我答案的全部或者一部分
llint mid=(tree[p].r+tree[p].l) >> 1;
//中间值
lazy_down(p);
//由于我们之前偷懒了,修改并没有修改到底层,但是如果我们查询的时候需要去到底层取值呢?
//那么我们的lazy标记就起作用了,他记录了我有没有修改此区间,以及改了多少
//那么同理,我还是想偷懒,你查到哪里我就改到哪里,如果你还要往下,并且当前我的lazy有值
//那我就提前进去改儿子区间的sum值,并将lazy标记下放到下一个区间
llint ans = 0;
//一个储存每层查找答案的容器,每一次递归都会创建一个ans所以他并不是一个全局的变量
//只是一个临时变量,只不过每次这个临时变量的名字都一样
if(L<=mid)
ans+=Find(p*2,L,R);
if(R>mid)
ans+=Find(p*2+1,L,R);
//和修改操作相同:向左向右分别尝试去得到查询值
return ans;
//将当前的这个ans返回到上一层
//有一丢丢的复杂,可以慢慢理解,没理解也没关系,学着学着就懂了。
}
int main(){
llint n,m;
scanf("%lld%lld",&n,&m);
for(llint i=1;i<=n;i++){
scanf("%lld",&date[i]);
}
//初始数据的输入嘛
build(1,1,n);
//初始数据都有了,自然可以建立初始线段树啦~
while(m--){
//m个操作嘛
char s[2];
//为什么用数组不用字符型变量呢?
//因为那样要加一个getchar来吞掉一个换行,是家在字符输入前面的,刚开始一直错,索性就用字符串了
llint a,b,c;
scanf("%s%lld%lld",s,&a,&b);
//可用数据的输入嘛
if(s[0]=='C'){
//第一种操作嘛
scanf("%lld",&c);
//输入修改值嘛
change(1,a,b,c);
//改嘛
}
else{
//第二种操作嘛
printf("%lld\n",Find(1,a,b));
//查询并输出嘛(偷懒嘛)
}
}
return 0;
}
//啊,温馨提示c++能过,G++超时,emm...这个跟提交逻辑有关,暂且不提
//一个被打上了线段树标签的题,时间卡得也确实有点死
//本代码可直接ac题(c++)
其他的就没什么好说的了,注意看注释,可能字有点小,黑了点,不太好看,我不知道怎么调,就这样看吧
对于线段树的区间修改和查询问题,最重要和最精髓的就是我提到的 “lazy”——懒惰标记,他帮我们大大节省了时间(贪!就硬贪!)
好好理解理解lazy的用处和用法。他很巧妙,但并不难理解。不需要很精通(当然能精通最好),马马虎虎也能写题,他不是什么很难的东西,走着走着你就发现他对你来说变简单了!能往前走就好!