详解代码(线段树) POJ-3468: A Simple Problem with Integers

题意:

你有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 ≤ NQ ≤ 100000。
第二行包含 N 个数字,即 A1、A2、...、AN 的初始值。-1000000000 ≤ Ai ≤ 1000000000。
接下来的每条 Q 行表示一个操作。
“C a b c”表示将 c 添加到 AaAa+1、...、Ab 的每个位置。-10000 ≤ c ≤ 10000。
“Q a b”表示查询 AaA 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的用处和用法。他很巧妙,但并不难理解。不需要很精通(当然能精通最好),马马虎虎也能写题,他不是什么很难的东西,走着走着你就发现他对你来说变简单了!能往前走就好!

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值