权值线段树与动态开点

前置知识:线段树

1.0 权值线段树

我们都知道,普通线段树是一种用来维护区间最值的数据结构。

而权值线段树,就是将序列各值出现的频次作为权值,再用线段树来维护值域的数据结构。

与其说是一种数据结构,不如说是线段树的一点技巧。

实际代码的话,用线段树维护桶数组就行了。

权值线段树重要作用是反映序列中元素大小,
如求第k大第k小问题

因为本身与普通线段树差不多,所以就借用模板了((

code:

#include <bits/stdc++.h>
using namespace std;

const int N=3e5+10;

int n,k_;
int bucket_[N];

int tree[N<<1];

void push_up(int node)
{
    tree[node]=tree[node<<1]+tree[node<<1|1];
}

void build(int node,int start,int end)
{
    if(start==end)
    {
        tree[node]=bucket_[start];
        return ;
    }
    int mid=start+end>>1;
    int lnode=node<<1;
    int rnode=node<<1|1;
    build(lnode,start,mid);
    build(rnode,mid+1,end);
    push_up(node);
}

void update(int node,int start,int end,int k,int val)//大小为val的数多k个,相当于单点修改
{
    if(start==end)
    {
        tree[node]+=k;
        return ;
    }
    int mid=start+end>>1;
    int lnode=node<<1;
    int rnode=node<<1|1;
    if(val<=mid) update(lnode,start,mid,k,val);
    else update(rnode,mid+1,end,k,val);

    push_up(node);
}

int query(int node,int start,int end,int val)//查询数字val有多少个,相当于单点查询
{
    if(start==end) return tree[node];
    int mid=start+end>>1;
    int lnode=node<<1;
    int rnode=node<<1|1;
    if(val<=mid) return query(lnode,start,mid,val);
    else return query(rnode,mid+1,end,val);
    push_up(node);
}

int query_kth(int node,int start,int end,int k)//查询第k小
{
    if(start==end) return start;
    int mid=start+end>>1;
    int lnode=node<<1;
    int rnode=node<<1|1;
    if(tree[lnode]>=k) return query_kth(lnode,start,mid,k);//如果左子树的权值大于k,证明第k小值左子树
    else return query_kth(rnode,mid+1,end,k-tree[lnode]);//进入右子树时,整个区间的第k小相当于右区间的第(k-左区间)小,记得减去左子树的值
}

int query_kthbig(int node,int start,int end,int k)//查询第k大
{
    if(start==end) return start;
    int mid=start+end>>1;
    int lnode=node<<1;
    int rnode=node<<1|1;
    if(tree[rnode]>=k) return query_kthbig(rnode,mid+1,end,k);//若右子树的权值大于k,证明第k大值在右子树
    else return query_kthbig(lnode,start,mid,k-tree[rnode]);//进入左子树时,记得减去右区间
}

int main()
{
    scanf("%d%d",&n,&k_);
    int maxn=0,tot=0;
    for(int i=1; i<=n; i++)
    {
        int x;
        scanf("%d",&x);
        maxn=maxn>=x?maxn:x;
        bucket_[x]++;
    }
    build(1,1,maxn);
    cout<<query_kth(1,1,maxn,k_);
    return 0;
}

1.1 板子题

第 k 小整数

由于重复的不算,所以每个桶只统计1就行了

1.2 例题

逆序对

值域\(10^9\),显然不是数组能直接开下的。

发现 \(n\) 的大小才 \(5\times10^5\),可以考虑离散化。

离散化是常用的缩小值域的方法,好写好用。

缺点也很显著:只能离线。

完成离散化后,就可以建立一棵空的权值线段树了。

按顺序遍历离散化后数组,每遍历一个值 \(x\) ,先在线段树中查询有多少大于 \(x\) 的数,即为当前点的答案,然后插入线段树中。

code:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

const int N=5e5+10;

struct Num
{
    int no__,val__;
} arr[N];//用结构体的方式存原数组,方便离散化

bool cmp(Num x,Num y)
{
    if(x.val__==y.val__) return x.no__<y.no__;
    return x.val__<y.val__;
}

int n;
int poi[N];//离散化之后的数组
ll tree[N<<2];

void push_up(int node)
{
    tree[node]=tree[node<<1]+tree[node<<1|1];
}
void build(int node,int start,int end)
{
    if(start==end)
    {
        tree[node]=0;//先建空树
        return ;
    }
    int mid=start+end>>1;
    int lnode=node<<1;
    int rnode=node<<1|1;
    build(lnode,start,mid);
    build(rnode,mid+1,end);

    push_up(node);

}

void update(int node,int start,int end,int k,int val)//单点修改
{
    if(start==end)
    {
        tree[node]=(ll)tree[node]+k;
        return;
    }
    int mid=start+end>>1;
    int lnode=node<<1;
    int rnode=node<<1|1;
    if(val<=mid) update(lnode,start,mid,k,val);
    else update(rnode,mid+1,end,k,val);

    push_up(node);
}

ll query(int node,int start,int end,int l,int r)//查询值为[l,r]之间数的出现次数和
{
    if(end<l||start>r) return 0;
    if(l<=start&&end<=r) return tree[node];

    ll sum=0;
    int mid=start+end>>1;
    int lnode=node<<1;
    int rnode=node<<1|1;
    if(l<=mid) sum+=query(lnode,start,mid,l,r);
    if(r>mid) sum+=query(rnode,mid+1,end,l,r);

    push_up(node);
    return sum;
}

int main()
{
    scanf("%d",&n);
    for(int i=1; i<=n; i++)
    {
        scanf("%d",&arr[i].val__);
        arr[i].no__=i;
    }

    sort(arr+1,arr+1+n,cmp);
    for(int i=1; i<=n; i++)
        poi[arr[i].no__]=i;//离散化

    ll ans=0;
    for(int i=1; i<=n; i++)
    {
        int x=poi[i];
        ans+=query(1,1,n,x+1,n);
        update(1,1,n,1,x);
    }
    printf("%lld",ans);
    return 0;
}

2.0 动态开点

刚刚我们说,如果 \(x\) 值域跨越范围极大,则可以用权值线段树+离散化

那如果题目不让你离线怎么办呢

于是就有了动态开点

空间复杂度MlogN,其中M表操作次数,N表值域

动态开点,正如其名,就是用多少点我们就开多少点。

初始建树时仅有根节点,然后各节点逐个插入。

具体的,当我们的修改操作修改到不存在的点时,我们就新建一个节点

动态开点线段树中,存储必须使用结构体形式(因为动态开点的点下标是认为设定的,且不按满二叉树的方式存储)。同时在新增节点时要更新父亲节点的子结点(即当前点)下标,所以传编号要传引用

动态开点的修改及查询操作:

#define lnode tree[node].lson
#define rnode tree[node].rson

void update(int &node,int start,int end,int k)//node是人为规定的编号,且涉及修改,所以传引用·
{
    if(!node)//新建节点
    {
        node=++tot;
        lnode=rnode=tree[node].sum=0;//这个点的左右儿子都是不存在的,赋为0
    }
    if(start==end)
    {
        tree[node].sum++;
        return ;
    }
    int mid=start+end>>1;
    if(k<=mid) update(lnode,start,mid,k);
    else update(rnode,mid+1,end,k);

    push_up(node);
}

ll query(int node,int start,int end,int l,int r)
{
    if(!node) return 0;//这个节点未被创建, 返回0
    if(l<=start&&end<=r) return tree[node].sum;

    ll sum=0;
    int mid=start+end>>1;
    if(l<=mid) sum+=query(lnode,start,mid,l,r);
    if(r>mid) sum+=query(rnode,mid+1,end,l,r);

    return sum;
}

整体框架依旧是普通线段树。

求逆序对(动态开点权值线段树)code:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

const int N=5e5+10;
const int MAX=1e9;

int n;
struct node
{
    ll sum;
    int lson,rson;
} tree[N<<5];
int tot=0;//计数变量, 记录开了多少节点

#define lnode tree[node].lson
#define rnode tree[node].rson

void push_up(int node)
{
    tree[node].sum=tree[lnode].sum+tree[rnode].sum;
}

void update(int &node,int start,int end,int k)//node是人为规定的编号,所以传引用·
{
    if(!node)//新建节点
    {
        node=++tot;
        lnode=rnode=tree[node].sum=0;
    }
    if(start==end)
    {
        tree[node].sum++;
        return ;
    }
    int mid=start+end>>1;
    if(k<=mid) update(lnode,start,mid,k);
    else update(rnode,mid+1,end,k);

    push_up(node);
}

ll query(int node,int start,int end,int l,int r)
{
    if(!node) return 0;//这个节点未被创建, 返回0
    if(l<=start&&end<=r) return tree[node].sum;

    ll sum=0;
    int mid=start+end>>1;
    if(l<=mid) sum+=query(lnode,start,mid,l,r);
    if(r>mid) sum+=query(rnode,mid+1,end,l,r);

    return sum;
}

int main()
{
    scanf("%d",&n);
    ll ans=0;
    int root=1;
    tot=1;
    for(int i=1; i<=n; i++)
    {
        int x;
        scanf("%d",&x);
        if(x+1<=MAX) ans+=query(1,1,MAX,x+1,MAX);
        update(root,1,MAX,x);
    }
    printf("%lld\n",ans);
    return 0;
}

71行

这玩意儿还能做到平衡树做的事情

虽然前驱后继用了map解决

P3369 普通平衡树

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

const int N=1e5+10;
const int MAX=1e7;

int n;
struct node
{
	ll sum;
	int lson,rson;
} tree[N<<4];
int node_tot=1;

#define lnode tree[node].lson
#define rnode tree[node].rson

inline void push_up(int node)
{
	tree[node].sum=tree[lnode].sum+tree[rnode].sum;
}

void update(int &node,int start,int end,int val,int k)
{
	if(!node)
	{
		node=++node_tot;
		lnode=rnode=tree[node].sum=0;
	}
	if(start==end)
	{
		tree[node].sum+=k;
		if(tree[node].sum<0) tree[node].sum=0;
		return ;
	}
	int mid=start+end>>1;
	if(val<=mid) update(lnode,start,mid,val,k);
	else update(rnode,mid+1,end,val,k);

	push_up(node);
}

ll query(int node,int start,int end,int l,int r)
{
	if(!node) return 0;
	if(l<=start&&end<=r) return tree[node].sum;

	int mid=start+end>>1;
	ll ans=0;
	if(l<=mid) ans+=query(lnode,start,mid,l,r);
	if(r>mid) ans+=query(rnode,mid+1,end,l,r);

	push_up(node);
	return ans;
}

int kth(int node,int start,int end,int k)
{
	if(start==end) return start;
	int mid=start+end>>1;
	if(tree[lnode].sum>=k) return kth(lnode,start,mid,k);
	else return kth(rnode,mid+1,end,k-tree[lnode].sum);
}

map<int,int> mp;

int main()
{
	scanf("%d",&n);
	int root=1;
	for(int i=1; i<=n; i++)
	{
		int opt,x;
		scanf("%d%d",&opt,&x);
		if(opt==1)
		{
			update(root,1,MAX*2,x+MAX,1);//将x多加一个MAX,全部转化为非负整数
			mp[x]++;
		}
		if(opt==2)
		{
			update(root,1,MAX*2,x+MAX,-1);
			mp[x]--;
			if(mp[x]==0) mp.erase(x);
		}
		if(opt==3) printf("%lld\n",query(root,1,MAX*2,1,x+MAX-1)+1);
		if(opt==4) printf("%d\n",kth(root,1,MAX*2,x)-MAX);
		if(opt==5) printf("%d\n",(--mp.lower_bound(x))->first);//直接STL
		if(opt==6) printf("%d\n",mp.upper_bound(x)->first);
	}
	return 0;
}

鸣谢:本文大量引用博文 https://www.cnblogs.com/IzayoiMiku/p/13997750.html,在此鸣谢

posted @ 2022-09-15 16:38  Tonvia  阅读(186)  评论(0)    收藏  举报
#Snow{ position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 99999; background: rgba(125,137,95,0.1); pointer-events: none; }