线段树
最基础线段树,就是支持区间加乘,查询区间最值
总的来说,就是可以在区间上进行操作
(给出的可能不是完整代码,不过都是精华)
代码中有几个细节需要注意:
默认先乘后加,在计算值的时候统一表达式:
t[bh].sum=( t[bh].sum*t[bh].mul%p + (t[bh].y-t[bh].x+1)*t[bh].ad%p )%p;
永远记住:先维护值,再下传标记
标记下传: son.mul=son.mul∗fa.mul,son.ad=son.ad∗fa.mul+fa.ad s o n . m u l = s o n . m u l ∗ f a . m u l , s o n . a d = s o n . a d ∗ f a . m u l + f a . a d
build的时候注意初始化
虽然我们在push的时候遇到叶节点就返回,不过也要把叶节点上的标记情况,防止冲突:
t[bh].ad=0; t[bh].mul=1; return;
虽然理论上线段树的空间是2*n,而且daloa们表示push的时候特判一下(叶节点不push),空间是可以缩小到2*n的,但是保险起见还是4倍数组(不要投机取巧)
const int N=100010;
struct node{
int x,y;
ll sum,ad,mul;
};
node t[N<<2];
ll a[N],p=100;
void update(int bh) {
t[bh].sum=(t[bh<<1].sum+t[bh<<1|1].sum)%p;
}
void push(int bh) {
if (t[bh].x==t[bh].y) {
t[bh].ad=0; t[bh].mul=1;
return;
}
int lc=bh<<1,rc=bh<<1|1;
if (t[bh].ad!=0||t[bh].mul!=1) { //默认乘操作在加操作之前
//永远记住:先维护值,再下传标记
t[lc].sum=( (t[lc].sum*t[bh].mul)%p + ((t[lc].y-t[lc].x+1)%p*t[bh].ad)%p )%p;
t[lc].mul=(t[lc].mul*t[bh].mul)%p;
t[lc].ad=( (t[lc].ad*t[bh].mul)%p+t[bh].ad )%p;
t[rc].sum=( (t[rc].sum*t[bh].mul)%p + ((t[rc].y-t[rc].x+1)%p*t[bh].ad)%p )%p;
t[rc].mul=(t[rc].mul*t[bh].mul)%p;
t[rc].ad=( (t[rc].ad*t[bh].mul)%p+t[bh].ad )%p;
t[bh].ad=0; t[bh].mul=1;
}
}
void build(int bh,int l,int r) {
t[bh].x=l; t[bh].y=r;
t[bh].sum=0;
t[bh].mul=1; t[bh].ad=0;
if (l==r) {
t[bh].sum=a[l]%p;
return;
}
int mid=(l+r)>>1;
build(bh<<1,l,mid);
build(bh<<1|1,mid+1,r);
update(bh);
}
void add(int bh,int l,int r,int L,int R,ll z) { //[L,R]
push(bh);
if (l>=L&&r<=R) {
t[bh].ad+=z; t[bh].ad%=p;
t[bh].sum=( t[bh].sum*t[bh].mul%p + (t[bh].y-t[bh].x+1)*t[bh].ad%p )%p;
return;
}
int mid=(l+r)>>1;
if (L<=mid) add(bh<<1,l,mid,L,R,z);
if (R>mid) add(bh<<1|1,mid+1,r,L,R,z);
update(bh);
}
void multi(int bh,int l,int r,int L,int R,ll z) {
push(bh);
if (l>=L&&r<=R) {
t[bh].mul*=z; t[bh].mul%=p;
t[bh].sum=( t[bh].sum*t[bh].mul%p + (t[bh].y-t[bh].x+1)*t[bh].ad%p )%p;
return;
}
int mid=(l+r)>>1;
if (L<=mid) multi(bh<<1,l,mid,L,R,z);
if (R>mid) multi(bh<<1|1,mid+1,r,L,R,z);
update(bh);
}
ll ask(int bh,int l,int r,int L,int R) {
push(bh);
if (l>=L&&r<=R) return t[bh].sum%p;
int mid=(l+r)>>1;
ll ans=0;
if (L<=mid) ans=(ans+ask(bh<<1,l,mid,L,R))%p;
if (R>mid) ans=(ans+ask(bh<<1|1,mid+1,r,L,R))%p;
return ans%p;
}
当然,线段树也可以维护简单的图上的信息(并不是说信息简单,只是图简单而已)
经典例题:线段树维护MST或图的连通性
进阶一点,我们可以用线段树维护二维平面上的线段,从而回答某个点上的最高线段或最低线段
超哥线段树
这种线段树最主要的思想就是:标记永久化
所以在查询的时候,只要一个区间有线段标记,我们就要维护一下
重点还是insert
struct node{
double a,b;
bool flag;
};
void insert(int bh,int l,int r,int L,int R,double a,double b) {
if (l>r) return;
int mid=(l+r)>>1;
if (l>=L&&r<=R) {
if (!t[bh].flag) {
t[bh].a=a; t[bh].b=b; t[bh].flag=1;
return;
}
double z1=(double)l*t[bh].a+t[bh].b; //原线段
double z2=(double)r*t[bh].a+t[bh].b;
double z3=(double)l*a+b; //新线段
double z4=(double)r*a+b;
//维护最高点
if (z1>=z3&&z2>=z4) return;
if (z1<=z3&&z2<=z4) {
t[bh].a=a; t[bh].b=b;
return;
}
double x=(b-t[bh].b)/(t[bh].a-a);
if (x<=mid) { //交点在左区间
if (z3>z1) insert(bh<<1,l,mid,L,R,a,b);
//新线段在左区间更优
else {
insert(bh<<1,l,mid,L,R,t[bh].a,t[bh].a);
//原线段在左区间更优
t[bh].a=a; t[bh].b=b;
//标记永久化
}
}
else { //交点在右区间
if (z4>z2) insert(bh<<1|1,mid+1,r,L,R,a,b);
//新线段在右区间更优
else {
insert(bh<<1|1,mid+1,r,L,R,t[bh].a,t[bh].b);
//原线段在右区间更优
t[bh].a=a; t[bh].b=b;
//标记永久化
}
}
}
if (L<=mid) insert(bh<<1,l,mid,L,R,a,b);
if (R>mid) insert(bh<<1|1,mid+1,r,L,R,a,b);
}
还有线段树和其他算法的经典结合:
经典例题:dp+线段树
经典例题:线段树+并查集
在图论中,可能会有区间连边的情况,这个时候我们就可以用线段树优化建图
一般还是建立两棵线段树:入数,出树
两棵树的编号范围分别为:
[1,4n],[4n+1,8n]
[
1
,
4
n
]
,
[
4
n
+
1
,
8
n
]
注意一下build即可
void build(int bh,int l,int r) {
if (l==r) {
add(bh+4*n,bh,0); //入树->出树 表示可以再次出发
pos[l]=bh; //l在线段树中对应的编号
return;
}
int mid=(l+r)>>1;
build(bh<<1,l,mid);
build(bh<<1|1,mid+1,r);
int lc=bh<<1,rc=bh<<|1;
add(lc,bh,0); //出树:子连父
add(rc,bh,0);
add(bh+4*n,lc+4*n,0); //入树:父连子
add(bh+4*n,rc+4*n,0);
}
线段树模拟费用流,简单来说就是线段树支持查询区间最大子序列和区间取反
比较单一的题型:区间内k次连续选择序列,使得收益最大
以上说的都是一维线段树的情况,举一反三,我们也可以把线段树拓展到二维平面上,不过目前以我的知识面来看,二维线段树只能解决区间最值问题
(区间和什么的用二维树状数组就好了)
然而二维线段树有两种写法(二者差别还是蛮大的):
单点修改+区间最值查询
这种线段树的特点就是:先确定x再确定y
update(x)需要通过y的查找完成
int mx[N<<2][N<<2],mn[N<<2][N<<2];
int maxx,minn,a[N][N];
void update(int x,int y) {
mn[x][y]=min(mn[x][y<<1],mn[x][y<<1|1]);
mx[x][y]=max(mn[x][y<<1],mn[x][y<<1|1]);
}
void build_y(int bh,int l,int r,int x,int xrt) {
if (l==r) {
if (x!=-1) mn[xrt][bh]=mx[xrt][bh]=a[x][l];
else {
mx[xrt][bh]=max(mx[xrt<<1][bh],mx[xrt<<1|1][bh]);
mn[xrt][bh]=min(mn[xrt<<1][bh],mn[xrt<<1|1][bh]);
}
return;
}
int mid=(l+r)>>1;
build_y(bh<<1,l,mid,x,xrt);
build_y(bh<<1|1,mid+1,r,x,xrt);
update(xrt,bh);
}
void build_x(int bh,int l,int r) {
if (l==r) {
build_y(1,1,n,l,bh);
return;
}
int mid=(l+r)>>1;
build_x(bh<<1,l,mid);
build_x(bh<<1,mid+1,r);
build_y(1,1,n,-1,bh); //update(bh)
}
void change_y(int bh,int l,int r,int x,int z,int xrt) {
if (l==r) {
if (z!=-1) mn[xrt][bh]=mx[xrt][bh]=z;
else {
mn[xrt][bh]=min(mn[xrt<<1][bh],mn[xrt<<1|1][bh]);
mx[xrt][bh]=max(mx[xrt<<1][bh],mx[xrt<<1|1][bh]);
}
return;
}
int mid=(l+r)>>1;
if (x<=mid) change_y(bh<<1,l,mid,x,z,xrt);
else change_y(bh<<1|1,mid+1,r,x,z,xrt);
update(xrt,bh);
}
void change_x(int bh,int l,int r,int x,int y,int z) {
if (l==r) {
change_y(1,1,n,y,z,bh);
return;
}
int mid=(l+r)>>1;
if (x<=mid) change_x(bh<<1,l,mid,x,y,z);
else change_x(bh<<1,mid+1,r,x,y,z);
change_y(1,1,n,y,-1,bh);
}
void ask_y(int bh,int l,int r,int L,int R,int xrt) {
if (l>=L&&r<=R) {
maxx=max(maxx,mx[xrt][bh]);
minn=min(minn,mn[xrt][bh]);
return;
}
int mid=(l+r)>>1;
if (L<=mid) ask_y(bh<<1,l,mid,L,R,xrt);
if (R>mid) ask_y(bh<<1|1,mid+1,r,L,R,xrt);
}
void ask_x(int bh,int l,int r,int xl,int xr,int yl,int yr) {
if (l>=xl&&r<=xr) {
ask_y(1,1,n,yl,yr,bh);
return;
}
int mid=(l+r)>>1;
if (xl<=mid) ask_x(bh<<1,l,mid,xl,xr,yl,yr);
if (xr>mid) ask_x(bh<<1|1,mid+1,r,xl,xr,yl,yr);
}
区间修改+区间最值查询
二维树状数组本身就不好push和update,所以我们要标记永久化
记录两个标记,A精准覆盖,B标记永久化
在查询的时候,B是准确的
int xl,xr,yl,yr,n,m;
struct node_y{
int a[N<<2],b[N<<2];
void change(int bh,int l,int r,int L,int R,int z) {
b[bh]=max(b[bh],z);
if (l>=L&&r<=R) {
a[bh]=max(a[bh],z);
return;
}
int mid=(l+r)>>1;
if (L<=mid) change(bh<<1,l,mid,L,R,z);
if (R>mid) change(bh<<1|1,mid+1,r,L,R,z);
}
int ask(int bh,int l,int r,int L,int R) {
if (l>=L&&r<=R) return b[bh];
int mid=(l+r)>>1;
int ans=a[bh];
if (L<=mid) ans=max(ans,ask(bh<<1,l,mid,L,R));
if (R>mid) ans=max(ans,ask(bh<<1|1,mid+1,r,L,R));
return ans;
}
};
struct node_x{
node_y a[N<<2],b[N<<2];
void change(int bh,int l,int r,int L,int R,int z) {
b[bh].change(1,1,m,yl,yr,z);
if (l>=L&&r<=R) {
a[bh].change(1,1,m,yl,yr,z);
return;
}
int mid=(l+r)>>1;
if (L<=mid) change(bh<<1,l,mid,L,R,z);
if (R>mid) change(bh<<1|1,mid+1,r,L,R,z);
}
int ask(int bh,int l,int r,int L,int R) {
if (l>=L&&r<=R) return b[bh].ask(1,1,m,yl,yr);
int mid=(l+r)>>1;
int ans=a[bh].ask(1,1,m,yl,yr);
if (L<=mid) ans=max(ans,ask(bh<<1,l,mid,L,R));
if (R>mid) ans=max(ans,ask(bh<<1|1,mid+1,r,L,R));
return ans;
}
}T;
树链剖分
树链剖分的精髓就在于处理树上路径(路径和,路径最大值)
本身可以说就是一个模板,但是结合其他算法的变化就比较多了
经典例题:(超哥)线段树+树链剖分
经典例题:维护带修改的带权重心到其余点的带权距离和
const int N=10010;
struct node{
int y,nxt;
};
node way[N<<1];
struct Tree{
ll sum,ad;
};
Tree t[N];
int n,m,st[N],tot=0,clo=0;
int pre[N],size[N],son[N],top[N],num[N],shu[N],deep[N],out[N];
ll p,a[N];
void add(int u,int w) {
tot++;way[tot].y=w;way[tot].nxt=st[u];st[u]=tot;
tot++;way[tot].y=u;way[tot].nxt=st[w];st[w]=tot;
}
void dfs_1(int now,int fa,int dep) {
pre[now]=fa;
size[now]=1;
deep[now]=dep;
int maxx=0;
for (int i=st[now];i;i=way[i].nxt)
if (way[i].y!=fa) {
dfs_1(way[i].y,now,dep+1);
size[now]+=size[way[i].y];
if (size[way[i].y]>maxx)
maxx=size[way[i].y],son[now]=way[i].y;
}
}
void dfs_2(int now,int fa) {
if (son[fa]==now) top[now]=top[fa];
else top[now]=now;
num[now]=++clo; shu[clo]=now;
if (son[now]) {
dfs_2(son[now],now);
for (int i=st[now];i;i=way[i].nxt)
if (way[i].y!=fa&&way[i].y!=son[now])
dfs_2(way[i].y,now);
}
out[now]=clo;
}
void push(int bh,int l,int r) {
if (l==r) {
t[bh].ad=0; //清零
return;
}
if (!t[bh].ad) return;
int lc=bh<<1,rc=bh<<1|1,mid=(l+r)>>1;
t[lc].sum+=t[bh].ad*(mid-l+1); t[lc].sum%=p;
t[lc].ad+=t[bh].ad; t[lc].ad%=p;
t[rc].sum+=t[bh].ad*(r-mid); t[rc].sum%=p;
t[rc].ad+=t[bh].ad; t[rc].ad%=p;
t[bh].ad=0;
}
void update(int bh) {
t[bh].sum=(t[bh<<1].sum+t[bh<<1|1].sum)%p;
}
void build(int bh,int l,int r) {
t[bh].ad=t[bh].sum=0;
if (l==r) {
t[bh].sum=a[shu[l]]%p;
return;
}
int mid=(l+r)>>1;
build(bh<<1,l,mid);
build(bh<<1|1,mid+1,r);
update(bh);
}
void change(int bh,int l,int r,int L,int R,ll z) {
push(bh,l,r);
if (l>=L&&r<=R) {
t[bh].sum+=(r-l+1)*z; t[bh].sum%=p;
t[bh].ad+=z; t[bh].ad%=p;
return;
}
int mid=(l+r)>>1;
if (L<=mid) change(bh<<1,l,mid,L,R,z);
if (R>mid) change(bh<<1|1,mid+1,r,L,R,z);
update(bh);
}
ll ask(int bh,int l,int r,int L,int R) {
push(bh,l,r);
if (l>=L&&r<=R) return t[bh].sum%p;
int mid=(l+r)>>1;
ll ans=0;
if (L<=mid) ans=(ans+ask(bh<<1,l,mid,L,R))%p;
if (R>mid) ans=(ans+ask(bh<<1|1,mid+1,r,L,R))%p;
return ans%p;
}
void changesum(int x,int y,ll z) { //传入原树上的结点编号
int f1=top[x];
int f2=top[y];
while (f1!=f2) {
if (deep[f1]<deep[f2]) swap(x,y),swap(f1,f2);
change(1,1,n,num[f1],num[x],z);
x=pre[f1];
f1=top[x];
}
if (num[x]>num[y]) swap(x,y);
change(1,1,n,num[x],num[y],z);
}
ll asksum(int x,int y) { //传入原树上的结点编号
int f1=top[x];
int f2=top[y];
ll ans=0;
while (f1!=f2) {
if (deep[f1]<deep[f2]) swap(x,y),swap(f1,f2);
ans=(ans+ask(1,1,n,num[f1],num[x]))%p;
x=pre[f1];
f1=top[x];
}
if (num[x]>num[y]) swap(x,y);
ans=(ans+ask(1,1,n,num[x],num[y]))%p;
return ans;
}