学习笔记:权值线段树
虽然题解很多,也有权值线段树,但我的和他们似乎不尽相同,跑的也挺快。
所谓权值线段树,就是用线段树来存储权值。
那什么是权值呢?似乎小学初中学统计的时候了解到,他是描述数在数据中比例大小的量,这里用作此数出现的次数。
建树
做法显然。
我们用\(cnt_i\)表示第\(i\)个数出现的次数,那么可以这样:
void update(int k){a[k].sum=a[k<<1].sum+a[k<<1|1].sum;}
void build(int k,int l,int r)
{
a[k].l=l,a[k].r=r;
int mid=(l+r)>>1;
if(l==r)
{
a[k].sum=cnt[l];//赋值
return;
}
build(k<<1,l,mid),build(k<<1|1,mid+1,r);//向下递归
update(k);//动态更新点区间和
}
之后
你是不是还在疑问这是在回答什么问题?
对这就是询问所有数中第\(x\)小值。
分析
如何运用权值线段树呢?
很简单,我们每次查询下左儿子的权值和\(sum\),如果\(x\geqslant sum\),就向右儿子递归询问第\(x-sum\)小,否则向左儿子递归找第\(x\)值就好了。
有没有发现纰漏?
这个数组必须有单调性。
那就排序啊......
反正排序是\(O(n\log n)\)的,线段树也是\(O(n\log n)\)的,不会成为瓶颈。
这里先不给代码了,下面再说。
其他的东西
我们想到线段树是单调数组,而使用是非常不便。
这就需要一种给力的映射关系。
其实就是离散化了。
这里就有代码了:
struct Node
{
int val,id;
}t[MAXN];
bool cmp(Node n,Node m)
{
return n.val<m.val;
}
int b[MAXN],num[MAXN],cnt[MAXN],c=1;
void hash(int n)
{
sort(t+1,t+n+1,cmp);//结构体排序
for(int i=1;i<=n;i++)
{
if(num[c]!=t[i].val) b[t[i].id]=++c,num[c]=t[i].val;//如果和上一个不同
else b[t[i].id]=c;
cnt[c]++;
}
}
慢慢来,比较绕。
\(b[]\)是原数组到离散化数组的映射,而\(num[]\)反之。
举个栗子:
数据:\(5,12,-6,7,5\)中,
\(b[1](t[1].val=5)=2,b[2](t[2].val=12)=4\);
\(ps\):这里给出的下标是未排序的。
\(num[1]=-6,num[3]=7\);
\(cnt[2]=2(num[2]=num[3]=5)\)。
回到上题
这样就可以求第\(x\)小值了。
用上面的思想解决:
int query(int k,int x)
{
if(a[k].l==a[k].r) return num[a[k].l];//找到了
else if(a[k<<1].sum>=x) return query(k<<1,x);//在左儿子中
else return query(k<<1|1,x-a[k<<1].sum); //在右儿子中,记得减一下
}
题解
你基本已经学会了权值线段树了,现在来看这个题:
题目链接:P1801 黑匣子
考虑权值线段树,加点并不好实现,不如强制离线,倒着试试。
删除一个数就十分简单了,我们只需用原数到离散化数组的映射关系,将此数对应的\(cnt--\)就好了,把答案存一下,逆序输出来即可。
下面是简单的单点修改:
void modify(int k,int x,int y)//现在在k点,目标是将x号搞成y
{
int mid=(a[k].l+a[k].r)>>1;
if(a[k].l==a[k].r)//找到目标
{
a[k].sum=y;
return;
}
else if(x<=mid) modify(k<<1,x,y);
else modify(k<<1|1,x,y);
update(k);//仍然记得更新
}
总的说,这样的复杂度是\(O((n+m)\log n)\)的,可以通过此题。
下面放上\(AC\)代码:
\(Code\):
#include<iostream>
#include<algorithm>
#include<cstdio>
using namespace std;
const int MAXN=2e5+5;
struct Node
{
int val,id;
}t[MAXN];
bool cmp(Node n,Node m)
{
return n.val<m.val;
}
int b[MAXN],num[MAXN],cnt[MAXN],c=1;//num是hash数组到原数组的映射。
/*num[2]=12,指原数组第二大的值为12*/
void hash(int n)
{
sort(t+1,t+n+1,cmp);
for(int i=1;i<=n;i++)
{
if(num[c]!=t[i].val) b[t[i].id]=++c,num[c]=t[i].val;
else b[t[i].id]=c;
cnt[c]++;
}
}
struct node
{
int l,r,sum,val;
node()
{
l=r=sum=0;
}
}a[MAXN<<2];
void update(int k){a[k].sum=a[k<<1].sum+a[k<<1|1].sum;}
void build(int k,int l,int r)
{
a[k].l=l,a[k].r=r;
int mid=(l+r)>>1;
if(l==r)
{
a[k].sum=cnt[l];
return;
}
build(k<<1,l,mid),build(k<<1|1,mid+1,r);
update(k);
}
int query(int k,int x)
{
if(a[k].l==a[k].r) return num[a[k].l];
else if(a[k<<1].sum>=x) return query(k<<1,x);
else return query(k<<1|1,x-a[k<<1].sum);
}
void modify(int k,int x,int y)
{
int mid=(a[k].l+a[k].r)>>1;
if(a[k].l==a[k].r)
{
a[k].sum=y;
return;
}
else if(x<=mid) modify(k<<1,x,y);
else modify(k<<1|1,x,y);
update(k);
}
int n,m,l[MAXN],now,ans[MAXN];
int main()
{
scanf("%d%d",&n,&m);
now=m;
for(int i=1;i<=n;i++) scanf("%d",&t[i].val),t[i].id=i;
for(int i=1;i<=m;i++) scanf("%d",&l[i]);
hash(n);
build(1,1,c);
for(int i=n;i>=1;i--)
{
while(i==l[now]) ans[now]=query(1,now),now--;//此时有询问,查询第now小值,并更新now
,modify(1,b[i],--cnt[b[i]]);//将数对应的点权值减一
}
for(int i=1;i<=m;i++) printf("%d\n",ans[i]);//将存储的答案输出
return 0;
}
这个版本的权值线段树会跑\(900\;ms\),翻翻记录,还挺快呢。