线段树
早期作品,不喜轻喷。
本来是一个很基础的内容,但是由于自己学的时候一直在打摆没有落实,所以一直不会用,但是最近写题的时候有了一些感觉,想记下来怕忘记。
你是否适合读这篇文章?
如果你是刚入门的萌新,看看书或请出门右拐找其他大佬的详解;
如果你是一位神犇,请自行忽略这篇文章,作者太弱了,没必要在这里浪费时间;
但如果你学过线段树,但又有些操作不太清晰,或者你想要固定一下自己线段树的写法,那么请你继续往下读。
前一部分是讲最一般的线段树,后一部分(先咕着)会讲一些线段树的变体和扩展。
基本操作
单点修改区间查询,加延迟标记后资磁区间修改区间查询,这些默认你都已经知道了。这不是本文的重点。
把最裸的板子放上来。去掉了头文件、读入和主函数。
单点修改的:
#define R register
#define I inline
const int S=500003,M=2000003;
int a[S],b[M];
I void upd(int k,int v){b[k]+=v;}//update
I void psu(int k,int p,int q){b[k]=b[p]+b[q];}//pushup
void bld(int k,int l,int r){//build
if(l==r){b[k]=a[l]; return ;}
R int p=k<<1,q=p|1,m=l+r>>1;
bld(p,l,m),bld(q,m+1,r),psu(k,p,q);
}
void mdf(int k,int l,int r,int x,int v){//modify
if(l==r){upd(k,v); return ;}
R int p=k<<1,q=p|1,m=l+r>>1;
if(x<=m) mdf(p,l,m,x,v);
else mdf(q,m+1,r,x,v);
psu(k,p,q);
}
int qry(int k,int l,int r,int x,int y){//query
if(x<=l&&r<=y) return b[k];
R int p=k<<1,q=p|1,m=l+r>>1,o=0;
if(x<=m) o+=qry(p,l,m,x,y);
if(m<y) o+=qry(q,m+1,r,x,y);
return o;
}
你可能会觉得上面写的upd和psu函数非常的zz,但请你写别急着骂作者菜,这是为后面做的准备。
区间修改的:
#define R register
#define I inline
#define L long long
const int S=100003,M=400003;
L a[S],b[M],c[M];
I void upd(int k,int l,int r,L v){b[k]+=v*(r-l+1),c[k]+=v;}//update
I void psu(int k,int p,int q){b[k]=b[p]+b[q];}//pushup
I void psd(int k,int l,int r){//pushdown
R int p=k<<1,q=p|1,m=l+r>>1;
upd(p,l,m,c[k]),upd(q,m+1,r,c[k]),c[k]=0;
}
void bld(int k,int l,int r){//build
if(l==r){b[k]=a[l]; return ;}
R int p=k<<1,q=p|1,m=l+r>>1;
bld(p,l,m),bld(q,m+1,r),psu(k,p,q);
}
void mdf(int k,int l,int r,int x,int y,L v){//modify
if(x<=l&&r<=y){upd(k,l,r,v); return ;}
R int p=k<<1,q=p|1,m=l+r>>1; psd(k,l,r);
if(x<=m) mdf(p,l,m,x,y,v);
if(m<y) mdf(q,m+1,r,x,y,v);
psu(k,p,q);
}
L qry(int k,int l,int r,int x,int y){//query
if(x<=l&&r<=y) return b[k];
R int p=k<<1,q=p|1,m=l+r>>1; L o=0; psd(k,l,r);
if(x<=m) o+=qry(p,l,m,x,y);
if(m<y) o+=qry(q,m+1,r,x,y);
return o;
}
注意区间修改的upd函数中多传了两个参数l和r,这是由加法运算的性质决定的,而不是由区间修改决定的,如果要求的是区间max,就不需要这两个参数了。(有没有闻到一股强烈的铺垫气息?)
接下来我们所讲的都是区间修改的情况,单点修改的变化太少了。
多标记
如果我们在维护区间和的同时进行区间加和区间乘的操作,你需要把代码写成这样:
#define R register
#define I inline
#define L long long
const int S=100003,M=400003;
L a[S],b[M],c[M],d[M],mod;
I void upd(int k,int l,int r,L u,L v){//update
((d[k]*=u)+=v*(r-l+1))%=mod,(b[k]*=u)%=mod,((c[k]*=u)+=v)%=mod;
}
I void psu(int k,int p,int q){d[k]=(d[p]+d[q])%mod;}//pushup
I void psd(int k,int l,int r){//pushdown
R int p=k<<1,q=p|1,m=l+r>>1; L u=b[k],v=c[k];
upd(p,l,m,u,v),upd(q,m+1,r,u,v),b[k]=1,c[k]=0;
}
void bld(int k,int l,int r){b[k]=1;//build
if(l==r){d[k]=a[l]; return ;}
R int p=k<<1,q=p|1,m=l+r>>1;
bld(p,l,m),bld(q,m+1,r),psu(k,p,q);
}
void mdf1(int k,int l,int r,int x,int y,L u){//modify1
if(x<=l&&r<=y){upd(k,l,r,u,0); return ;}
R int p=k<<1,q=p|1,m=l+r>>1; psd(k,l,r);
if(x<=m) mdf1(p,l,m,x,y,u);
if(m<y) mdf1(q,m+1,r,x,y,u);
psu(k,p,q);
}
void mdf2(int k,int l,int r,int x,int y,L v){//modify2
if(x<=l&&r<=y){upd(k,l,r,1,v); return ;}
R int p=k<<1,q=p|1,m=l+r>>1; psd(k,l,r);
if(x<=m) mdf2(p,l,m,x,y,v);
if(m<y) mdf2(q,m+1,r,x,y,v);
psu(k,p,q);
}
L qry(int k,int l,int r,int x,int y){//query
if(x<=l&&r<=y) return d[k];
R int p=k<<1,q=p|1,m=l+r>>1; L o=0; psd(k,l,r);
if(x<=m) (o+=qry(p,l,m,x,y))%=mod;
if(m<y) (o+=qry(q,m+1,r,x,y))%=mod;
return o;
}
注意对比上面的两份代码你可以发现,psu、psd、bld、mdf(只是多了一个而已,长得都一样)、qry函数基本都没变,只有一个upd函数在变。
所以现在需要解决一个问题,upd函数应该怎么变?
我们观察发现,upd函数中有两个操作,第一个是用标记更新我们维护的值,第二个是用标记更新标记,有的同学会对于第一个操作有一些疑问。应该先用哪个标记来更新值呢?
这里给出结论,对于两个标记的情况,如果一个标记能对另一个标记产生影响,就把这个标记先更新。例如对于加和乘的标记,先用乘标记更新,再用加标记更新;对于推平标记和加标记,应先用推平标记更新。
多询问
事实上你会发现,处理多询问只也需要把upd和psu函数修改一下,再多加一个qry函数就好了。
我懒得写了,这很简单呀
读到这里,不知你有没有这样的感觉:线段树支持不同的操作其实只要把upd函数和psu函数改动一下就行了?
这正是我所想的,换句话说:
是否存在一个一般式可以表达线段树呢?
以两种操作、两种查询为例,我尝试写了一下:(假设op是区间可加的操作,即满足线段树的使用条件)
#define R register
#define I inline
const int S=100003,M=400003;
int a[S],b[M],c[M],d[M],e[M];
I int op1(int x,int y){/*你维护的信息1*/;}
I int op2(int x,int y){/*你维护的信息2*/;}
I void upd(int k,int u,int v/*传的参数可能随维护信息的性质有变化*/){//update
/*现在这是这段代码中唯一一个要脑子的地方了*/
}
I void psu(int k,int p,int q){d[k]=op1(d[p],d[q]),e[k]=op2(e[p],e[q]);}//pushup
I void psd(int k/*传的参数可能随维护信息的性质有变化,跟着upd变*/){//pushdown
R int p=k<<1,q=p|1,u=b[k],v=c[k];
upd(p,u,v),upd(q,u,v),b[k]=c[k]=0;//不一定是0,保证是初始状态
}
void bld(int k,int l,int r){//build
if(l==r){d[k]=e[k]=a[l]; return ;}
R int p=k<<1,q=p|1,m=l+r>>1;
bld(p,l,m),bld(q,m+1,r),psu(k,p,q);
}
void mdf1(int k,int l,int r,int x,int y,int u){//modify1
if(x<=l&&r<=y){upd(k,u,0/*不改变的标记传初始值,不一定是0*/); return ;}
R int p=k<<1,q=p|1,m=l+r>>1; psd(k);
if(x<=m) mdf1(p,l,m,x,y,u);
if(m<y) mdf1(q,m+1,r,x,y,u);
psu(k,p,q);
}
void mdf2(int k,int l,int r,int x,int y,int v){//modify2
if(x<=l&&r<=y){upd(k,0/*传初始值*/,v); return ;}
R int p=k<<1,q=p|1,m=l+r>>1; psd(k);
if(x<=m) mdf2(p,l,m,x,y,v);
if(m<y) mdf2(q,m+1,r,x,y,v);
psu(k,p,q);
}
int qry1(int k,int l,int r,int x,int y){//query1
if(x<=l&&r<=y) return d[k];
R int p=k<<1,q=p|1,m=l+r>>1,o=0/*赋初始值,不一定是0*/; psd(k);
if(x<=m) o=op1(o,qry1(p,l,m,x,y));
if(m<y) o=op1(o,qry1(q,m+1,r,x,y));
return o;
}
int qry2(int k,int l,int r,int x,int y){//query2
if(x<=l&&r<=y) return e[k];
R int p=k<<1,q=p|1,m=l+r>>1,o=0/*赋初始值0*/; psd(k);
if(x<=m) o=op2(o,qry2(p,l,m,x,y));
if(m<y) o=op2(o,qry2(q,m+1,r,x,y));
return o;
}
可以看到,我们找出了普通的线段树一般的写法,其实还可以更简洁(把多个mdf、qry操作合为一个处理)只是我懒得写。很难说这样做有什么具体的作用,但我觉得这样的形式就非常优美了,或者说至少以后打线段树可以非常流畅的打板子了,对于一些应用的情况,思路也会更加清晰(只要改一个upd函数就好了!)。
推荐题目:
很久以后加上一点内容。
这里稍微讲一下动态开点线段树,主席树和其它的变体以后另写博客算了。
有一道很板子的题CF1111C Creative Snap。
一看这题就想到线段树,结果值域\(2 ^ {30}\),普通线段树显然爆空间,发现修改次数\(\le 1e5\),可以动态开点。具体来说对于每一次修改,如果在线段树上要修改的当前结点还不存在,就新开一个结点记录这个结点的信息,注意左右儿子需要单独记了,不能直接\(k << 1\)和\(k << 1 | 1\),其他操作就都一样了。这个题需要考虑空结点的贡献,只需要把\(0\)号节点的\(v\)值改成\(A\)就行了。
#include <cstdio>
#include <cctype>
#define R register
#define I inline
#define B 1000000
#define L long long
using namespace std;
const int N = 100003;
char buf[B], *p1, *p2;
I char gc() { return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, B, stdin), p1==p2) ? EOF : *p1++; }
I int rd() {
R int f = 0;
R char c = gc();
while (c < 48 || c > 57) c = gc();
while (c > 47 && c < 58) f = f * 10 + (c ^ 48), c = gc();
return f;
}
int a, b, T;
struct segtree {
int c, p, q, d;
L v;
}e[N << 5];
I L min(L x, L y) { return x < y ? x : y; }
void insert(int &k, int l, int r, int x) {
if (!k)
k = ++T, e[k].d = r - l + 1;
++e[k].c;
if (l == r) {
e[k].v = 1ll * b * e[k].c;
return ;
}
R int m = l + r >> 1;
if (x <= m)
insert(e[k].p, l, m, x);
else
insert(e[k].q, m + 1, r, x);
e[k].v = min(e[e[k].p].v + e[e[k].q].v, 1ll * b * (e[e[k].p].c + e[e[k].q].c) * e[k].d);
}
int main() {
R int n, m, rt = 0, i, x;
n = 1 << rd(), m = rd(), a = rd(), b = rd();
e[0].v = a;
for (i = 1; i <= m; ++i)
x = rd(), insert(rt, 1, n, x);
printf("%I64d", e[rt].v);
return 0;
}
可以结合这道题好好理解动态开点线段树。