线段树
定义
用一个满二叉树(叶子节点可以为空)来维护一个连续数组,整个树的所有叶子节点从左到右表示整个数组,每个非叶子节点表示其所有叶子的集合所描述的一个连续子数组的某一特性(最小值,最大值等)。
性质
可以在logn的复杂度内实现对任意连续字段的给定特性的查询(最小值等),可以在logn的时间内完成对任意连续字段的修改(都改变相同的量)
实现(以求任意连续区间的最小值为例)
- 由于是满二叉树,可以用堆来保存这棵树。
- 对于每次修改,如果每次都对整个树做修改,会有n的复杂度,所以可以采用延时修改,等到查询访问到时在修改,这样将复杂度降到logn,且不会提高查询的复杂度。(这是值一个连续区间的修改,若是只对一个点进行修改,无需如此)。
- 数据结构
struct SegTreeNode{
int val; //记录属性值(最小值)
int add; //记录修改值,
}segTree[10000];
- 建立线段树
void build(int root, int* array, int s, int e){
segTree[root].add = 0;
if(s==e){
segTree[root] = array[s].val; //叶子节点。
return;
}
int mid = (s+e)>>1;
build(root*2+1,array,s,mid); //左子树
build(root*2+2,array,mid+1,e); //右子树
segTree[root].val = min(segTree[root*2+1].val,segTree[root*2+2].val);
}
- 刷新修改(即将延时修改并向下推)
void pushDown(int root){
if(segTree[root].add == 0) return;
segTree[root*2+1].add += regTree[root].add;
segTree[root*2+1].val += regTree[root].add;
segTree[root*2+2].add += regTree[root].add;
segTree[root*2+2].val += regTree[root].add;
regTree[root].add = 0;
}
- 区间延时更新
void update(int root, int ns,int ne,int s,int e,int addval){
//[ns,ne] 是节点root表示的区间,[s,e] 是要更新的区间。
if(ns > e || ne < s) return
if(ns >= s && ne <= e){
segTree[root].add = addval;
segTree[root].val+=addval;
return;
}
pushDown(root);
int nm = (ns+ne)>>1;
update(root*2+1,ns,nm,s,e,addval);
update(root*2+1,nm+1,ne,s,e,addval);
segTree[root].val = min(segTree[root*2+1].val,segTree[root*2+2].val);
}
- 查询
int query(int root,int ns,int ne,int s,int e){
if(ns>e || ne<s) return INT_MAX;
if(ns >= s && ne <= e) return segTree[root].val;
pushDown(root);
int mid = (ns+ne)>>1;
return min(query(root*2+1,ns,mid,s,e),query(root*2+2,mid+1,ne,s,e));
}
树状数组
定义
树状数组是一颗树,它的所有叶子节点从左到右对应一个连续数组,用s1,s2,…,sn表示。非叶子节点树与非叶子节点树相同,表示为t1,t2,…,tn,其每个非叶子节点ti有ki个子节点,ki的大小与i的二进制的最后一个1的大小相同(如 10 对应2, 100 对应 4)。每个叶子节点si的父节点是ti,每个非叶子节点ti的父节点是t(i+ki)。每个非叶子节点中记录了它的所有叶子节点的属性(一般是和)。
性质
第一是每个非叶子节点ti的值对应了s(i-ki+1)到 si 这ki个连续数据的属性(一般是和),因此可以查询任意连续区间的属性,且复杂度为logn。第二是可以对单个数si做修改,修改的复杂度是logn。
实现
- 根据其定义,可以用两个数组来表示该树。
int val[1000],sum[1000];
- 计算ki,即找出i的最后一个1的位置对应的大小。
void lowbit(int i){
return x & -x;
// or return x - (x & x - 1);
}
- 建立
void build(int n){
for(int i=0;i<=n;i++)
for(int j=i;j<=x;j+=lowbit(j)) sum[j]+=val[i];
}
- 查询
int sum_i(int i){
//求前i个数之和。
int res = 0;
for(;i>0;i-=lowbit(i)) res+=sum[i];
return res;
}
int query(int s,int e){
return sum(e) - sum(s-1);
}
- 修改
void update(int i,int val,int n){
for(;i<n;i+=lowbit(i)) sum[i]+=val;
}
主席树—-包括无修改和可修改。
定义
主席树是由一个连续数组的很多颗前缀树所组成的,每一个前缀1到i对应一颗树ti。每棵树有相同的结构,可以实现树之间的加减,对于每棵树的叶子节点,包含了一些该前缀的属性信息(由于主席树主要用来求第k大的数,因此包含的信息一般是第i个节点的值表示该前缀中第i小的数的个数),而非叶子节点的值为其子节点之和。且利用相邻的两棵浅醉树只相差一个树,也就只有一个飞叶子节点值不同,在树中也就一条路径不同,于是公用其他部分,只需一条新路径即可。
性质
由于每棵树的结构都是一样的,且非叶子节点表示大小排第i的数的个数,因此需要在开始前确定树的结构,在此使用的是满二叉树,因此也就等价于需要确定叶子节点的个数,叶子节点对于与数组中树的大小排名,因此需确定数组中有多少个不同的数,这一点对于无修改的很简单,但对于有修改的就需要先知道所有修改后的值。
对于可修改的情况,我们使用一个数组数组来维护修改值,也就是先确定无修改的主席树,然后新建一个树状数组,树状数组的每个节点也是一个同样结构的二叉树,但起始时都是空树(不占空间),根据树状数组的特征知每次修改只与logn个节点相关,而对每个节点,也就是最多会改变其二叉树的两个叶子节点的值,影响两条路径,这是修改或动态添加该路径即可,只有logn的消耗(时间和空间)。
因此总的时间与空间消耗复杂度都是一样的,无修改时为nlogn,修改时为nlognlogn
实现
- 确定不同的数的个数
int main{
...
//val 是原数组,rank是原数组的排序信息,st是val无重复数字的有序数组。
for(int i=1;i<=n;i++) rank[i]=i;
sort(rank+1,rank+n+1,[](int x,int j){return val[x]<val[y];});
size = 1; //无重复数字个数
for(int i=2;i<=n;i++){
if(val[rank[i]]!=val[rank[i-1]]) st[++size] = val[rank[i]];
val[rank[i]] = size;
}
st[1]=val[rank[1]];
val[rank[1]] = 1;
...
}
- 树的结构
int val[1000]; //原数组
int S[1000],T[1000],tot,size;
int L[100],R[100]; // 用于查询时维护树状数组,对应于每个节点的树的子树。
struct chairTreeNode{
int l;
int r;
int v;
}chairTree[100000];
- 建二叉树
void buildNode(int &root,int preRoot,int l,int r,int x){
root = ++tot;
chairTree[root].l = chairTree[preTree].l;
chairTree[root].r = chairTree[preTree].r;
chairTree[root].v = chairTree[preTree].v+1;
if(l == r) return;
int mid = l+r>>1;
if(x<=mid) builldNode(chairTree[root].l,chairTree[preTree].l,l,mid,x);
else builldNode(chairTree[root].r,chairTree[preTree].r,mid+1,r,x);
}
- 建主席树
void build(int n){
tot = 0;
T[0]=0;
chairTree[0].l=chairTree[0].r=chairTree[0].v = 0
for(int i=1;i<=n;i++) build(T[i],T[i-1],1,size,val[i]);
//对于可修改主席树,
for(int i=1;i<=n;i++) S[i] = T[0];
}
- 修改单个节点
void update(int &root,int index,int op,int s,int e){
if(root == 0){
root = ++tot;
chairTree[root].l=chairTree[root].r=chairTree[root].v=0;
}
chairTree[root].v+=op;
if(s==e) return;
int mid = s+e>>1;
if(x<=s) update(chairTree[root].l,x,op,s,mid);
else update(chairTree[root].r,x,op,mid+1,e);
}
void add(int index,int x,int op){
//每次修改需两次修改,一次删除op=-1,一次添加op=1
for(;index<=n;index+=lowbit(index))update(S[index],x,op,1,size);
}
- 查询
int query(int lroot,int rroot,int s,int e,int k){
if(l == r)return l;
int sum = chairTree[chairTree[rroot].l].v - chairTree[chairTree[lroot].l].v;
for(int i=1;i<=R[0];i++) sum+=chairTree[chairTree[R[i]].l].v;
for(int i=1;i<=L[0];i++) sum-=chairTree[chairTree[L[i]].l].v;
int mid = s+e>>1;
if(k<=sum){
for(int i=1;i<=R[0];i++) R[i] = chairTree[R[i]].l;
for(int i=1;i<=L[0];i++) L[i] = chairTree[L[i]].l;
return query(chairTree[lroot].l,chairTree[rroot].l,s,mid,k);
}else{
for(int i=1;i<=R[0];i++) R[i] = chairTree[R[i]].r;
for(int i=1;i<=L[0];i++) L[i] = chairTree[L[i]].r;
return query(chairTree[lroot].r,chairTree[rroot].r,mid+1,e,k-sum);
}
}
int query(int s,int e,int k){
L[0] = R[0] = 0;
for(int i=s-1;i;i-=lowbit(i)) L[++L[0]] = S[i];
for(int i=e;i;i-=lowbit(i)) R[++R[0]] = S[i];
return querySub(T[s-1],T[e],1,size,k);
}