主席树 【权值线段树】 && 例题K-th Number POJ - 2104
一、主席树与权值线段树区别
主席树是由许多权值线段树构成,单独的权值线段树只能解决寻找整个区间第k大/小值问题(什么叫整个区间,比如你对区间[1,8]建立一颗对应权值线段树,那么你不能询问区间[2,5]第k大/小值,你只能询问[1,8]第k大/小值问题)
二、权值线段树是什么鬼
学权值线段树之前你肯定要知道线段树,线段树是对区间中的所有数进行维护
权值线段树维护的对象和它不同,权值<==>这个数在这个区间中出现的次数
例如1、5、3、2、4
建立的权值线段树:
你会发现,权值线段树的建立和线段树不一样,线段树要考虑每一个数在区间中的位置
但是权值线段树不用管,相当于排完序之后建立的一颗树 。 要注意:权值代表这个数出现了几次
例如:我们寻找这个区间的第3小值
我们从根节点[1,5]的左节点来判断,因为左节点的权值要大于3,所以区间内第3小值肯定在根节点的左区间内
我们再看区间[1,3]的左节点,发现它的左节点权值为2小于3,所以答案肯定在区间[1,3]的右节点内。注意:这个时候我们要在右节点内寻找第3-2小值
之后那区间[3,3]内第1小值肯定就是3本身了
要注意如果题目给你的数据特别大,例如1、1000、200000000这三个数,我们要是直接建立权值线段树,那内存就炸了
所以这个时候就要离散化处理
三、主席树构成
我们文章开头说过,主席树是由许多权值线段树构成,而且主席树可以解决:在对应区间建立的主席树,可以找寻该区间所有子区间内的第k大/小值
(即、对区间[1,5]建立主席树,那么我们也可以找寻区间[1,3]的第k大/小值)
1、解决方法1:
排序后找到那个位置输出(不用想了,暴力方法肯定会被卡)
2、解决方法2:
对于一个区间[l, r]我们用一个用这个区间内出现过的数的个数组成一颗权值线段树,然后查询就完事了
但是多次询问区间第k小,我们每次这样建立一个线段树,这样不仅空间复杂度非常之高,而且时间复杂度也非常高,甚至比普通排序还要高,那么我们只不是可以想一个办法,使得对于每次我们查询不同的区间我们不需要重新建树,如果这样。时间复杂度和空间复杂度就大大降低了。
3、解决方法3:
这就用到了前缀和,比如我们有一个问题。就是每次静态的求区间和,我们可以预处理所以的前缀和sum[i],我们每次求解区间[l, r]和时,我们可以直接得到答案为sum[r] - sum[l -1],这样就不需要对区间中的每个数进行相加来求解了。(这里我们设sum[i]中放的是权值线段树上i节点的权值)
想到就写起来
我们可以对区间[1,5]建立6棵权值线段树,分别是[0,0],[1,1],[1,2],[1,3],[1,4],[1,5] ([0,0]是一颗空树)
这样我们处理任意区间[l, r]时就可以通过处理区间[1,l - 1], [1,r],就行,然后两者的处理结果进行相加相减就行。为什么满足相加减的性质,我们简单分析一下就很容易得出。如果在区间[1,l - 1]中有x个数小于一个数,在[1,r]中有y个数小于那个数,那么在区间[l,r]中就有y - x 个数小于那个数了,这样就很好理解为什么可以相加减了,另外,每颗树的结构都一样,都是一颗叶节点为n个的线段树。
上述利用前缀和的思想只是解决了时间复杂度的问题,并没有解决空间复杂度的问题,要解决空间复杂度问题。我们需要用到线段树的性质,我们每次更新一个数,那么与更新之前相比,这颗线段树改变只是一条链(从根节点到某一叶节点),那么我们可以充分利用这个特点,因为第i颗树与第i- 1颗树相比,只是更新了第i个元素,那么实际上第i颗树与第i-1颗树之间只有log个节点的信息是不同的。.所以这两棵树有很多相同的节点,所以这两棵树可以共用很多节点,也就是说,我们在第i-1颗树上 插入一个节点a[i] 得到第i颗权值线段树,而单点插入过程中只会修改根到那个叶子节点的路径上的那log个节点。于是这样就解决空间复杂度问题。
还是以上面的1、3、2、5、4为例子:
1、刚开始的空树
2、加一个1节点
3、加一个2节点
4、加一个节点3
后面的4和5号节点就不写了,大家懂了就行。。。
四、主席树复杂度
插入一个点的时空复杂度都为O(log n),所以建立这颗主席树【权值线段树总体】的时空复杂度就是O(n log n),单次询问经过log n个节点,时间复杂度也为O(log n)
5、例题
K-th Number POJ - 2104
代码1:
1 #include<stdio.h> 2 #include<iostream> 3 #include<algorithm> 4 #include<string.h> 5 using namespace std; 6 const int maxn=1e5+10; 7 int cnt,ranks[maxn],v[maxn],root[maxn]; 8 struct shudui 9 { 10 int value,id; 11 }w[maxn]; 12 struct Node 13 { 14 int l,r,sum; 15 Node(){ 16 sum=0; 17 } 18 }tree[maxn*20]; 19 bool mmp(shudui x,shudui y) 20 { 21 return x.value<y.value; 22 } 23 void init() 24 { 25 cnt=1; 26 root[0]=0; 27 tree[0].l=tree[0].r=tree[0].sum=0; 28 } 29 void inserts(int num,int &rt,int l,int r) 30 { 31 tree[cnt++]=tree[rt]; 32 rt=cnt-1; 33 tree[rt].sum++; 34 if(l==r) return; 35 int mid=(l+r)>>1; 36 if(num<=mid) inserts(num,tree[rt].l,l,mid); 37 else inserts(num,tree[rt].r,mid+1,r); 38 } 39 int query(int i,int j,int k,int l,int r) 40 { 41 int d=tree[tree[j].l].sum-tree[tree[i].l].sum; 42 if(l==r) return l; 43 int mid=(l+r)>>1; 44 if(k <= d) return query(tree[i].l, tree[j].l, k, l, mid); //这里是小写的L,mid可不是数字1 45 else return query(tree[i].r, tree[j].r, k - d, mid + 1, r); 46 } 47 int main() 48 { 49 int n,m; 50 scanf("%d%d",&n,&m); 51 for(int i=1;i<=n;++i) 52 { 53 scanf("%d",&w[i].value); 54 w[i].id=i; 55 } 56 sort(w+1,w+1+n,mmp); 57 w[0].value=-1; 58 int j=1; 59 for(int i=1;i<=n;++i) 60 { 61 if(w[i].value!=w[i-1].value) 62 ranks[w[i].id]=j,v[j]=w[i].value,j++; 63 else ranks[w[i].id]=j-1; 64 } 65 init(); 66 for(int i=1;i<=n;++i) 67 { 68 //printf("%d %d\n",ranks[i],v[i]); 69 root[i]=root[i-1]; 70 inserts(ranks[i],root[i],1,n); 71 } 72 while(m--) 73 { 74 int l,r,x; 75 scanf("%d%d%d",&l,&r,&x); 76 printf("%d\n",v[query(root[l-1],root[r],x,1,n)]); 77 } 78 return 0; 79 }
代码2:
1 //洛谷 P3834 可持久化线段树(主席树) 2 3 #include<cstdio> 4 5 #include<cstring> 6 7 #include<algorithm> 8 9 using namespace std; 10 11 const int N=200005; 12 13 int n,m,q,t=0; 14 15 int a[N],b[N],root[N]; 16 17 struct node 18 19 { 20 21 int ls,rs,sum; 22 23 }tree[N*20]; 24 25 void disc() 26 27 { 28 29 int i; 30 31 sort(b+1,b+n+1); 32 33 m=unique(b+1,b+n+1)-(b+1); 34 35 for(i=1;i<=n;++i) 36 37 a[i]=lower_bound(b+1,b+m+1,a[i])-b; 38 39 } 40 41 42 43 //insert函数就是说:当前插入的数p,会影响节点x,所以把x节点的sum加1. 44 45 46 47 //节点x代表一个权值区间,影响x就是说p在节点x所代表的权值区间内。 48 49 //那么先把前一个树的对应区间的节点复制过来,再加1,就行了。 50 51 //可以结合刚才的图感性理解一下。 52 53 void insert(int y,int &x,int l,int r,int p) 54 55 { 56 57 x=++t; //t相当于是一个节点的地址,每个节点是不同的。 58 59 tree[x]=tree[y]; //复制前一个树的对应节点【它们代表的权值区间相同】。 60 61 tree[x].sum++; //给这个节点的sum加1.(这个1指的就是p) 62 63 if(l==r) return; //搜索到根节点就返回。 64 65 int mid=(l+r)>>1; 66 67 68 69 //判断在哪个区间继续插入。 70 71 if(p<=mid) insert(tree[y].ls,tree[x].ls,l,mid,p); 72 73 else insert(tree[y].rs,tree[x].rs,mid+1,r,p); 74 75 } 76 77 78 79 //k是查询第k小 80 81 //x和y相当于是树的节点的地址。而l和r就是这两个节点的权值区间。 82 83 //一开始query(root[l-1],root[r],1,m,k)。 84 85 //root[l-1]就是第l-1颗树的根节点。root[r]就是第r颗树的根节点。 86 87 //比较它们左儿子代表的区间中的数的个数,差值为delta。根据delta判断这两个节点一起往哪个方向跳。 88 89 //分析过程和刚才二分的过程一样。 90 91 int query(int x,int y,int l,int r,int k) 92 93 { 94 95 if(l==r) return l; //查到权值线段树的叶子节点就返回这个值。 96 97 int delta=tree[tree[y].ls].sum-tree[tree[x].ls].sum; 98 99 int mid=(l+r)>>1; 100 101 if(k<=delta) return query(tree[x].ls,tree[y].ls,l,mid,k); 102 103 else return query(tree[x].rs,tree[y].rs,mid+1,r,k-delta); 104 105 } 106 107 int main() 108 109 { 110 111 int l,r,i,k; 112 113 scanf("%d%d",&n,&q); 114 115 for(i=1;i<=n;++i) 116 117 { 118 119 scanf("%d",&a[i]); 120 121 b[i]=a[i]; 122 123 } 124 125 disc(); 126 127 for(i=1;i<=n;++i) 128 129 insert(root[i-1],root[i],1,m,a[i]); 130 131 for(i=1;i<=q;++i) 132 133 { 134 135 scanf("%d%d%d",&l,&r,&k); 136 137 138 139 //query函数返回的是第k小的权值。 140 141 //把这个权值转化为原来这个权值对应的数就行了。 142 143 printf("%d\n",b[query(root[l-1],root[r],1,m,k)]); 144 145 } 146 147 return 0; 148 149 }
参考博客:
https://blog.csdn.net/g21glf/article/details/82986968
https://blog.csdn.net/creatorx/article/details/75446472