权值线段总结
权值线段树就是把线段树的每个点权,赋予一定的含义,比如数字出现的次数,数值前缀出现的次数,并用区间求和维护一个前缀信息,比如数字出现的次数,第K大等(不能实现区间第K大),前缀第K大等。
权值线段树优点:
能够比较容易实现平衡树的一系列操作
一个序列中,插入一个数,删除一个数,求值为数的排名,查询第K小的数,求比这个数小的数,求比这个数大的数。
上述操作都可以通过权值线段树实现。但是需要注意的是,上述操作数的范围如果过大,那么权值线段树将开不下,因为权值线段树存储的是节点的单点信息,也就是从1-N开始的序列,需要把所有操作的值进行存储,并进行离散化。这样权值线段树某种程度上说也就是离线的算法了。
下面对我所做过的权值线段树题目进行总结,提供关键代码
洛谷P3369 【模板】普通平衡树
您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:
1插入x数
2删除x数(若有多个相同的数,因只删除一个)
3查询x数的排名(排名定义为比当前数小的数的个数+1。若有多个相同的数,因输出最小的排名)
4查询排名为x的数
5求x的前驱(前驱定义为小于x,且最大的数)
6求x的后继(后继定义为大于x,且最小的数)
思路:
离散化:
由于这个题目的x范围非常大,需要先把询问记录下来,并进行离散化
查询第K小的数:
这个可以利用权值线段树存储的是数出现的次数,维护区间内数字出现的次数,查询左右子树数字出现的次数,如果左儿子数字出现次数和是小于K,代表这个第K小在右子树,但是它在右子树的排名是K减去左子树儿子的个数,这样不断往下进行查找,当查找到单点的时候,就是第K小。
int Kth(int rt,int k){
int l=tree[rt].l;
int r=tree[rt].r;
if(l==r){
return l;
}
int mid=(l+r)>>1;
if (tree[lson].sum>=k){
return Kth(lson,k);
}else {
return Kth(rson,k-tree[lson].sum);
}
查询x数的排名:
可以利用权值线段树存储的数字个数的信息,查询[1,x-1]区间内部数字出现的个数+1,直接区间查询即可
求x的前驱:
我们可以求在[1,x-1]区间内部数字出现的次数记为k,那么第k大其实就是前面最靠近x的数,也就是前驱
求x的后继:
我们可以求在[1,x]区间内部数字出现的的次数为k,那么第k+1大其实就是后面最靠近x的数,也就是后继。
代码
#include<iostream> #include<stdio.h> #include<string.h> #include<vector> #include<algorithm> #define LL long long #define rep(i,j,k) for(int i=j;i<=k;i++) #define per(i,j,k) for(int i=j;i>=k;i--) #define pb push_back #define lson rt<<1 #define rson rt<<1|1 using namespace std; const int maxx = 100005; struct node{ int l,r,sum; }tree[maxx<<2]; int op[maxx]; int a[maxx]; vector<int>v; int getid(int x){ return lower_bound(v.begin(),v.end(),x)-v.begin()+1; } void buildtree(int rt,int l,int r){ tree[rt].l=l; tree[rt].r=r; tree[rt].sum=0; if (l==r){ return; } int mid=(l+r)>>1; buildtree(lson,l,mid); buildtree(rson,mid+1,r); } void update(int rt,int pos,int w){ int l=tree[rt].l; int r=tree[rt].r; if (l==r){ tree[rt].sum+=w; return; } int mid=(l+r)>>1; if(pos<=mid){ update(lson,pos,w); }else{ update(rson,pos,w); } tree[rt].sum=tree[lson].sum+tree[rson].sum; } int query(int rt,int ql,int qr){ if(ql>qr)return 0; int l=tree[rt].l; int r=tree[rt].r; if(ql<=l && r<=qr){ return tree[rt].sum; } int mid=(l+r)>>1; if (qr<=mid){ return query(lson,ql,qr); }else if (ql>mid){ return query(rson,ql,qr); }else { return query(lson,ql,mid)+query(rson,mid+1,qr); } } int Kth(int rt,int k){ int l=tree[rt].l; int r=tree[rt].r; if(l==r){ return l; } int mid=(l+r)>>1; if (tree[lson].sum>=k){ return Kth(lson,k); }else { return Kth(rson,k-tree[lson].sum); } } int main(){ int t; int x; scanf("%d",&t); rep(i,1,t){ scanf("%d%d",&op[i],&a[i]); if(op[i]!=4) v.pb(a[i]); } sort(v.begin(),v.end()); v.erase(unique(v.begin(),v.end()),v.end()); int tot=v.size(); buildtree(1,1,tot); rep(i,1,t){ if(op[i]==1){ update(1,getid(a[i]),1); }else if (op[i]==2){ update(1,getid(a[i]),-1); }else if (op[i]==3){ printf("%d ",query(1,1,getid(a[i])-1)+1); }else if (op[i]==4){ printf("%d ",v[Kth(1,a[i])-1]); }else if (op[i]==5){ int pre=query(1,1,getid(a[i])-1); printf("%d ",v[Kth(1,pre)-1]); }else{ int bac=query(1,1,getid(a[i])); printf("%d ",v[Kth(1,bac+1)-1]); } } return 0; }
HDU – 1394
给你一个序列,你可以循环左移,问最小的逆序对是多少???
逆序对其实是寻找比这个数小的数字有多少个,这个问题其实正是权值线段树所要解决的
我们把权值线段树的单点作为1-N的数中每个数出现的次数,并维护区间和,然后从1-N的数,在每个位置,查询比这个数小的数字的个数,这就是当前位置的逆序对,然后把当前位置数的出现的次数+1,就能得到答案。
然后我们考虑循环右移。我们每次循环右移,相当于把序列最左边的数字给放到最右边,而位于序列最左边的数字,它对答案的功效仅仅是这个数字大小a[i]-1,因为比这个数字小的数字全部都在它的后面,并且这个数字放到最后了,它对答案的贡献是N-a[i],因为比这个数字大数字全部都在这个数字的前面,所以每当左移一位,对答案的贡献其实就是
Ans=Ans-(a[i]-1)+n-a[i]
由于数字从0开始,我们建树从1开始,我们把所有数字+1即可
#include<iostream> #include<string.h> #include<algorithm> #include<stdio.h> using namespace std; const int maxx = 5005; int tree[maxx<<2]; inline int L(int root){return root<<1;}; inline int R(int root){return root<<1|1;}; inline int MID(int l,int r){return (l+r)>>1;}; int a[maxx]; void update(int root,int l,int r,int pos){ if (l==r){ tree[root]++; return; } int mid=MID(l,r); if (pos<=mid){ update(L(root),l,mid,pos); }else { update(R(root),mid+1,r,pos); } tree[root]=tree[L(root)]+tree[R(root)]; } int query(int root,int l,int r,int ql,int qr){ if (ql<=l && r<=qr){ return tree[root]; } int mid=MID(l,r); if (qr<=mid){ return query(L(root),l,mid,ql,qr); }else if (ql>mid){ return query(R(root),mid+1,r,ql,qr); }else { return query(L(root),l,mid,ql,qr)+query(R(root),mid+1,r,ql,qr); } } int main(){ int n; while(~scanf("%d",&n)){ int ans=0; memset(tree,0,sizeof(tree)); for (int i=1;i<=n;i++){ scanf("%d",&a[i]); a[i]++; ans+=query(1,1,n,a[i],n); update(1,1,n,a[i]); } int minn=ans; for (int i=1;i<=n;i++){ ans=ans+(n-a[i]+1)-a[i]; minn=min(ans,minn); } printf("%d ",minn); } return 0; }
POJ – 2104
给你一个序列,这个序列代表原来序列的[1,i]位置的 前缀逆序对数目,原序列是什么
对于逆序对问题,我们应该从边界开始入手,考虑最右边,我们我们可以通过a[n]-a[n-1]
算到n位置的逆序对,也就是前面比当前位置的数字大的个数,那么这个位置的数一定是
n-a[n]-a[n-1].
我们思考一下可不可以推广呢?其实是没问题的
我们考虑从后往前,我们容易知道权值线段树可以查询第K小
那么初始化全部的权值线段树的单点全部为1
那么从后往前,我们查询前i位置的第i-a[i]-a[i-1]小,那么这个数就是当前位置的数
并把权值线段树的这个数的出现的次数变为0,这样就消除了,后面已经出现过的数,前面的第xx小的影响。
#include<iostream> #include<stdio.h> #include<string.h> #include<algorithm> #include<vector> using namespace std; const int maxx = 50005; int a[maxx]; int ans[maxx]; struct node{ int l,r,w; }tree[maxx<<2]; inline int L(int root){return root<<1;}; inline int R(int root){return root<<1|1;}; inline int MID(int l,int r){return (l+r)>>1;}; void buildtree(int root,int l,int r){ tree[root].l=l; tree[root].r=r; if (l==r){ tree[root].w=1; return; } int mid=MID(l,r); buildtree(L(root),l,mid); buildtree(R(root),mid+1,r); tree[root].w=tree[L(root)].w+tree[R(root)].w; } void update(int root,int pos){ int l=tree[root].l; int r=tree[root].r; if (l==r){ tree[root].w=0; return; } int mid=MID(l,r); if (pos<=mid){ update(L(root),pos); }else { update(R(root),pos); } tree[root].w=tree[L(root)].w+tree[R(root)].w; } int query(int root,int ql,int qr,int k){ int l=tree[root].l; int r=tree[root].r; if(l==r){ return l; }; int mid=MID(l,r); if (tree[L(root)].w>=k){ return query(L(root),ql,qr,k); }else { return query(R(root),ql,qr,k-tree[L(root)].w); } } int main(){ int t; scanf("%d",&t); int n; while(t--){ scanf("%d",&n); for(int i=1;i<=n;i++){ scanf("%d",&a[i]); } buildtree(1,1,n); for (int i=n;i>=1;i--){ int tmp=a[i]-a[i-1]; ans[i]=query(1,1,n,i-tmp); update(1,ans[i]); } for(int i=1;i<=n;i++){ if (i-1)printf(" %d",ans[i]); else printf("%d",ans[i]); } printf(" "); } return 0; }
BZOJ1503: [NOI2004]郁闷的出纳员
I命令 I_k 新建一个工资档案,初始工资为k。
如果某员工的初始工资低于工资下界,他将立刻离开公司。
A命令 A_k 把每位员工的工资加上k
S命令 S_k 把每位员工的工资扣除k
F命令 F_k 查询第k多的工资
注意:员工发现自己的工资已经低于了合同规定的工资下界,他就会立刻离开
对于操作F,如何人数不够,输出-1,最后输出离开的总人数
I命令的条数不超过100000
A命令和S命令的总条数不超过100
F命令的条数不超过100000
每次工资调整的调整量不超过1000
新员工的工资不超过100000
询问强制在线
我们可以考虑建立权值线段树,我们记录一个add代表涨工资的量,nowmin代表现在的最低工资。
考虑命令I,我们需要知道初始工资下界是最开始的nowmin(因为它没有享受到涨工资)
所为了消除涨工资的影响,初始工作k满足k+add>=nowmin
考虑命令A,记录一个add+=w,nowmin-=w(当前每个人最低工资下界将降低)我们暂时记录这个值,不做更新。因为肯定没有人从这个操作中会因为工资太低而走人
考虑命令S,我们更新add-=w,nowmin+=w(最低工资下界上升),那么可能会有人从中被淘汰。我们对权值线段树的[1,nowmin]区间修改全部赋值为0并打上laze标记
考虑命令F,直接查询第K大即可。
由于有可能最低工资下界会变为负数,用动态开点就不用担心了
代码
#include<iostream> #include<stdio.h> #include<algorithm> #include<string.h> #define rep(i,j,k) for(int i=j;i<=k;i++) #define per(i,j,k) for(int i=j;i>=k;i--) using namespace std; const int maxx = 2e5+50; int lson[maxx*18],rson[maxx*18],sum[maxx*18]; int tot=1,root=1; bool laze[maxx*18]; void push_down(int x){ if(laze[x]){ if(!lson[x])lson[x]=++tot; if(!rson[x])rson[x]=++tot; sum[lson[x]]=0; sum[rson[x]]=0; laze[lson[x]]=1; laze[rson[x]]=1; laze[x]=0; } } void inserts(int l,int r,int x,int &p){ if(!p)p=++tot; if (l==r){ sum[p]++; return; } int mid=(l+r)>>1; push_down(p); if (x<=mid){ inserts(l,mid,x,lson[p]); }else { inserts(mid+1,r,x,rson[p]); } sum[p]=sum[lson[p]]+sum[rson[p]]; } void update(int l,int r,int ul,int ur,int &p){ if (!p)p=++tot; if (ul<=l && r<=ur){ sum[p]=0; laze[p]=1; return; } push_down(p); int mid=(l+r)>>1; if(ul<=mid)update(l,mid,ul,ur,lson[p]); if(ur>mid)update(mid+1,r,ul,ur,rson[p]); sum[p]=sum[lson[p]]+sum[rson[p]]; } int kth(int l,int r,int k,int &p){ if(l==r){ return l; } int mid=(l+r)>>1; push_down(p); if(k<=sum[lson[p]]){ return kth(l,mid,k,lson[p]); }else { return kth(mid+1,r,k-sum[lson[p]],rson[p]); } } int main(){ int n,m,w; int add=0,nowmin,num=0; char op[10]; while(~scanf("%d%d",&n,&nowmin)){ rep(i,1,n){ scanf("%s%d",op,&w); if (op[0]=='I'){ //减去影响后如果起薪小于工资下界 if (w-add<nowmin)continue; //他的工资实际上应该是他的起 inserts(-maxx,maxx,w-add,root); num++; }else if(op[0]=='A'){ //偏移应该加上 add+=w; //同时此时要求最低的工资降低 nowmin-=w; }else if(op[0]=='S'){ add-=w; nowmin+=w; //比最低工资低的值全部赋值为0 update(-maxx,maxx,-maxx,nowmin-1,root); }else { //如果所有人被开除 if(w>sum[1]){ printf("-1 "); continue; }else { //查询剩下的第K大,并且要加上偏移 //其实等价于查询当前第K大=总数-第K小+1 printf("%d ",kth(-maxx,maxx,sum[1]-w+1,root)+add); } } } printf("%d ",num-sum[1]); } return 0; }
2019牛客多校第七场 C Governing sand 权值线段树
题意:
有一个树林,树林中不同种类的树有不同的数量,高度,砍伐它们的价格。现在要求砍掉一些树,使得高度最高的树占剩下的树的总数的一半以上,求最小花费
首先我们应该从低到高枚举每一种高度的树,这里需要做一个离散化。因为我们需要先求出每一种高度树中,比他高的树的数目和所需要消耗的花费,为了方便求前缀和,以及把相同高度和不同花费的树储存在一起,我们把每个高度小于等于高度的树的数目和砍掉的花费的前缀求出来。这样我们能够O(1)的求出每个高度对应所需要砍掉比这个树高度更高的树的数目和消费,以及比这个高度小的树的数目,以及当前高度树的数目。
我们可以很方便的知道我们需要砍伐数目最小需要多少,我们暂时设定需要K个。
那么我们需要寻找的是比这个树高度小的树中,前K小消费的和。想到这里,权值线段树已经呼之欲出了。
我们离散化所有的消耗,按照消耗进行建立权值线段树,线段树维护的是砍单个树消耗为某个值时,出现的次数,以及单点消耗的总和=单个树消耗*次数,并求区间和,维护区间信息。
我们需要查询前K小的和。当每次枚举一个高度以后,把所有这个高度的树,加入权值线段树中,更新线段树。这样问题就解决了。
询问前k小
LL query(int root,int l,int r,LL k){
if (l==r)return vcost[l-1]*k;//单点就直接算
if (tree[root].cnt==k)return tree[root].w;//如果内部个数正好为K个直接返回w
int mid=MID(l,r);
if (tree[L(root)].cnt>=k){
return query(L(root),l,mid,k);
}else {
//需要返回左子树的和同时查询右子树
return tree[L(root)].w+query(R(root),mid+1,r,k-tree[L(root)].cnt);
}
}
代码
#include<iostream> #include<string.h> #include<algorithm> #include<stdio.h> #include<vector> #define LL long long #define pii pari<int,int> #define pb push_back #define rep(i,j,k) for(int i=j;i<=k;i++) #define per(i,j,k) for(int i=j;i>=k;i--) using namespace std; const int maxx = 1e5+6; inline int L(int root){return root<<1;}; inline int R(int root){return root<<1|1;}; inline int MID(int l,int r){return (l+r)>>1;}; vector<LL>vh; vector<LL>vcost; struct node{ int l,r; LL cnt; LL w; }tree[maxx<<2]; struct Node{ LL h,cost,num; }a[maxx]; LL sumc[maxx]; LL sumn[maxx]; int gethight(LL x){ return lower_bound(vh.begin(),vh.end(),x)-vh.begin()+1; } int getcost(int x){ return lower_bound(vcost.begin(),vcost.end(),x)-vcost.begin()+1; } void update(int root,int l,int r,int pos,LL cnt,LL val){ if (l==r){ tree[root].cnt+=cnt; tree[root].w+=cnt*val; return ; } int mid=MID(l,r); if (pos<=mid){ update(L(root),l,mid,pos,cnt,val); }else { update(R(root),mid+1,r,pos,cnt,val); } tree[root].cnt=tree[L(root)].cnt+tree[R(root)].cnt; tree[root].w=tree[L(root)].w+tree[R(root)].w; } LL query(int root,int l,int r,LL k){ if (l==r)return vcost[l-1]*k; if (tree[root].cnt==k)return tree[root].w; int mid=MID(l,r); if (tree[L(root)].cnt>=k){ return query(L(root),l,mid,k); }else { return tree[L(root)].w+query(R(root),mid+1,r,k-tree[L(root)].cnt); } } bool cmp(Node a,Node b){ if (a.h==b.h){ if (a.cost==b.cost){ return a.num>b.num; } return a.cost<b.cost; } return a.h<b.h; } int main(){ int n; while(~scanf("%d",&n)){ vh.clear(); vcost.clear(); memset(sumc,0,sizeof(sumc)); memset(sumn,0,sizeof(sumn)); memset(tree,0,sizeof(tree)); rep(i,1,n){ scanf("%lld%lld%lld",&a[i].h,&a[i].cost,&a[i].num); vh.pb(a[i].h); vcost.pb(a[i].cost); } sort(vh.begin(),vh.end()); sort(vcost.begin(),vcost.end()); vh.erase(unique(vh.begin(),vh.end()),vh.end()); vcost.erase(unique(vcost.begin(),vcost.end()),vcost.end()); sort(a+1,a+1+n,cmp); rep(i,1,n){ a[i].h=gethight(a[i].h); sumc[a[i].h]+=(LL)a[i].num*a[i].cost; sumn[a[i].h]+=a[i].num; } int tot=vh.size(); rep(i,1,tot){ sumc[i]+=sumc[i-1]; sumn[i]+=sumn[i-1]; } LL cost,ans=1e18; int pos=1,j; rep(i,1,tot){ cost=sumc[tot]-sumc[i]; LL num=sumn[i]-sumn[i-1]; // cout<<"..."<<cost<<"..."<<num<<"..."<<sumn[i-1]<<endl; if(num>sumn[i-1]){ ans=min(ans,cost); }else { cost+=query(1,1,tot,sumn[i-1]-num+1); ans=min(ans,cost); } for (j=pos;j<=n && a[j].h==a[pos].h;j++){ update(1,1,tot,getcost(a[j].cost),a[j].num,a[j].cost); } pos=j; } printf("%lld ",ans); } return 0; }