……

学习笔记:权值线段树

虽然题解很多,也有权值线段树,但我的和他们似乎不尽相同,跑的也挺快。
所谓权值线段树,就是用线段树来存储权值。
那什么是权值呢?似乎小学初中学统计的时候了解到,他是描述数在数据中比例大小的量,这里用作此数出现的次数

建树

做法显然
我们用\(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\),翻翻记录,还挺快呢。

posted @ 2020-02-23 21:35  童话镇里的星河  阅读(261)  评论(2编辑  收藏  举报