感觉我整理数据结构的速度永远跟不上考试的速度啊…
那么!撰写日期是在7.17,在这一天的模拟赛的T2用到了线段树。虽然打出来了板子但是忘掉了一些东西。
就趁着这个机会AK线段树吧!!
----补于8.6
因为知识体系的庞大,所以这篇博文里包含了朴素线段树、Lazy标记、区间修改、扫描线、动态开点与线段树合并。
但动态开点于线段树合并由于彻底抛弃了某些常用概念。
------于 2019 11/11
猛然发现挂出来的线段树板子是个挂了的板子…原因是一个区间符号打错了。现已修正。
理论概念
线段树是基于分治思想的二叉树结构。树上的每一个节点都代表一个区间,其根节点就是整个统计范围(1-N),每一个叶节点就是长度为1的区间(也就是一个元素)。对于每个内部节点[l-r],它的左节点是[l,mid],右节点是[mid+1,r],其中mid=(l+r)/2(向下取整)。
根据这些定义,我们大概也能判断他常用的题型了。什么区间最值啊之类的涉及区间的东西。因为他大多情况下是一颗比较完整的完全二叉树(我的意思是不会浪费特别大的空间…),所以我们一般用“父子二倍”的编号方法。即根节点编号为1。对于任意一个节点x,它的左节点为2x,右节点为2x+1。为了保证不会越界,对于N个叶节点的线段树,我们将数组的长度开到4N
搭建
线段树的基本用途就是对序列的维护,支持查询与修改指令。线段树可以非常方便的从下向上传递信息。我们举个区间最小值的例子(诶嘿就是要跟小蓝书反着走!),对于一个节点Data(l,r),显然Data(l,r)=min(Data(l,mid),Data(mid+1,r))。所以啊,我们用一个结构体保存每个节点。每个节点就包括三个信息。区间左端点L,区间右端点R,和这个节点存放的值Data
那么接下来上一手区间最小值的代码233
struct S{
int l,r;
int dat;
}T[4*MAXN];
void build(int p,int l,int r){
T[p].l=l;//别问我P是什么东西,你用数组的时候总要有个下标的吧= =
T[p].r=r;
if(l==r){//叶子节点!!
T[p].dat=R[l];
return;
}
int mid=(T[p].l+T[p].r)/2;
build(p*2,l,mid);//左节点
build(p*2+1,mid+1,r);//右节点
T[p].dat=min(T[p*2].dat,T[p*2+1].dat);//如果你想改成区间最大值,就把这个min函数改动一下好了。
}
build(1,1,n);//入口
单点修改
这里支持的修改是单点,也就是一次只更改一个节点的值。你可以指定他改成某个元素或者令他进行计算以改变值。
在线段树中,根节点是各种指令的入口。我们需要从根节点出发,层层递归来找代表着所需区间的节点。然后从下往上更新。
下面上的是复杂度为O(log N)的单点修改
void change(int p,int x,int v){
//v是要减小的值,p是入口,x是所需更该节点的位置
if(T[p].l==T[p].r){
T[p].dat-=v;
return;
}
int mid=(T[p].l+T[p].r)/2;
if(x<=mid){
change(p*2,x,v);
}
else{
change(p*2+1,x,v);
}
T[p].dat=min(T[p*2].dat,T[p*2+1].dat);
}
change(1,x,v);//入口
区间修改
注意:为了把功能放在一起,所以我把这个知识点放在了单点修改下面。但这个知识点更接近于压轴难度。所以可以先看下面的查询功能,再来看这个区间修改。
(小声)怎么办小蓝书上并没有纯粹的区间修改…给了个例题还加持了数论的buff
啊数论啊!我数学蒟蒻啊兄Dei!!
好的我们换一个质朴的例题来打板子吧。
(说着他翻开了小蓝书的下一页,并发现了藏在延迟标记里的区间修改)
如果我们要修改区间,那么想想看,如何实现?
众所周知,OI的本质就是倒水(雾)。我们最轻松可以想到的是,直接循环调用单点修改。
那么我们本来高效的O(log N)就这么变成了O(N)!!!!这不能忍。可怎么想,这也没什么可优化的余地啊。那怎么办?
偷懒啊!
什么意思?我们仔细思考一下,这就和我们做作业是一个道理。最恐怖的不是你没做作业,最恐怖的是你废了一大堆时间做作业,老!师!不!检!查!(大雾!!!)
所以,我们就在他每次要求我们修改的时候,只要他不问,我就不修改!
那我怎么要在他问的时候还能让程序记着这个点要修改了再用呢?
这就是延迟标记(也有人叫懒惰/Lazy标记)的作用咯。
那也就是说,我们要在执行修改指令时,发现P点所在区间被完全覆盖时,可以立即返回,不过我们要留一个延迟标记再走。
在后续的指令中,往下递归的时候,如果发现了这里存在一个延迟标记,那么就根据这个标记的内容来更新下方的节点,同时清除这个标记,为下方的子节点打上标记。
故,这个标记的真正定义是:
这个节点曾经被修改,但其子节点尚未被更新。
请留意这个概念,因为犯错概率有点大…
下面放出代码,有些长。
#include<iostream>
using namespace std;
const int MAXN=100010;
struct SegmentTree{
int l,r;
long long sum,add;//这个add就是所谓的延迟标记!
}tree[MAXN*4];
int a[MAXN],n,m;
void build(int p,int l,int r){
tree[p].l=l;
tree[p].r=r;
tree[p].add=0;
if(l==r){
tree[p].sum=a[l];
return ;
}
int mid=(l+r)/2;
build(p*2,l,mid);
build(p*2+1,mid+1,r);
tree[p].sum=tree[p*2].sum+tree[p*2+1].sum;
}
void spread(int p){
if(tree[p].add){
//如果这个节点被标记了延迟标记
/*
----------Warning--------
延迟标记的意义,是“这个节点曾经被修改,但其子节点尚未被更新。”也就是说,他自己已经更新过了
但他下面的子节点都没有更新。这就意味着我们遇到这个标记的时候,要对他的子节点进行更新。
*/
tree[p*2].sum+=tree[p].add*(tree[p*2].r-tree[p*2].l+1);//更新左节点
tree[p*2+1].sum+=tree[p].add*(tree[p*2+1].r-tree[p*2+1].l+1);//更新右节点
tree[p*2].add+=tree[p].add;//给左节点打标
tree[p*2+1].add+=tree[p].add;//给右节点打标
tree[p].add=0;//清除p的标记
}
}
void change(int p,int l,int r,int d){
if(l<=tree[p].l&&r>=tree[p].r){
tree[p].sum+=(long long)d*(tree[p].r-tree[p].l+1);//更新该节点
tree[p].add+=d;//打上延迟标记
return ;
}
spread(p);//传播延迟标记
int mid=(tree[p].l+tree[p].r)/2;
if(l<=mid){
change(p*2,l,r,d);
}
if(r>mid){
change(p*2+1,l,r,d);
}
tree[p].sum=tree[p*2].sum+tree[p*2+1].sum;
}
long long ask(int p,int l,int r){
if(l<=tree[p].l&&r>=tree[p].r){
return tree[p].sum;
}
spread(p);
int mid=(tree[p].l+tree[p].r)/2;
long long val=0;
if(l<=mid){
val+=ask(p*2,l,r);
}
if(r>mid){
val+=ask(p*2+1,l,r);
}
return val;
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
}
build(1,1,n);
while(m--){
char op[2];int l,r,d;
cin>>op>>l>>r;
if(op[0]=='C'){
cin>>d;
change(1,l,r,d);
}
else{
cout<<ask(1,l,r)<<endl;
}
}
}
区间查询
想查询某个区间的最值等等?就在这个功能里实现啦!
我们只需要从根节点开始,递归执行这样的操作:
//注:下面的l,r代表的是你需要查询的区间左端点、右端点。
1.若[l,r]完全覆盖了当前节点代表的区间,那么立即回溯,并将该节点的Data值作为候选答案
2.若左子节点和[l,r]有重叠部分,则递归访问左子节点
3.若右子节点与[l,r]有重叠,则递归访问右子节点
那么附上查询的代码
int ask(int p,int l,int r){
if(l<=T[p].l&&r>=T[p].r){
return T[p].dat;
}
int mid=(T[p].l+T[p].r)/2;
int val=1<<30;//如果你要查询区间最大值,那么把这个值加个负号就好了。
if(l<=mid){
val=min(val,ask(p*2,l,r));
}
if(r>mid){
val=min(val,ask(p*2+1,l,r));
}
return val;
}
合并
这个操作是在动态开点的线段树上进行的。
什么是动态开点?详见下文在对 A simple Problem with Integers 中的题解。
如果若干个线段树,他们都维护相同的区间[1,n],那么它们对于各个子区间的划分是一致的。假设有m次单点修改的操作,每次在某一棵线段树上进行。所有操作完成,我们要把对应位置上的值想加,同时得到区间最大值。
对于这个问题,我们就可以用线段树合并来实现。我们依次合并这些线段树。在合并时,用两个指针p,q从两个根节点出发,以递归的方式同步遍历两棵树。确保他们总是代表相同的子区间。
1.若两者有一为空,则以非空者为合并后节点
2.若均不为空,则递归合并两颗左子树和右子树,然后删除q,以p为合并后的节点。自底部向上更新最值信息。
3.如果到达叶子节点,则直接把两个的最值相加
时间复杂度大约在O(m log n)
代码实现如下:
int merge(int p,int q,int l ,int r){
if(!p){
return q;
}
if(!q){
return p;
}
//如果有任意一边树为空,你不用进行合并
if(l==r){
//到达叶子
tr[p].dat+=tr[q].dat;
return p;
}
int mid=(l+r)>>1;
tr[p].lc=merge(tr[p].lc,tr[q].lc,l,mid);//递归合并左子树
tr[q].rc=merge(tr[p].rc,tr[q].lc,mid+1,r);//递归合并右子树
tr[p].dat=max(tr[tr[p].lc].dat,tr[tr[p].rc].dat);//更新
return p;//以p为合并后的节点,相当于删除了q
}
题目中的Show Time
子题型—扫描线
-------补充于8月6日。不得不说线段树这个知识点是真的庞大…
什么是扫描线?既然是题型,那么就在题目里诠释掉吧。
Atlantis POJ1151
翻译的事情我就不去管了。相信各位都是英文鬼才,精通21国语言那种。
那说一下题目大意,给你N个矩形,矩形可能有重叠。求他们的面积并(也就是重叠部分只算一次)。
第一个数据给测试样例的个数…第二个数据给你个数,下面的数据给你他的顶点坐标………(这是为知识点服务的例题,所以我拒绝花大量功夫在诠释题意上)
那么扫描线是针对这种题型的经典做法。设想有一根线,从下向上抑或从左向右扫过去。那么只有两条矩形临界边会对覆盖面积的大小产生影响。
也就是说,我们从左向右(这里仅以从左向右举例)扫过去,记录下每个矩形的左边和右边。那么他在直线上留下的高度一定是固定的,假定为L。那么其面积一定为L*该段的宽度。
那么这条直线就是扫描线。我们需要取出N个矩形的左右边界,也就是有2N段边线,划分出了2N-1个区间。
我们可以把两个对角顶点坐标暂记为(x1,y1) (x2,y2)并假定x1<x2 , y1<y2
那么就将左边界标记为四元组(x1,y1,y2,1)
右边界标记为(x2,y1,y2,-1)
然后将这2N个四元组间按x进行排序。
但在本题中,y的数据范围不仅很大,而且甚至连整数都不是,所以我们不妨做一下离散化。这里用到一个离散化利器。为了让博文井然有序,我们选择引个超链。
利用sort unique 和Map可以简单的实现离散化。这里设raw(i)表示不同的y值
开一个数组c[i]来表示第i个区间的覆盖次数。c[i]的初始值为0
扫描一遍四元组。对于每一个四元组,我们将其raw(y1)到raw(y2)区间的每个raw[i]加上第四元值(1或者-1),表示覆盖或者结束覆盖。
既然我们要求面积,那如何求呢?长度乘以宽度。宽度一定为x2-x1,那么长度(即在扫描线的高度)是什么呢?
只要c[i]数组不为0,就意味着第i段依然有覆盖。那么局部覆盖长度就是固定的raw(i+1)-raw(i)。我们将长度累加,就可以得到这一条线上被覆盖的总长度。
由此我们得到了长度和宽度,相乘便可以得到局部面积。我们将由此计算来的所有局部面积累加,就得到了最终的面积。
如下是朴素方法的代码实现,其复杂度在O(N2)
#include<iostream>
#include<map>
#include<algorithm>
using namespace std;
const int MAXN=150;
int n,cnt;
double x[MAXN*2],y[MAXN*2];
int c[MAXN*2];
map<double,int> val;
struct Line{
double x1,y1,y2;
int p;
}Lines[MAXN*2];
bool Ruler(const Line &a,const Line &b){
return a.x1<b.x1;
}
double solve(){
sort(y+1,y+1+2*n);//使用STL中函数的离散化时,必须先进行排序操作
sort(Lines+1,Lines+1+2*n,Ruler);//为了从左到右依次处理扫描线,我们需要将Lines进行排序
int y_count=unique(y+1,y+1+2*n)-y-1;//unique完成去重,并且返回去重后数组长度
for(int i=1;i<=y_count;i++){
val[y[i]]=i;
//使用Map实现简单的离散化对应
}
double ans=0;
for(int i=1;i<=2*n;i++){
double y1=Lines[i].y1,y2=Lines[i].y2;
for(int j=val[y1];j<val[y2];j++){
c[j]+=Lines[i].p;
}
double len=0;
for(int j=1;j<y_count;j++){
if(c[j]){
len+=y[j+1]-y[j];
}
}
ans+=(Lines[i+1].x1-Lines[i].x1)*len;
}
return ans;
}
int main(){
while(scanf("%d",&n) && n != 0){
for(int i = 1;i <= n;i++){
scanf("%lf%lf%lf%lf",&x[i],&y[i],&x[i+n],&y[i+n]);
//将一个矩形分为两条竖线,存放在lines内
Lines[i].x1=x[i];
Lines[i].y1=y[i];
Lines[i].y2=y[i+n];
Lines[i].p=1;
Lines[i+n].x1=x[i+n];
Lines[i+n].y1=y[i];
Lines[i+n].y2=y[i+n];
Lines[i+n].p=-1;
}
double ans = solve();
printf("%.2f\n\n",ans);
}
return 0;
}
所以说,这和线段树有什么关系呢?
当然是没关系哒!
将扫描线的覆盖次数统计c[i]、覆盖长度y2-y1查询改成对线段树的区间修改、区间查询操作。
那么就可以将计算扫描线的覆盖长度所付出的代价由O(N)降低为O(logN)
Starts in Your Window/窗口的星星 POJ2482/P1502
耗时一夜…补档于8.14 凌晨2:13…
这里我只写了一遍LuoGu的,我没仔细看,好像两题差不多。如果有差距,下文是LuoGu版本的解法。
首先抛出算法标签,这是一道扫描线的题。首先这道题,可以抽象成我们在拿一个矩形框架框星星,而每个数据的框架大小是固定的。
由于框架大小的固定,我们可以用任何一个顶点来代表这个矩形。我们假定是右上角这格点。这也就是说,对于每一个星星,能框柱它的矩形的右上角会有一个范围。那么对于每个星星的不同范围,产生重叠的区域会同时框住。也就是说,在所有带权的点中,我们需要找到一个权值和最大的重叠区域。
那么问题就向扫描线抽象了。我们只需要界定这个每个区域的边界,也就是矩形的左右两边界。我们可以依然沿用四元组的思维来表示扫描线。
即四元组 (x,y1,y2,id) 其中,x表示横坐标,y1表示矩形下边高,y2表示矩形上边高,id来判断他是左边界还是右边界。
这里由于题目忽略了边框,所以我们对于每个矩形的左下角看做(x,y),右上角看做(x+w-1,y+h-1),就可以处理掉边框问题。
那么还是使用Map+sort+unique的离散化(我认为比较简洁,是否高效我不做保证),沿用上文经典扫描线的思路进行处理。
这题绝对接受不了O(N2),所以我们要引入线段树进行处理。
我们关于Y坐标建立一个线段树,维护区间最大值。初始均为0。我们依次处理每个扫描线,让其进行区间修改,并时刻更新最大值。扫描结束时,得到的最大值就是我们需要的答案。
完成一组测试样例后,切记要进行数据清零,否则会影响后续测试。(我测试了一下,不清零是40分哟)
以下完整代码
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<map>
#include<cstring>
using namespace std;
const int Maxn=10010;
int T;
int n,w,h;
map< double , int > Val;
struct node{
double x,y1,y2;
int Id;
node(){}
node(double x1,double a1,double a2,int iid){
x=x1;y1=a1;y2=a2;Id=iid;
}
}Lines[2002000];
int lazy[4004000];
int Maxer[4004000];
int count_y;
int y[2002000];//离散过后的y
bool Ruler(const node &a,const node &b){
if(a.x==b.x){
return a.Id>b.Id;
}
return a.x<b.x;
}
void Down(int p,int l,int r){
if(lazy[p]==0){
return ;
}
Maxer[p]+=lazy[p];
if(l!=r){
lazy[p<<1]+=lazy[p];
lazy[p<<1|1]+=lazy[p];
}
lazy[p]=0;
}
void Change(int p,int l,int r,int x,int y,int z){
if(x<=l&&y>=r){
lazy[p]+=z;
return;
}
int mid=(l+r)>>1;
if(x<=mid){
Change(p<<1,l,mid,x,y,z);
}
if(y>mid){
Change(p<<1|1,mid+1,r,x,y,z);
}
Down(p<<1,l,mid);
Down(p<<1|1,mid+1,r);
Maxer[p]=max(Maxer[p<<1],Maxer[p<<1|1]);
}
int slove(){
int res=0;
sort(y+1,y+1+n*2);
sort(Lines+1,Lines+1+n*2,Ruler);
count_y=unique(y+1,y+1+2*n)-y-1;
for(int i=1;i<=count_y;i++){
Val[y[i]]=i;
}
//完成离散化
//Build(1,1,count_y);
for(int i=1;i<=2*n;i++){
int l=Val[Lines[i].y1];
int r=Val[Lines[i].y2];
Change(1,1,count_y,l,r,Lines[i].Id);
res=max(res,Maxer[1]);
}
return res;
}
void clean(){
memset(Maxer,0,sizeof(Maxer));
memset(lazy,0,sizeof(lazy));
}
int main(){
scanf("%d",&T);
while(T--){
scanf("%d%d%d",&n,&w,&h);
for(int i=1;i<=n;i++){
int xi,yi,li;
scanf("%d%d%d",&xi,&yi,&li);
Lines[i]=node(xi,yi,yi+h-1,li);//可以容纳此星星的矩形范围的左边界
Lines[i+n]=node(xi+w-1,yi,yi+h-1,-li);//右边界
y[i]=yi;
y[i+n]=yi+h-1;
}
int ans=slove();
printf("%d\n",ans);
clean();
}
}
A simple Problem with Integers POJ3468
经典解法被我当例题开了。详见上文区间修改讲解。
但是。他还有可以利用的价值。
线段树变形—动态开点
众所周知,当水平恒定在某一个阶段的时候,空间和时间就可以进行相互转换,达到某种平衡就可以AC!我们称其为AC秘法
为了降低空间复杂度,我们可以去尝试不构造出整棵树结构,而是只在建立之初留下一个根节点,代表整个区间。当需要访问线段树的某个子树即某个子区间的时候,再建立这个子区间的节点。这和lazy标记有异曲同工之妙,他们都是有用时再做的优化思想。采用这种方法的线段树,它抛弃了父子2倍编号规则,改为使用变量记录左右子节点的编号。同时,他不保存每个节点代表的区间,而是在每次递归访问的时候作为参数传递。
对于这种变形知识点,我们就直接上演示代码,以获得更直观的理解。
#include<iostream>
using namespace std;
struct SegmentTree{
int lc,rc;//左右子节点的编号
int dat;//区间最大值
}tr[SIZE*2];
int root,tot;
int build(){
tot++;
tr[tot].lc=tr[tot].rc=tr[tot].dat=0;
return tot;
}
void instert(int p,int l,int r,int val,int delta){
if(l==r){
tr[p].dat+=delta;
return ;
}
int mid=(l+r)>>1;
if(val<=mid){
if(!tr[p].lc){
tr[p].lc=build();//动态开点
}
insert(tr[p].lc,l,mid,val,delta);
}
else{
if(!tr[p].rc){
tr[p].rc=build();
}
insert(tr[p].rc,mid+1,r,val,delta);
}
tr[p].dat=max(tr[tr[p].lc].dat,tr[tr[p].rc].dat);
}
int main(){
tot=0;
root=build();
insert(root,1,n,val,delta);//调用
}
借教室 NOIP2012提高组Day2真题
----7.18补档
啊,丢死人了。昨天写了一天的线段树,被来打算顺手改好AC这个题做完美谢幕,结果愣是找了一个晚自习的Bug没改出来。
今天仔细看了看,发觉到了一些问题。
最主要的就是,在建树的时候!不!要!忘!记!把!标!记!赋!值!成!0!
其次就是要搞清楚所求范围和节点范围,因为都是r l,所以是相当容易混乱。
我自己试了一下,下面的代码在LuoGu上跑起来确实会有一个点TLE,但这个代码为了可读性(我觉得是),少做了很多优化。比如说把“/2”的操作更改成位运算,加个快读等等。这都能大幅提高速度,所以理论上来说是不会被卡点的。
#include<iostream>
#include<cstdio>
using namespace std;
const long long MAXN=1000010;
long long n,m;
long long R[MAXN];
long long d[MAXN],s[MAXN],t[MAXN];
struct S{
long long l,r;
long long dat;
long long lazy;
}T[4900010];
void build(long long p,long long l,long long r){
//cout<<p<<" "<<l<<" "<<r<<endl;
T[p].l=l;
T[p].r=r;
T[p].lazy=0;
if(l==r){
T[p].dat=R[l];
return;
}
long long mid=(l+r)/2;
//cout<<mid<<endl;
build(p*2,l,mid);
build(p*2+1,mid+1,r);
T[p].dat=min(T[p*2].dat,T[p*2+1].dat);
}
void spread(long long x){
if(T[x].lazy!=0){
//T[x*2].dat-=T[x].lazy*(T[x*2].r-T[x*2].l+1);
// T[x*2+1].dat-=T[x].lazy*(T[x*2+1].r-T[x*2+1].l+1);
T[x*2].dat-=T[x].lazy;
T[x*2+1].dat-=T[x].lazy;
T[x*2].lazy+=T[x].lazy;
T[x*2+1].lazy+=T[x].lazy;
T[x].lazy=0;
}
}
long long ask(long long p,long long l,long long r){
if(l<=T[p].l&&r>=T[p].r){
//spread(p);
return T[p].dat;
}
spread(p);
long long mid=(T[p].l+T[p].r)/2;
long long val=1<<30;
if(l<=mid){
val=min(val,ask(p*2,l,r));
}
if(r>mid){
val=min(val,ask(p*2+1,l,r));
}
// for(long long i=1;i<=n;i++){
// cout<<T[i].dat<<" ";
// }
// cout<<endl;
return val;
}
//ask 1 l r
void change(long long p,long long l,long long r,long long d){
if(l<=T[p].l&&r>=T[p].r){
//T[p].dat-=d*(T[p].r-T[p].l+1);//更新该节点
//spread(p);
T[p].dat-=d;
T[p].lazy+=d;//打上延迟标记
return ;
}
spread(p);//传播延迟标记
long long mid=(T[p].l+T[p].r)/2;
if(l<=mid){
change(p*2,l,r,d);
}
if(r>mid){
change(p*2+1,l,r,d);
}
T[p].dat=min(T[p*2].dat,T[p*2+1].dat);
}
//change 1 x, v
int main(){
//freopen("classroom.in","r",stdin);
//freopen("classroom.out","w",stdout);
scanf("%d%d",&n,&m);
//cin>>n>>m;
//cout<<n<<m<<endl;
for(long long i=1;i<=n;i++){
scanf("%d",&R[i]);
//cin>>R[i];
}
for(long long i=1;i<=m;i++){
//cin>>d[i]>>s[i]>>t[i];
scanf("%d%d%d",&d[i],&s[i],&t[i]);
}
//感觉上,线段树会很好用
//维护一个区间最小值。如果在这个区间中最小值小于询问时的所需量,则返回订单编号并输出-1
build(1,1,n);
for(long long i=1;i<=m;i++){
//cout<<abc<<endl;
if(ask(1,s[i],t[i])<d[i]){
//printf("-1 \n");
//printf("%d",i);
//puts(-1);
printf("%d\n%d",-1,i);
//cout<<-1<<endl<<i;
//cout<<-1<<endl<<i;
return 0;
}
else{
change(1,s[i],t[i],d[i]);
}
}
printf("%d",0);
//cout<<0;
return 0;
}
代码是考场写的代码修改得来的,考试的时候没写区间修改,复杂度太高所以死的很惨。现在加上这个区间修改,至少在自己学校的OJ上是AC了的。
疯狂的馒头
Input
第一行四个正整数N,M,p,q
Output
一共输出N行,第i行表示第i个馒头的最终颜色(如果最终颜色是白色就输出0)。
Sample Input
4 3 2 4
Sample Output
2
2
3
0
这个题其实也可以用个线段树。十分有趣,要稍微做点变形。把染色的步骤倒过来。这样,只要这个节点已经有颜色了,那么他就不能再次被染色。
#include<iostream>
#include<cstdio>
using namespace std;
const int MAXN=1e7+10;
int n,m,p,q;
int Left;
int Right;
struct chui{
int l;
int r;
int color;
}Tree[MAXN*4];
void Build(int p,int l,int r,int colo,int L,int R){
if(Tree[p].color){
return;
}
//Tree[p].l=l;
//Tree[p].r=r;
if(l==r){
Tree[p].color=colo;
//ans[l]=Tree[p].color;
return;
}
int mid=(l+r)/2;
if(L<=mid){
Build(p*2,l,mid,colo,L,R);
}
if(R>mid){
Build(p*2+1,mid+1,r,colo,L,R);
}
Tree[p].color=Tree[p*2].color&&Tree[p*2+1].color;
}
void Print(int p,int l,int r){
if(l==r){
printf("%d\n",Tree[p].color);
//cout<<Tree[p].color<<endl;
return;
}
int mid=(l+r)/2;
Print(p*2,l,mid);
Print(p*2+1,mid+1,r);
}
int main(){
cin>>n>>m>>p>>q;
for(int i=m;i>=1;i--){
Left=(1ll*i*p+q)%n+1;
Right=(1ll*i*q+p)%n+1;
if(Left>Right){
swap(Left,Right);
}
Build(1,1,n,i,Left,Right);
}
Print(1,1,n);
}
小白逛公园 P4513
这题比较容易看出来是个区间和的最大值问题。一个区间的最大值会如何产生呢?
1.从区间左端点l开始,向右得到
2.从区间又端点r开始,向左得到
3.跨过中间点Mid,从左半边的右端点向左,从右半边的左端点向右得到。
那么,我们就需要维护四个数据:
从左开始得到的最大子段和值
从右开始得到的最大子段和值
整个区间的最大子段和值
整个区间段和
这么一寻思,满足向上传递的性质,自然就会想到线段树来维护这个操作。那么如何来维护这几个个数据呢?
void Update(int p)
{
int left=p<<1;
int right=p<<1|1;
Tree[p].sum=Tree[left].sum+Tree[right].sum;
Tree[p].lmax=max(Tree[left].lmax,Tree[left].sum+Tree[right].lmax);
Tree[p].rmax=max(Tree[right].rmax,Tree[right].sum+Tree[left].rmax);
Tree[p].Max=max(max(Tree[left].Max,Tree[right].Max),Tree[left].rmax+Tree[right].lmax);
}
因为用不着什么下放,故不需要搞Lazy标记
那么完整AC代码:
#include<iostream>
#include<cstdio>
using namespace std;
const int MAXN=500010;
int n,m;
int Read_in[MAXN];
struct node
{
int sum;
int lmax;
int rmax;
int Max;
int l;
int r;
} Tree[MAXN*4];
void Update(int p)
{
int left=p<<1;
int right=p<<1|1;
Tree[p].sum=Tree[left].sum+Tree[right].sum;
Tree[p].lmax=max(Tree[left].lmax,Tree[left].sum+Tree[right].lmax);
Tree[p].rmax=max(Tree[right].rmax,Tree[right].sum+Tree[left].rmax);
Tree[p].Max=max(max(Tree[left].Max,Tree[right].Max),Tree[left].rmax+Tree[right].lmax);
}
void Build(int P,int l,int r)
{
Tree[P].l=l;
Tree[P].r=r;
if(l==r)
{
Tree[P].lmax=Tree[P].rmax=Tree[P].Max=Tree[P].sum=Read_in[l];
return ;
}
int mid=(l+r)>>1;
Build(P<<1,l,mid);
Build(P<<1|1,mid+1,r);
Update(P);
}
void change(int p,int pos,int num)
{
if(Tree[p].l==Tree[p].r)
{
Tree[p].sum=Tree[p].lmax=Tree[p].rmax=Tree[p].Max=num;
return ;
}
int mid=(Tree[p].l+Tree[p].r)>>1;
if(mid>=pos)
{
change(p<<1,pos,num);
}
else
{
change(p<<1|1,pos,num);
}
Update(p);
}
node Ask(int p,int l,int r)
{
if(l<=Tree[p].l&&Tree[p].r<=r)
{
return Tree[p];
}
int mid=(Tree[p].l+Tree[p].r)>>1;
if(r<=mid)
{
return Ask(p<<1,l,r);
}
else if(l>mid)
{
return Ask(p<<1|1,l,r);
}
else
{
node x=Ask(p<<1,l,r);
node y=Ask(p<<1|1,l,r);
node re;
re.sum=x.sum+y.sum;
re.lmax=max(x.sum+y.lmax,x.lmax);
re.rmax=max(y.sum+x.rmax,y.rmax);
re.Max=max(max(x.Max,y.Max),x.rmax+y.lmax);
return re;
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1; i<=n; i++)
{
scanf("%d",&Read_in[i]);
}
Build(1,1,n);
int Index,a,b;
node ans;
for(int i=1; i<=m; i++)
{
scanf("%d",&Index);
if(Index==1)
{
scanf("%d%d",&a,&b);
if(a>b)
{
swap(a,b);
}
ans=Ask(1,a,b);
printf("%d\n",ans.Max);
}
else
{
scanf("%d%d",&a,&b);
change(1,a,b);
}
}
}