权值线段树(+动态开点)
咕咕咕这只鸽子在学主席树的时候终于想起来要更权值线段树学习笔记了
先来看道题:
给你一个序列和一些操作:
操作 \(1\):询问序列第k小,保证有答案。
操作 \(2\):修改某个数
这题首先考虑暴力:
暴力 \(1\)
对于操作 \(1\),拷贝一份,然后排序,第 \(k\) 个数就是第 \(k\) 小。
对于操作 \(2\),直接暴力修改。
时间复杂度太大,貌似没有优化的余地,直接舍去。
暴力 \(2\)
建一个桶 \(b\),\(b_i\) 表示值为 \(i\) 的数的个数。
对于操作 \(1\),可以直接从 \(1\) 向后枚举,同时用一个变量 \(x\) 把 \(b_i\) 累加起来。直到 \(x \ge k\),这个 \(i\) 就是答案。
但是值域可能很大,所以需要对原数组离散化。
小优化:对 \(b\) 数组求个前缀和,然后就可以二分查询答案。
但是,这有个弊端,就是修改之后整个前缀和数组可能都要变过。
权值线段树
该我们的主角登场啦~
我们思考对于暴力 \(2\),能不能搞出来一个数据结构,可以使他不用前缀和又能快速查询呢?
快速的话,比如复杂度是 \(O(log)\)的?
诶?线段树可以吗?
当然可以。我们把上面那个桶改成线段树,这就是权值线段树。它与普通线段树不同的地方就在于,权值线段树是以权值为下标,而普通线段树这是以位置为下标。
然后我们考虑维护这个线段树,除了每个节点维护的区间 \([l,r]\),还有值在这个范围内的数的数量。
接下来考虑查询。我们增加一个参数表示这个点维护的区间的右侧,即 \([1,l-1]\) 中有多少数字。然后二分。
若前面的数字 \(+mid \le k\),,则递归左儿子,否则递归右儿子。当到达叶子结点时,就得到了答案。
接下来考虑修改,这就更容易了。我们同时记录原数组,然后先把 \(a_i\) 这个值在线段树中 \(-1\),修改后的值的位置 \(+1\),再把 \(a_i\) 改掉即可。
由于这是我临时随口造的一道题,所以就没有代码 awa。
例题
诶诶诶先别急着归并排序啊,用权值线段树试试嘛~ 喂(#`O′),别走啊~
上面这段话请不要在意
咳咳,让我们来看下这道题。我们用权值线段树解决,就能做到我们梦寐以求的事:枚举每个数,找到它前面比它大的数。
具体咋做?对于读入的每个数,先查询权值线段树中比它大的数,也就是递归时记录右侧,即值 \(>r\) 的数的范围。然后嘞,如果 \(mid\)(相当于这个区间内的数的中位数吧?)比当前查询的数大,那么自然是要走左儿子的。而右儿子呢?仔细考虑一下就能发现,无论如何右儿子都要走。查询完后,把这个数加入权值线段树就行了。
但是但是,相信已经有些人发现问题了:权值太大了。怎么办?
两种办法。
- 离散化,不做过多解释
- 动态开点,接下来重点介绍。
什么叫动态开点嘞?就是说,在需要的时候开一个点,不需要就不开。
所以,这种情况下不能用堆式存储法了,而是要记录左右儿子。
我们注意到,数只有 \(n\) 个,但值域却很大,我们能不能不用其他没用的点呢?可以。
我们在建树时,先只建一个根,待有需要了再建其他点:
int build(int ll,int rr)
{
tot++;
w[tot]=0;
ls[tot]=rs[tot]=0;
l[tot]=ll,r[tot]=rr;
return tot;
}
//tot表示点的个数,w是点权,ls,rs分别表示左右儿子,l,r表示范围。
很简单吧?然后是单点修改。这里就有一点区别了。在加入值的时候,这个节点可能是没有的。怎么办?建一个呗。
void change(int k,int x)
{
if(l[k]==r[k])
{
w[k]++;
return ;
}
int mid=(l[k]+r[k])/2;
if(x<=mid)
{
if(ls[k]==0) ls[k]=build(l[k],mid);//这就是特判没有建这个点的情况。
change(ls[k],x);
}
else
{
if(rs[k]==0) rs[k]=build(mid+1,r[k]);//特判,同上。
change(rs[k],x);
}
w[k]=w[ls[k]]+w[rs[k]];//记得更新
return ;
}
最后就是查询了,这个也比较简单。
int ask(int k,int x)
{
if(l[k]>=x) return w[k];
int mid=(l[k]+r[k])/2,res=0;
if(x<=mid)
if(ls[k]!=0) res+=ask(ls[k],x);//这里也要特判一下没有点的情况哦。不过不用建点,因为是查询,既然没有点,那答案当然就是0了。
if(rs[k]!=0) res+=ask(rs[k],x);//上面说过了,右子树是无论如何都要递归的,除非没有点。
return res;
}
然后,整个操作就完成了。最后给大家看下主程序:
int main()
{
scanf("%d",&n);
tr.build(0,1000000000);//这里是最开始建一个根
for(int i=1;i<=n;i++)
{
int x;
scanf("%d",&x);
ans+=tr.ask(1,x+1);//先查询
tr.change(1,x);//再加入
}
printf("%lld\n",ans);//最后输出~
return 0;
}
于是,这题就完成了!