动态开点
为了降低权值线段树的空间复杂度,可以不直接建出整棵线段树的结构,而是在最初只建立一个根节点,当需要访问某棵为建立的子树的时候,再建立代表这个子树的节点。
动态开点的线段树用变量记录左右节点的编号。
值域为1-n的动态开点线段树在m次单点修改后,节点规模为O(mlogn)
例题:
P1908 逆序对
(这题n最大1e9,正常应该用离散化做,但是有了动态开点不怕)
涉及单点修改、查询区间和
code:
#include<cstdio>
#include<cstring>
#include<iostream>
#include<cmath>
#include<algorithm>
typedef long long ll;
using namespace std;
const int maxm=1e7+5;
struct Node{
int lc,rc,sum;//lc,rc记录左右子节点编号
}tr[maxm];
int cnt;
int build(){//返回新节点的编号
cnt++;
tr[cnt].lc=tr[cnt].rc=tr[cnt].sum=0;
return cnt;
}
void init(){//定义1为根节点
cnt=1;
tr[1].lc=tr[1].rc=tr[1].sum=0;
}
void pushup(int node){
tr[node].sum=tr[tr[node].lc].sum+tr[tr[node].rc].sum;
}
void update(int x,int v,int l,int r,int node){
if(l==r){
tr[node].sum+=v;
return ;
}
int mid=(l+r)/2;
if(x<=mid){
if(!tr[node].lc)tr[node].lc=build();//如果左边是空的就建立
update(x,v,l,mid,tr[node].lc);
}else{
if(!tr[node].rc)tr[node].rc=build();//如果右边是空的就建立
update(x,v,mid+1,r,tr[node].rc);
}
pushup(node);
}
int ask(int st,int ed,int l,int r,int node){
if(st<=l&&ed>=r){
return tr[node].sum;
}
int mid=(l+r)/2;
int ans=0;
if(st<=mid){
if(tr[node].lc)ans+=ask(st,ed,l,mid,tr[node].lc);
}
if(ed>=mid){
if(tr[node].rc)ans+=ask(st,ed,mid+1,r,tr[node].rc);
}
return ans;
}
int main(){
init();
int n;
scanf("%d",&n);
ll ans=0;
for(int i=0;i<n;i++){
int t;
scanf("%d",&t);
ans+=ask(t+1,1e9,1,1e9,1);
update(t,1,1,1e9,1);
}
printf("%lld",ans);
return 0;
}
线段树合并
若多棵动态开点线段树值域都为1-n,显然所有树对于子区间的划分是相同的。
现在要把所有树的信息整合在一起,即线段树合并.
合并流程:
选择两棵线段树,用p,q指向两棵树的根节点,向下递归合并,(递归方向必须相同,即p,q代表相同区间)。
如果过程中p,q某一为空,则以非空的作为合并之后的节点。
否则继续递归合并两棵左子树和两棵右子树,回溯时删除其中一个节点(如删除q),以另一节点作为合并之后的节点(如p),同时自底向上统计合并后的信息。
code:
维护区间最值
const int maxm=1e7+5;
struct Node{
int lc,rc,ma;
}tr[maxm];
int merged(int p,int q,int l,int r){
if(!p)return q;//p、q中有一个为空
if(!q)return p;
int mid=(l+r)/2;
tr[p].lc=merged(tr[p].lc,tr[q].lc,l,mid);
tr[p].rc=merged(tr[p].rc,tr[q].rc,mid+1,r);
tr[p].ma=max(tr[tr[p].lc].ma,tr[tr[p].rc].ma);
return p;
}
因为发生递归必有一节点被删除,因此函数执行次数不会超过节点总数+1,因此复杂度为O(mlogn),与单点操作复杂度相同