主席树【可持久化线段树/函数式线段树/n棵线段树】
目录
一. 概念理解
- 离线数据结构
- 可以查询区间第k大
- 复杂度log(n)
1.求所有数字中的第k大数?
先将所有数字离散化【排序+去重】(所以是离线)。
对离散化后的数字建立一颗线段树,每个节点【 统计当前范围内的数的个数 】 。
自顶向下查找,如果左边区间个数大于等于k则在左边,小于则在右边(从大到小排序)。
2.求任意一段区间 [ L , r ]中的第k大数?
建立n棵线段树,每棵维护 [ 1 , i ] 的数字出现情况。
则 [ L , R ] = [ 1 , R ] - [ 1 , L - 1 ] (前缀和思想)。
3.会不会空间超限?
n棵线段树,每棵2n个点,怎么也是n^2的空间?
优化方法:注意到每棵新树都转移到上一棵树,只是改了一条从某叶子到根节点的路径 。
那么除了这条路径,其他的都可以直接从上一棵树上转移过来
大概就长这样了 ↓↓↓ 总共只用维护一棵树。空间复杂度O(nlogn)。

( 这个添加过程可以看做是:“链ys”加入“初始树xs”,即加入第一个元素 f 的过程 )
(d',g',f'都是新树相对于原树有修改的点,增加结点编号,把新旧结点都储存下来 )
当然,看到这里还是难以理解(反正本蒟蒻是没懂qaq)
那就让本蒟蒻手动模拟一下试试吧...(像素渣&&字渣无误)
- 以序列 4 1 3 2 为例,查询区间 [ 2,4 ] 中第3小的数。
用num[ ]记录原序列。首先“离散化+排序判重”,得到排序后的s数组 1 2 3 4 。
先确定初始线段树状态,再按照num[ ]的顺序,将 4 1 3 2 依次存入线段树中。
ONE POINT{很重要的理解}:下图圆圈内的数就是线段树存的权值,代表着:
统计(按照排名记录的区间)区间 [ i , j ] 中,已经存入的原序列的数(num[ ])的个数。
底层叶子节点记录的是,这个排名位置的数是否已经在树中建立。
0代表还没有建立,1表示已经建立,同时关联的上方管理数组的权值也会相应改变。
下面来模拟建树过程——
(状态1)原始空线段树,所有点的权值都为零。

(状态2)插入num[1]=4,到排序后它应该在的4号位置(是第四小的数)。
第一棵修改树可以用根节点序号标记为root[1]=8(具体root的实现看后面)。

(状态3)插入num[2]=1,到排序后它应该在的1号位置(是第一小的数)。

(状态4)插入num[3]=3,到排序后它应该在的3号位置(是第三小的数)。
(状态5)插入num[4]=2,到排序后它应该在的2号位置(是第二小的数)。

这样,未优化空间的、插入元素使用的、5棵树就建好了。
为优化空间,可以使用树的合并(就是上面第三大点说的【优化空间复杂度】)。

主席树的两个主要函数:插入和合并。
插入:动态开点。很多时候线段树维护的区间很大,而能定义的空间是有限的,
所以我们就只给那些有用的点一个编号就可以了(只有在修改时被访问过的点才是有用的)。
虽然这样的线段树是残缺不全的,但也还是线段树。
合并:当两棵线段树的下标和维护的范围大小都一样,这两棵线段树就可以合并。
(null表示还没有存入的节点,圆圈中的数表示存储的下方的已插入结点的个数)
(图中最下方的数字指的是离散化后的排名情况...)

对于每个位置都建一颗线段树,其实是一条链。
然后再按照顺序把线段树都合并起来。第 i 棵线段树维护的就是 1~i 区间的信息。
通过合并可以知道,线段树满足可加性,那么得到的信息肯定可以相减。(↓前缀和思想)
对于每一次询问通过第 r 棵线段树和第 ( l - 1 ) 棵线段树作差,即可得到该区间的信息。


因为我们是以离散数组构建的主席树,那么从根节点出发,左子树部分的数必定不大于右子树部分的数。
于是就可以将左儿子的节点个数 x 与 k 做比较,若 k≤x,则第 k 小值一定在左子树里面,
若 x≤k,则第 k 小值一定在右子树里面,然后递归往下走,缩小范围。
注意,前者递归时, k 直接传下去即可,后者递归时,需要将 k 减去左子树的数的个数再传递这个 k 值。
例如我们查找 [1,4] 中第 2 小的值,图示如下,绿色节点为该值存在的区间位置。

↑↑↑ 需要注意的是,第二个绿色节点才是绿色根节点的左子树,因为左子树表示的区间是靠前的那一半。
方法步骤总结:

现在我们真正来解决区间询问 [ l , r ] 的问题。
解决方案就是将主席树 [1,r] 减去主席树 [1,l−1] 。
首先看到主席树的底层,全部是对数字个数的统计。
当主席树 [1,r] 减去主席树 [1,l−1] 时,统计也跟着减了。
而我们不需要单独减,只需要边递归查询边减,具体查询部分代码如下:
//初始的u和v分别代表的是点l-1和点r,l和r分别表示线段树点代表的区间,初始的k如题
int query(int u, int v, int l, int r, int k){
int ans, mid = ((l + r) >> 1), x = sum[lc[v]] - sum[lc[u]];
//因为主席树是区间统计好了的,只要减一下即可,无需递归到叶子再处理
if(l == r)//找到目标位置
return l;
if(x >= k) ans = query(lc[u], lc[v], l, mid, k);
else ans = query(rc[u], rc[v], mid+1, r, k-x);//右子树记得改变k的值
return ans;
}
实现方法差不多就是这样了 ,具体操作过程还要看代码 ↓↓↓。
二. 例题演练
1【主席树】第K小的数Ⅰ(caioj1441)
- 给n(n<=100000)个数字a[1],a[2],...,a[n](a[i]<=1000000000),
- m(1<=m<=100000)次询问l到r之间的第k大的值。
【分析】主席树的两个主要函数:插入和合并。
插入:动态开点。很多时候线段树维护的区间很大,而能定义的空间是有限的,
所以我们就只给那些有用的点一个编号就可以了(只有在修改时被访问过的点才是有用的)。
虽然这样的线段树是残缺不全的,但也还是线段树。
合并:当两棵线段树的下标和维护的范围大小都一样,这两棵线段树就可以合并。
(null表示还没有存入的节点,圆圈中的数表示存储的下方的已插入结点的个数)
(图中最下方的数字指的是离散化后的排名情况...)

对于每个位置都建一颗线段树,其实是一条链。
然后再按照顺序把线段树都合并起来。第 i 棵线段树维护的就是 1~i 区间的信息。
通过合并可以知道,线段树满足可加性,那么得到的信息肯定可以相减。(↓前缀和思想)
对于每一次询问通过第 r 棵线段树和第 ( l - 1 ) 棵线段树作差,即可得到该区间的信息。
因为我们是以离散数组构建的主席树,那么从根节点出发,左子树部分的数必定不大于右子树部分的数。
于是就可以将左儿子的节点个数 x 与 k 做比较,若 k≤x,则第 k 小值一定在左子树里面,
若 x≤k,则第 k 小值一定在右子树里面,然后递归往下走,缩小范围。
值得注意的是,前者递归时, k 直接传下去即可,后者递归时,需要将 k 减去左子树的数的个数再传递这个 k 值。
例如我们查找 [1,4] 中第 2 小的值,图示如下,绿色节点为该值存在的区间位置。
【参考程序1】//这个程序在细节处理方面有点小问题,洛谷p3834最后两个点TLE了...
#include <bits/stdc++.h>
using namespace std;
/*【主席树模板】【caioj 1441】【洛谷p3834】第k小的数1
给n(n<=100000)个数字a[1],a[2],...,a[n](a[i]<=1000000000),
m(1<=m<=100000)次询问l到r之间的第k小的值。 */
struct node{
int lc,rc,c;
//c是线段树的权值(记录下方已存入的原序列数个数)
//也就是结点所代表的这个区间、维护的最下层节点个数
}t[2100000];
int cnt,n,k;
int wr[110000],s[110000],rt[110000];
//↑↑↑原数组,排序数组,根节点数组
int Pos(int num){ //用离散化编号建树
//↑↑↑(二分)寻找当前根节点的【数值】所对应的【大小编号】
int mid,ans,l,r;
l=1; r=n;
while(l<=r){
mid=(l+r)/2;
if(s[mid]<=num) ans=mid, l=mid+1;
else r=mid-1;
}
return ans; //此时的根节点是第ans小的,要存入线段树最底层的ans号位置
}
//↓↓↓插入函数:动态开点,只会加入[(按编号)存入原数组数值的路径上]被访问过的点
void Link(int &u,int l,int r,int p){
//↑↑↑u是每次要询问子树的管理节点编号,也就是存入原数组编号数值的路径上的某个点
//↑↑↑从(节点编号++的)根节点出发,寻找到达排名p节点的路径,u的值是随时改变的
if(!u) u=++cnt; //一开始rt[]均为0,也就是给每条新链的根节点都重新编号
t[u].c++; //先变为1,然后在Merge函数中,用类似前缀和的方法累加:c[i]+=c[i-1];
if(l==r) return; //区间生成完毕,完成[寻找排名p节点的链式路程]建立
int mid=(l+r)/2; //分治思想:左右子树分别处理
if(p<=mid) Link(t[u].lc,l,mid,p);
else Link(t[u].rc,mid+1,r,p); //t[新节点].lc/rc均为0,即:给新链的所有节点都重新编号
}
//↓↓↓合并函数:与前i-1条链合并,总树(只有一颗树)就可以维护的就是1~i的信息
void Merge(int &u1,int u2){ //用根节点序号表示链
if(!u1){ u1=u2; return; } //一开始没有树,直接把链当做树
if(!u2) return; //u2是空链
//↓↓↓把链u2合并到总树u1,u1为主体,利用u2的一些点
t[u1].c+=t[u2].c; //类似前缀和的c[i]+=c[i-1];
Merge(t[u1].lc,t[u2].lc); //合并的树和链(未满的树)大小形状都相同
Merge(t[u1].rc,t[u2].rc);
}
int Calc(int u1,int u2,int l,int r,int k){ //计算区间第k大
if(l==r) return s[l]; //l=r时,即找到了区间第k大对应的离散化编号l
int c=t[t[u1].lc].c-t[t[u2].lc].c;
//↑↑↑用u1这棵线段树减u2这棵线段树就可以得到[u1到u2]这段区间的信息
//因为c为权值数组,表示管理的下层数的个数,所以可以直接加减
int mid=(l+r)/2;
if(k<=c) return Calc(t[u1].lc,t[u2].lc,l,mid,k);
else return Calc(t[u1].rc,t[u2].rc,mid+1,r,k-c);
} //在右边,需要将k减去左子树的数的个数,再传递↑↑↑
int main(){
cnt=0; //节点编号:每个含有更新状态的节点都要重新标号
//当然原状态的那个节点编号不变,可以用于历史化搜索
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++)
scanf("%d",&wr[i]), s[i]=wr[i];
sort(s+1,s+n+1); //排序,用此时的编号代入树中(排序,但未去重)
for(int i=1;i<=n;i++){
Link(rt[i],1,n,Pos(wr[i]));
//新链的根(在函数中重新编号)+链(未满树)的管理区间+序号i的数值的排名位置
Merge(rt[i],rt[i-1]); //将其与前i-1条链合并,总树维护的就是1~i的信息
} //注意:rt[i]在函数中会自行改变,用于记录链i在总树中的编号
while(k--){
int x,y,k; scanf("%d%d%d",&x,&y,&k);
printf("%d\n",Calc(rt[y],rt[x-1],1,n,k));
//区间 x,y 转化为 rt[y]-rt[x-1] 链的相减(总树的两个小部分)
//区间第k小 转化为 该树中编号为k的位置的数
}
return 0;
}
【参考程序2】
#include <bits/stdc++.h>
using namespace std;
/*【主席树模板】【caioj 1441】【洛谷p3834】第k小的数1
给n(n<=100000)个数字num[1],num[2],...,num[n](num[i]<=1000000000),
m(1<=m<=100000)次询问l到r之间的第k小的值。 */
int cnt, n, m, pos; //pos为排名位置
int sum[200010<<5], rt[200010], lc[200010<<5], rc[200010<<5]; //sum记录权值
int num[200010], s[200010];//原序列和离散序列
void build(int &t, int l, int r){ //建立链
t = ++cnt;
if(l == r) return;
int mid = (l + r) >> 1;
build(lc[t], l, mid);
build(rc[t], mid+1, r);
}
//节点o表示区间[l,r],修改值根据题意设定(此处我们先不谈题目,只谈数据结构)
int modify(int o, int l, int r, int pos){
int oo = ++cnt;
lc[oo] = lc[o]; rc[oo] = rc[o]; sum[oo] = sum[o] + 1;//新节点,这里是根据模板题来的
//递归底层返回新节点编号,修改父节点的儿子指向
//sum[oo] = t;如果题目要求sum是加t的再这样弄,然后上面的+1就去掉
if(l == r) return oo;
int mid = (l + r) >> 1;
if(pos <= mid) lc[oo] = modify(lc[oo], l, mid);
else rc[oo] = modify(rc[oo], mid+1, r);
return oo;
}
//初始的u和v分别代表的是点l-1和点r,l和r分别表示线段树点代表的区间
int query(int u, int v, int l, int r, int k){
int ans, mid = ((l + r) >> 1), x = sum[lc[v]] - sum[lc[u]];
//因为主席树是区间统计好了的,只要减一下即可,无需递归到叶子再处理
if(l == r) return l;//找到目标位置
if(x >= k) ans = query(lc[u], lc[v], l, mid, k);
else ans = query(rc[u], rc[v], mid+1, r, k-x);//右子树记得改变k的值
return ans;
}
int main(){
int l, r, k, len, ans;
scanf("%d%d", &n, &m);
for(register int i = 1; i <= n; i += 1)
scanf("%d", &num[i]), s[i] = num[i];
sort(s+1, s+n+1); //排序
len = unique(s+1, s+n+1) - s - 1; //去重
build(rt[0], 1, len);
for(register int i = 1; i <= n; i += 1){
pos = lower_bound(s+1, s+len+1, num[i])-s;
//↑↑↑可以视为查找最小下标的匹配值,核心算法是二分查找
rt[i] = modify(rt[i-1], 1, len);
}
while(m--){
scanf("%d%d%d", &l, &r, &k);
ans = query(rt[l-1], rt[r], 1, len, k);
printf("%d\n", s[ans]);
}
return 0;
}
// 下面的题,对于现在还没有研究树套树的我决定先搁置orz
2 【主席树】第K小的数Ⅱ(caioj1442)
【分析】树状数组套主席树。
因为我们求 l 到 r 这个区间的信息是使用类似前缀和的方法,
那么带修改的前缀和的问题可以用线段树或者树状数组来维护。
在树状数组的每个节点下都建一棵权值线段树,维护的是第 i 个节点的修改情况。
修改:每次先减去之前值的影响,再加上这个新的值影响。
询问:用树状数组各自求 1~l 和 1~r 的和,再相减,再加上原来的信息。
至于树状数组求和该怎么在线段树跳,我们就先预处理出来就OK。
【参考程序】
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
struct node
{
int lc,rc,c;
}t[5100000];
int rt[210000],ust[210000],a[110000];
int cnt,n,m;
char opt[20];
int lowbit(int x){return x&-x;}
void Link(int &u,int l,int r,int p,int c)
{
if(!u)u=++cnt;
t[u].c+=c;
if(l==r)return ;
int mid=(l+r)/2;
if(p<=mid)Link(t[u].lc,l,mid,p,c);
else Link(t[u].rc,mid+1,r,p,c);
}
void Merge(int &u1,int u2)
{
if(!u1){u1=u2;return ;}
if(!u2)return ;
t[u1].c+=t[u2].c;
Merge(t[u1].lc,t[u2].lc);
Merge(t[u1].rc,t[u2].rc);
}
void Turn(int u,int c)
{
while(u>=n+1)
{
if(c==-1)ust[u]=rt[u];//从根跳
else if(c==0)ust[u]=t[ust[u]].lc;//往左跳
else if(c==1)ust[u]=t[ust[u]].rc;//往右跳
u-=lowbit(u);
}
}
void Modify(int u,int p,int c)
{
while(u<=2*n)
{
Link(rt[u],0,1000000000,p,c);
u+=lowbit(u);
}
}
int Getsum(int u)
{
int ret=0;
while(u>=n+1)
{
ret+=t[t[ust[u]].lc].c;
u-=lowbit(u);
}
return ret;
}
void Calc(int u1,int u2,int p1,int p2,int l,int r,int k)
{
if(l==r)
{
printf("%d\n",l);
return ;
}
int c=t[t[u2].lc].c-t[t[u1].lc].c+Getsum(p2+n)-Getsum(p1+n);
//用树状数组求出p1和p2区间的信息和再加上原来的信息就可以得到新的信息
int mid=(l+r)/2;
if(k<=c)
{
Turn(p1+n,0);
Turn(p2+n,0);
Calc(t[u1].lc,t[u2].lc,p1,p2,l,mid,k);
}
else
{
Turn(p1+n,1);
Turn(p2+n,1);
Calc(t[u1].rc,t[u2].rc,p1,p2,mid+1,r,k-c);
}
}
int main()
{
cnt=0;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
Link(rt[i],0,1000000000,a[i],1);
Merge(rt[i],rt[i-1]);
}
//rt[1]~rt[n]就是普通的主席树,维护原来的信息
//rt[n+1]~rt[2*n]维护的就是第i-n棵线段树修改的情况
while(m--)
{
scanf("%s", opt+1);
if(opt[1]=='C')
{
int p, c;
scanf("%d%d",&p,&c);
Modify(p+n,a[p],-1);
//先减去之前的值的影响
a[p]=c;
Modify(p+n,a[p],1);
//先加上要修改的值的影响
}
else
{
int l, r, k;
scanf("%d%d%d",&l,&r,&k);
Turn(l-1+n,-1);
Turn(r+n,-1);
//先预处理出跳的方向。
Ask(rt[l-1],rt[r],l-1,r,0,1000000000,k);
}
}
return 0;
}
3 【主席树】第K小的数Ⅲ(caioj1443)
【分析】对于 i 这条链它的定义就修改为 i 到根的信息。
询问 ( l , r ) 时用 l 这棵线段树加 r 这棵线段树减去一个最近公共祖先的线段树,
再减去最近公共祖先父亲节点的线段树,就可以得出 l 到 r 路径上的信息。
【参考程序】
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
struct tnode
{
int c,lc,rc;
} t[2600000]; int cnt;
int rt[110000],val[110000],dep[110000],f[110000][25],s[110000];
int n,m,root;
struct node
{
int x,y,next;
}e[210000];int len,last[110000];
void Ins(int x, int y)
{
e[++len].x=x;e[len].y=y;
e[len].next=last[x];last[x]=len;
}
int Pos(int num)
{
int l,r,mid,ret;
l=1;
r=n;
while(l<=r)
{
mid=(l+r)/2;
if(s[mid]<=num)
{
ret=mid;
l=mid+1;
}
else r=mid-1;
}
return ret;
}
void Link(int &u,int l,int r,int p)
{
if(!u)u=++cnt;
t[u].c++;
if(l==r)return ;
int mid=(l+r)/2;
if(p<=mid)Link(t[u].lc,l,mid,p);
else Link(t[u].rc,mid+1,r,p);
}
void Merge(int &u1,int u2)
{
if(!u1){u1=u2;return ;}
if(!u2)return ;
t[u1].c+=t[u2].c;
Merge(t[u1].lc,t[u2].lc);
Merge(t[u1].rc,t[u2].rc);
}
int Calc(int u1,int u2,int u3,int u4,int l,int r,int k)//u3是u1和u2的最近公共祖先,u4是最近公共祖先的父亲
{
if(l==r)return s[l];
int c=t[t[u1].lc].c+t[t[u2].lc].c-t[t[u3].lc].c-t[t[u4].lc].c;
//u1这棵线段树加u2这棵线段树减去一个最近公共祖先的线段树再减去最近公共祖先父亲节点的线段树
int mid=(l+r)/2;
if(k<=c)return Calc(t[u1].lc,t[u2].lc,t[u3].lc,t[u4].lc,l,mid,k);
else return Calc(t[u1].rc,t[u2].rc,t[u3].rc,t[u4].rc,mid+1,r,k-c);
}
void Build(int x,int fa)
{
dep[x]=dep[fa]+1;
f[x][0]=fa;
for(int i=1;(1<<i)<=dep[x];i++)
f[x][i]=f[f[x][i-1]][i-1];
Merge(rt[x],rt[fa]);//将其与其父亲节点合并,其他都是倍增lca的过程
for(int k=last[x];k;k=e[k].next)
{
int y=e[k].y;
if(y!=fa)
Build(y,x);
}
}
int Lca(int x,int y)//求lca的过程
{
if(dep[x]<dep[y])swap(x,y);
for(int i=20;i>=0;i--)
if(dep[x]-dep[y]>=(1<<i))
x=f[x][i];
if(x==y)return x;
for(int i=20;i>=0;i--)
if(dep[x]>=(1<<i)&&f[x][i]!=f[y][i])
x=f[x][i],y=f[y][i];
return f[x][0];
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%d",&val[i]);
s[i]=val[i];
}
sort(s+1,s+n+1);
len=0;
memset(last,0,sizeof(last));
for(int i=1;i<n;i++)
{
int x, y;
scanf("%d%d",&x,&y);
Ins(x,y);
Ins(y,x);
}
cnt=0;
for(int i=1;i<=n;i++)Link(rt[i],1,n,Pos(val[i]));
//一开始不直接合并,因为i-1并不一定是i的父亲节点
root=1;
Build(root,0);//构建f数组,建树
while(m--)
{
int x,y,lca,k;
scanf("%d%d%d",&x,&y,&k);
lca=Lca(x,y);//求最近公共祖先
printf("%d\n",Calc(rt[x],rt[y],rt[lca],rt[f[lca][0]],1,n,k));
}
return 0;
}
4 【主席树】逆序对数(caioj1444)
【分析】这道题要用到分块+树状数组+主席树。
我们一开始可能有个思路,用一个 ans[ i ][ j ] 数组表示从 i 起到第 j 个数的逆序列对数,
这个中间的过程我们可以用树状数组来求出,但时间复杂度很高 O(n^2*log n)。
那我们再考虑我们的老朋友主席树。我能想到的方法就是枚举 l 到 r 的每个节点,
主席树在线求出他们前面有多少对逆序对数,这种方法也对,但是时间复杂度是 O(m*n*log n)。
那么我们这里可以分块,分成 sqrt( n ) 块,我们的 ans[ i ][ j ] 就指从第 i 块的开头起,
一直到第j个数中有多少对逆序对数,这样我们就可以预处理出右半部分的答案,
剩下的部分就可以用主席树在线求。
预处理的时间复杂度就是 O(n*sqrt n*log n),在线处理的时间复杂度为 O(m*sqrt n*n)。
【参考程序】
#include<cmath>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
struct node
{
int lc,rc,c;
}t[1600000];int cnt=0;
int rt[51000],a[51000],h[51000];
int n,m,belong[51000],l[250],r[250],anss[250][51000];
int s[51000],q[51000];
int Pos(int num)
{
int mid,ans,l,r;
l=1;
r=n;
while(l<=r)
{
mid=(l+r)/2;
if(h[mid]<=num)
{
ans=mid;
l=mid+1;
}
else r=mid-1;
}
return ans;
}
int lowbit(int x){return x&-x;}
void add(int x,int y)
{
while(x<=n)
{
s[x]+=y;
x+=lowbit(x);
}
}
int getsum(int x)
{
int sum=0;
while(x)
{
sum+=s[x];
x-=lowbit(x);
}
return sum;
}
void Link(int &u,int l,int r,int x)
{
if(!u)u=++cnt;
t[u].c++;
if(l==r)return;
int mid=(l+r)/2;
if(x<=mid)Link(t[u].lc,l,mid,x);
else Link(t[u].rc,mid+1,r,x);
}
void Merge(int &u1, int u2)
{
if(!u1){u1=u2;return;}
if(!u2)return;
t[u1].c+=t[u2].c;
Merge(t[u1].lc,t[u2].lc);
Merge(t[u1].rc,t[u2].rc);
}
int Calc(int u1,int u2,int l,int r,int ll,int rr)
{
if(!u1&&!u2)return 0;
if(l==ll&&rr==r)return t[u1].c-t[u2].c;
int mid=(ll+rr)/2;
if(r<=mid)return Calc(t[u1].lc,t[u2].lc,l,r,ll,mid);
else if(l>mid)return Calc(t[u1].rc,t[u2].rc,l,r,mid+1,rr);
else return Calc(t[u1].lc,t[u2].lc,l,mid,ll,mid)+Calc(t[u1].rc,t[u2].rc,mid+1,r,mid+1,rr);
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
h[i]=a[i];
}
sort(h+1,h+1+n);
for(int i=1;i<=n;i++)a[i]=Pos(a[i]);
//求出a[i]离散化后的值
int sq=int(sqrt(double(n+1)));
for(int i=1;i<=n;i++)//分块的过程
{
belong[i]=(i-1)/sq+1;
r[belong[i]]=i;
if(!l[belong[i]])l[belong[i]]=i;
}
int t=0;
for(int i=1;i<=belong[n];i++)//用树状数组来预处理出anss数组。
{
memset(s,0,sizeof(s));
anss[i][l[i]]=0;
add(a[l[i]],1);
for(int j=l[i]+1;j<=n;j++)
{
anss[i][j]=anss[i][j-1]+(j-l[i]-getsum(a[j]));
add(a[j],1);
}
}
for(int i=1;i<=n;i++)
{
Link(rt[i],1,n,a[i]);
Merge(rt[i],rt[i-1]);
}
memset(s,0,sizeof(s));
int ans=0;
while(m--)
{
int L,R;
scanf("%d%d",&L,&R);
if(L>R)swap(L,R);
int bl=belong[L],br=belong[R];
if(bl==br)//如果两个点在同一个区间,就直接用主席树求逆序对数。
{
ans=0;
for(int i=L;i<=R;i++)
ans+=Calc(rt[R],rt[i],1,a[i]-1,1,n);
printf("%d\n",ans);
}
else
{
ans=anss[bl+1][R];//预处理的答案
for(int i=L;i<=r[bl];i++)//就直接用主席树求左区间的逆序对数。
ans+=Calc(rt[R],rt[i],1,a[i]-1,1,n);
printf("%d\n",ans);
}
}
return 0;
}
5【主席树】去月球(caioj1447)
【分析】对于每一次修改都增加一条链,将这条链跟之前的链合并。
修改我们肯定要用到lazy标记了,因为这是可持续化线段树,
标记是不用下传的,所以不用担心会改变历史的信息。
【参考程序】
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define LL long long
using namespace std;
struct node
{
LL c,lazy,lc,rc;
} t[5100000];
int cnt;
LL rt[110000],a[110000],s[110000],now,n,m;
void Build(LL &u,LL l,LL r)
{
if(!u)u=++cnt;
t[u].c=s[r]-s[l-1];
if(l==r)return ;
LL mid=(l+r)/2;
Build(t[u].lc,l,mid);
Build(t[u].rc,mid+1,r);
}
void Calc(LL u,LL l,LL r,LL cl,LL cr,LL &ret,LL mark)
{
if(l==cl&&r==cr)
{
ret+=t[u].c;
ret+=mark*(r-l+1);
return ;
}
LL mid=(l+r)/2;
//标记不用下传,只用累加就好了
if(cr<=mid)Calc(t[u].lc,l,mid,cl,cr,ret,mark+t[u].lazy);
else if(cl>mid)Calc(t[u].rc,mid+1,r,cl,cr,ret,mark+t[u].lazy);
else
{
Calc(t[u].lc,l,mid,cl,mid,ret,mark+t[u].lazy);
Calc(t[u].rc,mid+1,r,mid+1,cr,ret,mark+t[u].lazy);
}
}
void Link(LL &u,LL l,LL r,LL cl,LL cr,LL c)
{
if(!u)u=++cnt;
t[u].c+=(cr-cl+1)*c;
if(l==cl&&r==cr)
{
t[u].lazy+=c;//lazy标记~
return ;
}
LL mid=(l+r)/2;
if(cr<=mid)Link(t[u].lc,l,mid,cl,cr,c);
else if(cl>mid)Link(t[u].rc,mid+1,r,cl,cr,c);
else
{
Link(t[u].lc,l,mid,cl,mid,c);
Link(t[u].rc,mid+1,r,mid+1,cr,c);
}
}
void Merge(LL &u1,LL u2)
{
if(!u1)
{
u1=u2;
return ;
}
if(!u2)return ;
t[u1].c+=t[u2].c;
t[u1].lazy+=t[u2].lazy;
Merge(t[u1].lc,t[u2].lc);
Merge(t[u1].rc,t[u2].rc);
}
int main()
{
scanf("%lld%lld",&n,&m);
s[0]=0;
for(LL i=1;i<=n;i++)
{
scanf("%lld",&a[i]);
s[i]=s[i-1]+a[i];
}
cnt=0;
Build(rt[0],1,n);
now=0;
while(m--)
{
int kk;
scanf("%d",&kk);
if(kk==2)
{
LL l,r,ret;
scanf("%lld%lld",&l,&r);
ret=0;
Calc(rt[now],1,n,l,r,ret,0);
printf("%lld\n",ret);
}
else if(kk==4)
{
LL bt;
scanf("%lld",&bt);
for(int i=bt+1;i<=now;i++)rt[i]=0;
now=bt;//将不用的线段树删除即可
}
else if(kk==1)
{
LL l,r,c;
scanf("%lld%lld%lld",&l,&r,&c);
now++;
Link(rt[now],1,n,l,r,c);//每次修改插入一条一棵线段树,代表一个区间的修改
Merge(rt[now],rt[now-1]);//让其与前面的链合并,这样它维护的就是前i个时间点的修改
}
else if(kk==3)
{
LL l,r,h,ret;
scanf("%lld%lld%lld",&l,&r,&h);
ret=0;
Calc(rt[h],1,n,l,r,ret,0);
printf("%lld\n",ret);
}
}
return 0;
}
【练习】
1、caioj1445:【主席树】求区间种类
2、caioj1446:【主席树】简单询问
3、caioj1448:【主席树】简单查询
4、bzoj1146:[CTSC2008]网络管理Network
5、bzoj2653:middle
——时间划过风的轨迹,那个少年,还在等你。


2928

被折叠的 条评论
为什么被折叠?



