目录
背景
明天JSOI2019省选。
将线段树理了一遍。本文主要包括:本人习惯的线段树写法,主流的线段树写法,类主席树的线段树写法,k小值版经典主席树,区间修改主席树。其中除了第四种,其他都支持区间加与区间求和。
在文末提到:zkw线段树,动态开点线段树,扫描线,二维线段树,权值线段树(但这些暂时没有代码)。
线段树
本人习惯的线段树
主要特征是每一个节点记录好左右端点,而不是等遍历时传过来。
利:写起来方便,而且递归调用时传入参数较少。
弊:时空常数皆较大
struct Segment_tree_type1{//my classical mode, record the left and right point, but may be slower
struct node{
int l,r,he,p;
};
node t[MAXN*4+1];
void build(int cn,int cl,int cr)
{
t[cn].l = cl;
t[cn].r = cr;
t[cn].he = t[cn].p = 0;
if(cl == cr)return;
build(cn*2,cl,(cl+cr)/2);
build(cn*2+1,(cl+cr)/2+1,cr);
}
void jia(int cn,int cl,int cr,int cm)
{
int l = t[cn].l,r = t[cn].r;
if(l>cr || r<cl)return;
if(cl<=l && r<=cr){
t[cn].he += (r-l+1)*cm;
t[cn].p += cm;
return;
}
tui(cn);
jia(cn*2,cl,cr,cm);
jia(cn*2+1,cl,cr,cm);
update(cn);
}
void tui(int cn)
{
t[cn*2].p += t[cn].p;
t[cn*2].he += t[cn].p * (t[cn*2].r-t[cn*2].l+1);
t[cn*2+1].p += t[cn].p;
t[cn*2+1].he += t[cn].p * (t[cn*2+1].r-t[cn*2+1].l+1);
t[cn].p = 0;
}
void update(int cn)
{
t[cn].he = t[cn*2].he + t[cn*2+1].he;
}
int qiu(int cn,int cl,int cr)
{
int l = t[cn].l,r = t[cn].r;
if(l>cr || r<cl)return 0;
if(cl<=l && r<=cr)return t[cn].he;
tui(cn);
return qiu(cn*2,cl,cr) + qiu(cn*2+1,cl,cr);
}
};
主流的线段树
就是可以经常在网上看见的那种,在节点中不记录左右端点。
利:快一些,一般人都会理解,网上的细节比较丰富
弊:(我觉得)写起来烦一点,且容易写错
struct Segment_tree_type2{//don't record the left and right point
struct node{
int he,p;
};
node t[MAXN*4+1];
void build(int cn,int cl,int cr)
{
t[cn].he = t[cn].p = 0;
if(cl == cr)return;
build(cn*2,cl,(cl+cr)/2);
build(cn*2+1,(cl+cr)/2+1,cr);
}
void jia(int cn,int cl,int cr,int cm,int l,int r)
{
if(cl<=l && r<=cr){
t[cn].he += cm * (t[cn].r-t[cn].r+1);
t[cn].p += cm;
return;
}
tui(cn);
if(cl<=(l+r)/2)jia(cn*2,cl,cr,cm,l,(l+r)/2);
if(cr>(l+r)/2)jia(cn*2+1,cl,cr,cm,(l+r)/2+1,r);
update(cn);
}
int qiu(int cn,int cl,int cr,int l,int r)
{
if(cl<=l && r<=cr)return t[cn].he;
tui(cn);
int guo = 0;
if(cl<=(l+r)/2)guo += qiu(cn*2,cl,cr,l,(l+r)/2);
if(cr>(l+r)/2)guo += qiu(cn*2+1,cl,cr,(l+r)/2+1,r);
return guo;
}
void tui(int cn,int l,int r)
{
int zh = (l+r)/2;
t[cn*2].p += t[cn].p;
t[cn*2].he += t[cn].p * (zh-l+1);
t[cn*2+1].p += t[cn].p;
t[cn*2+1].he += t[cn].p * (r-zh);
t[cn].p = 0;
}
void update(int cn)
{
t[cn].he = t[cn*2].he + t[cn*2+1].he;
}
};
动态开点线段树
儿子节点的编号不再是父亲节点乘以二或乘二加一,而是在使用时分配,把左右儿子的编号记录在父亲上。
利:在有些地方快一点,好写一点,空间更优。有利于主席树的入门理解
弊:不可zkw化
struct Segment_tree_type3{//just like president tree
struct node{
int ls,rs,he,p;
};
node t[MAXN*4+1];
int tlen;
int build()
{
qing(tlen = 1);
}
int qing(int cn)
{
t[cn].ls = t[cn].rs = t[cn].he = t[cn].p = 0;
return cn;
}
void jia(int cn,int cl,int cr,int cm,int l,int r)
{
if(cl<=l && r<=cr){
t[cn].p += cm;
t[cn].he += cm * (r-l+1);
return;
}
tui(cn,l,r);
if(cl <= (l+r)/2)jia(t[cn].ls,cl,cr,cm,l,(l+r)/2);
if(cr > (l+r)/2)jia(t[cn].rs,cl,cr,cm,(l+r)/2+1,r);
update(cn);
}
int qiu(int cn,int cl,int cr,int l,int r)
{
if(cl<=l && r<=cr)return t[cn].he;
tui(cn,l,r);
int guo = 0;
if(cl <= (l+r)/2)guo += qiu(t[cn].ls,cl,cr,l,(l+r)/2);
if(cr > (l+r)/2)guo += qiu(t[cn].rs,cl,cr,(l+r)/2+1,r);
return guo;
}
void tui_e(int cn,int &cm,int len)
{
if(!cm)cm = qing(++tlen);
t[cm].p += t[cn].p;
t[cm].he += len * t[cn].p;
}
void tui(int cn,int l,int r)
{
int zh = (l+r)/2;
tui_e(cn,t[cn].ls,zh-l+1);
tui_e(cn,t[cn].rs,r-zh);
t[cn].p = 0;
}
void update(int cn)
{
t[cn].he = t[t[cn].ls].he + t[t[cn].rs].he;
}
};
可持久化线段树
k小值版经典主席树
功能是单点修改,区间查询(在k小值中这个功能是用来线段树上二分的)。
基础为类主席树的线段树,只不过在每一回更改时都在原来的基础上新建一个点。新点把原来的点存的值都继承过去后,再进行修改。
利:适合于主席树入门理解,易写。
弊(主席树共有):常数太大,空间较大(多一个)。
弊(这棵树独有):若需要打上懒惰标记,则需要进行更改。
struct Zxs_type1{//the classical president tree, used to change one point
struct node{
int ls,rs,he;
};
node t[MAXN*40+1];
int tlen;
void build()
{
qing(tlen = 1);
}
int qing(int cn)
{
t[cn].ls = t[cn].rs = t[cn].he = 0;
return cn;
}
void jia(int oro,int &ro,int cl,int cm,int l,int r)
{
t[ro = ++tlen] = t[oro];
if(l == r){
t[cn].he = cm;
return;
}
if(cl <= (l+r)/2)jia(t[cn].ls,t[cn].ls,cl,cm,l,(l+r)/2);
if(cl > (l+r)/2)jia(t[cn].rs,t[cn].rs,cl,cm,(l+r)/2+1,r);
update(cn);
}
int qiu(int cn,int cl,int cr,int l,int r)
{
if(cl<=l && r<=cr)return t[cn].he;
int guo = 0;
if(cl <= (l+r)/2)guo += qiu(t[cn].ls,cl,cr,l,(l+r)/2);
if(cr > (l+r)/2)guo += qiu(t[cn].rs,cl,cr,r,(l+r)/2+1);
return guo;
}
void update(int cn)
{
t[cn].he = t[t[cn].ls].he + t[t[cn].rs].he;
}
};
区间修改主席树
顾名思义,这棵树比前面那棵高级一点;你可以区间修改,而且与通常版本改动不大,主要就一个新建点的功能。
利:功能强大
弊:可能常数太大
struct Zxs_type2{//basic president tree, can change a range
struct node{
int ls,rs,he,p;
};
node t[MAXN*40+1];
int tlen;
void build()
{
qing(tlen = 1);
}
int qing(int cn)
{
t[cn].ls = t[cn].rs = t[cn].he = t[cn].p = 0;
return cn;
}
void jia(int cn,int cl,int cr,int cm,int l,int r)
{
if(cl <= l && r<=cr){
t[cn].p += cm;
t[cn].he += cm * (r-l+1);
return;
}
tuo(cn);
tui(cn,l,r);
if(cl <= (l+r)/2)jia(t[cn].ls,cl,cr,cm,l,(l+r)/2);
if(cr > (l+r)/2)jia(t[cn].rs,cl,cr,cm,(l+r)/2+1,r);
update(cn);
}
void qiu(int cn,int cl,int cr,int l,int r)
{
if(cl<=l && r<=cr)return t[cn].he;
tui(cn,l,r);
int guo = 0;
if(cl <= (l+r)/2)guo += qiu(t[cn].ls,cl,cr,l,(l+r)/2);
if(cr > (l+r)/2)guo += qiu(t[cn].rs,cl,cr,(l+r)/2+1,r);
return guo;
}
inline void tui(int cn,int l,int r)
{
int zh = (l+r)/2;
tui_e(cn,t[cn].ls,zh-l+1);
tui_e(cn,t[cn].rs,r-zh);
t[cn].p = 0;
}
inline void tui_e(int cn,int cm,int len)
{
t[cm].p += t[cn].p;
t[cm].he += t[cn].p * len;
}
void update(int cn)
{
t[cn].he = t[t[cn].ls].he + t[t[cn].rs].he;
}
inline void tuo_e(int cn,int &cm)
{
t[cm = qing(++tlen)] = t[cn];
}
inline void tuo(int cn)
{
tuo_e(t[cn].ls,t[cn].ls);
tuo_e(t[cn].rs,t[cn].rs);
}
};
其他好东西
zkw线段树,常数极小,但不是那么使用广泛,容易理解。线段树的修改查询功能都有,也可可持久化。本质是利用左儿子为父亲乘二,右儿子为父亲乘二加一,快速算出目标的位置,再从下往上算。
扫描线:应该算是一种应用,可以解决一些二维上看似要用二维线段树或者主席树做的题目。基础功能是算平面上若干矩形的并的面积。把所有矩形抽象成左右两条线,从左往右看,对于当前的x坐标,把左端线所覆盖的纵坐标区间加上1,计算有多少个不为零的纵坐标,再减去右端线覆盖的纵坐标区间(各减一)。
二维线段树:它死了(基本不会有人用,不会有人出,常数就比log大,基本可以被树套树,主席树,扫描线解决)。
权值线段树:是对于从零到很大的数(比如1e9)开一棵树,先不全都分配,在用到时再临时分配(当然有些时候先分配也没问题)。(很像动态开点线段树)(k小值的线段树就是一颗权值线段树)(所以可以很方便的拓展到可持久化)