Hdu2104(主席树)

主席树入门
可直接看上面的博客,下面的可以不看,自己摘录总结的
主席树是一种可持久化线段树,所谓可持久化就是可保存历史版本。例如让你往权值线段树中依次插入一列数,在你插入完所有数之后询问在你插入第x个数之后所有数中第k大的是多少?这就需要你记录每个时刻的线段树,最暴力也是最容易想到的方法是每次插入一个数之前再建一棵线段树复制上一时刻的线段树,然后再在新建的树上插入。但这样不仅占很大空间还很费时间。这个问题可持久化线段树就可以正常地回答出来

那么如何实现这样的一棵线段树呢?

想象一棵普通的线段树,我们要对它进行单点修改,需要修改logn个点。每次修改的时候,我们丝毫不修改原来的节点,而是在它旁边新建一个节点,把原来节点的信息(如左右儿子编号、区间和等)复制到新节点上,并对新节点进行修改。

那么如何查询历史版本呢?只需记录每一次修改对应的新根节点编号(根据上面描述的操作,根节点每次一定会新建一个的),每次询问从对应的根节点往下查询就好了。

定义的数据:

int idx; //index,记录目前一共建过多少节点
int sum[M], lson[M], rson[M]; //区间和、左儿子、右儿子
int root[N]; //每次修改对应的根节点编号

假设这道题一开始序列全是0,首先我们把一棵空的树建出来:

void build(int &k, int l, int r){
    //k传的是地址,这样在这一层函数中修改k就可以直接修改上一层的lson或rson了
    k = ++idx; //为新节点编号
    if(l == r) return; //一定要在创建完新节点之后再return
    int mid = (l + r) >> 1;
    build(lson[k], l, mid);
    build(rson[k], mid + 1, r);
}

接下来实现修改操作,把位置p上的数增加x。

//old是这个位置原来的节点,k是当前要创建的新节点(的地址)
void change(int old, int &k, int l, int r, int p, int x){
    k = ++idx; //修改的时候要创建新点
    lson[k] = lson[old], rson[k] = rson[old];
    sum[k] = sum[old] + x; //先把原来节点的信息复制过来,顺便修改区间和
    if(l == r) return; //仍然要记得先建点后return
    int mid = (l + r) >> 1;
    if(p <= mid) change(lson[k], lson[k], l, mid, p, x);
    else change(rson[k], rson[k], mid + 1, r, p, x);
}

下面进行区间和查询:

int query(int k, int l, int r, int ql, int qr){
    if(ql <= l && qr >= r) return sum[k];
    int mid = (l + r) >> 1, ans = 0;
    if(ql <= mid) ans += query(lson[k], l, mid, ql, qr);
    if(qr > mid) ans += query(rson[k], mid + 1, r, ql, qr);
    return ans;
}

例如,有m次修改操作,那么必然会建立m个线段树,除了第一个线段树是完整的树以外,后面的都是在上一个线段树的基础上进行更新的。
主席树的实质,就是以最初的线段树作为模板,通过"结点复用“的方式,实现存储多个线段树。

实战练习

题意:

来看Hdu2104,就是给n个数,m次查询,每次查询【l, r】区间的第k小。

思路:

首先如果给出只有1个权值线段树,让你查第k小,这个是容易的,每次比较k 和 左区间的个数,如果k<=左区间的个数,那说明这个第k小一定在左区间,反之,在右区间查找,如此递归,直到叶节点。

这个在只有1个权值线段树的情况下,但现在是主席树,主席树有多个权值线段树,那么查询区间
【l, r】对应于哪一个线段树呢,只要找到了这个线段树,剩下的根据刚才的分析,就可以做了。

首先来看插入,有n个数,所以n次插入,于是就生成了n个新的线段树(每次插入,都要改变一条从根到叶的路径,新建一些节点来存这个改变了的节点,没有改变的直接从上一个线段树继承过来)。比如,插入1 5 2 6 3 7 4,依次生成了第1,2,3,4,5,6,7个线段树。第i个线段树是在第i-1个线段树上累加更新的。现在查询区间是【l, r】,那么需要用第r个线段树 - 第l-1个线段树,这就是我们要找的那个线段树。
是不是很像前缀和!

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int INF = 0x3f3f3f3f;
typedef long long LL;
const int maxn = 100000+5;
int A[maxn], X[maxn];	// 原数组 和 离散化后的数组 
// 主席树 
int Tree[maxn*20];
int cnt;			// 每次新建的结点编号
int lson[maxn*20], rson[maxn*20]; //lson[x]和rson[x]是结点x的左右儿子结点的编号。
int root[maxn*20]; 	//第i棵线段树的根节点的编号

void Build(int& rt, int l, int r){
	rt = ++cnt; Tree[rt] = 0;
	if(l == r) return;
	int mid = (l+r) >> 1;
	Build(lson[rt], l, mid);
	Build(rson[rt], mid+1, r);
}

void update(int& new_rt, int old_rt, int l, int r, int pos, int val){
	new_rt = ++cnt;
	lson[new_rt] = lson[old_rt]; rson[new_rt] = rson[old_rt];	// 先复制旧的,再改
	Tree[new_rt] = Tree[old_rt] + val;
	if(l == r) return;
	int mid = (l+r) >> 1;
	if(pos <= mid) update(lson[new_rt], lson[old_rt], l, mid, pos, val);
	else update(rson[new_rt], rson[old_rt], mid+1, r, pos, val);
}

// [la, rb]上找第k小 
int Query(int la, int rb, int l, int r, int k){
	if(l == r) return l;
	int mid = (l+r) >> 1, sum_left = Tree[lson[rb]] - Tree[lson[la]];
	if(k <= sum_left) return Query(lson[la], lson[rb], l, mid, k);
	else return Query(rson[la], rson[rb], mid+1, r, k-sum_left);
}

int main()
{
	//freopen("E:/Cgit/Practice/ACM/in.txt","r",stdin);
	int n, m;
	while(scanf("%d%d", &n,&m) == 2){
		int tot = 0;
		for(int i = 1; i <= n; ++i) scanf("%d", &A[i]), X[++tot] = A[i];
		// 离散化 
		sort(X+1, X+tot+1);
		int k = 1;
		for(int i = 2; i <= n; ++i) if(X[i] != X[i-1]) X[++k] = X[i];
		// 建树 
		cnt = 0;
		Build(root[0], 1, k);
		// n个数,n次插入更新,建立n个线段树 
		for(int i = 1; i <= n; ++i){
			int pos = lower_bound(X+1, X+k+1, A[i]) - X;
			update(root[i], root[i-1], 1, k, pos, 1);
		}
		// 依次查询
		while(m--){
			int x,y,z; scanf("%d%d%d", &x,&y,&z);
			int ans = Query(root[x-1], root[y], 1, k, z);
			printf("%d\n", X[ans]);
		} 
		
	}
	fclose(stdin);
	return 0;
}

参考:https://www.cnblogs.com/RabbitHu/p/segtree.html
https://www.cnblogs.com/Khada-Jhin/p/9307210.html

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值