yuwj  

主席树

这篇题解很神!

P3919 【模板】可持久化线段树 1(可持久化数组) - 洛谷

https://www.cnblogs.com/zinthos/p/3899565.html
oi wiki

目标问题:

求区间第k大的值

思想:

维护历史版本的线段树,只复制修改的线段树节点就保存了历史的线段树,然后就建成了一颗以线段树为节点主席树

主席树1

主席树2

主席树的性质:

1、每一次修改增加的节点个数为log(n)。

2、增加的非叶子结点会连向一个是其他版本的节点,一个是连向新节点。

3、主席树有很多根……

4、对于每一个根都可以构成一棵完整的线段树。

5、每一个节点都有可能有不只一个爸爸……

所以我们可以知道主席树只会对部分节点进行复制,并且每一次复制的节点个数是log(n)。我们每一次想询问一个版本的线段树,就可以在那个版本的根构成的线段树中询问。

1、怎么构建新节点?怎么给新节点编号?怎么连边?

2、怎么访问子节点?

3、怎么存根?

现在给出刚才问题的答案:

1、直接开一块内存池存新节点。编号为此时总节点数个数+1。开结构体存子节点编号;线段树建什么边,一指了事。

2、访问子节点编号,不是像线段树一样乘2或乘2+1,而是在结构体存子节点编号。

3、另外开个数组存。

代码:

struct kkk{
	int l,r,val;
}tree[maxn];

//新建节点
int clone(int node){
	top++;
	tree[top]=tree[node];//全部信息都传到新节点
	return top;
}

//建树就是新建主席树上的节点
int maketree(int node,int begin,int end){
	node=++top;
	if(begin==end){
		tree[node].val=a[begin];
		return top;
	}
	int mid=(begin+end)>>1;
	tree[node].l=maketree(tree[node].l,begin,mid);
	tree[node].r=maketree(tree[node].r,mid+1,end);
	return node;
}

//更新
int update(int node,int begin,int end,int x,int val){
	node=clone(node);	//更新就要新建节点 
	if(begin==end){
		tree[node].val=val;
	}else{
		int mid=(begin+end)>>1;
		if(x<=mid)
			tree[node].l=update(tree[node].l,begin,mid,x,val);	//访问左子树 
		else
			tree[node].r=update(tree[node].r,mid+1,end,x,val);	//访问右子树 
	}
	return node;
}

//询问
int query(int node,int begin,int end,int x){
	if(begin==end){
		return tree[node].val;
	}else{
		int mid=(begin+end)>>1;
		if(x<=mid)
			return query(tree[node].l,begin,mid,x);	//访问左子树 
		else
			return query(tree[node].r,mid+1,end,x);	//访问右子树 
	}
}

//再来看主程序,里面看根怎么存储:
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)scanf("%d",&a[i]);
	root[0]=maketree(0,1,n);	//root[i]为i版本的根编号,刚开始编号为0 
	for(int i=1;i<=m;i++){
		scanf("%d%d%d",&rt,&mode,&x);
		if(mode==1){
			scanf("%d",&y);
			root[i]=update(root[rt],1,n,x,y);	//保存版本 
		}
		else{
			printf("%d\n",query(root[rt],1,n,x));	//输出 
			root[i]=root[rt];					//新建版本 
		}
	}
}

【模板1】可持久化线段树1(可持久化数组)

代码:

/*
点修改 + 点查询
每次都新建一棵树(每次复制都只是复制修改的那条路径保证节省空间)
具体步骤:
复制 -> 更新 -> 递归-> 返回

可持久化数组的0版本:
               [1..5]
               /    \
      [1..3]            [4..5]
       /    \            /   \
   [1..2]  [3..3]    [4..4] [5..5]
    /   \     │        │      │
  (59) (46) (14)   (87)   (91)
*/
#include <bits/stdc++.h>
using namespace std;
#define rep(i,a,b) for(int i = a; i <= b; i++)
const int N = 1e6 + 10, MAXN = N << 5;
int a[N], root[N], top; //主席树上的节点编号

struct Node{
    int l, r, val;
}tree[MAXN];

//新建节点
int clone(int node){
    top++;
    tree[top] = tree[node]; // 复制一遍
    return top; //返回当前新建的树的根节点
}

//建树就是新建主席树上的节点
int maketree(int node, int begin, int en){
    node = ++top; // 新建节点就是建树
    if(begin == en){
        tree[node].val = a[begin];
        return top;
    }

    int mid = (begin + en) >> 1;
    tree[node].l = maketree(tree[node].l, begin, mid);
    tree[node].r = maketree(tree[node].r, mid+1, en);
    return node;
}

//修改,就是新建一棵树,然后只改变修改路径上的值(复制路径)
int update(int node, int begin, int en, int x, int val){
    node = clone(node);
    if(begin == en){
        tree[node].val = val;
    }else{
        int mid = (begin+en) >> 1;
        if(x <= mid) tree[node].l = update(tree[node].l, begin, mid, x, val);
        else tree[node].r = update(tree[node].r, mid + 1, en, x, val);
    }

    return node;
}

//单点查询
int query(int node, int begin, int en, int x){
    if(begin == en){
        return tree[node].val;
    }else{
        int mid = (begin + en) >> 1;
        if(x <= mid) return query(tree[node].l, begin, mid, x);
        else return query(tree[node].r, mid + 1, en, x);
    }
}

int main(){
    ios::sync_with_stdio(0);cin.tie(0);
    int n, m; cin >> n >> m;
    rep(i, 1, n) cin >> a[i];
    top = 0;
    root[0] = maketree(0,1,n);
    int ver, op, p;
    rep(i, 1, m){
        cin >> ver >> op >> p;
        if(op == 1){
            int x; cin >> x;
            root[i] = update(root[ver], 1,n, p, x);
        }else{
            cout << query(root[ver], 1, n, p) << '\n';
            root[i] = root[ver];
        }
    }
    return 0;
}

【模板2】可持久化线段树2

P3824

代码:

/*
区间第k小:
主席树 = 离散化 + 权值线段树 + 前缀和思想
前缀【1,r】第k小:权值树上二分即可,k <= cntL,说明在左边有第k小,否则在右边找 k - cntL
区间【l,r】第k小:r版本的信息 - (l-1)版本的信息即可

每次插入都维护新的版本,维护出现值域出现次数就好了

举个例子:
离散化后的原始数组:
【3,1,2,4,5】
求解区间【2,4】第2小的过程:
1.建主席树:逐个离散化后插入权值线段树中,每次插入都新建一颗树
2.查询:2,4的第2小,本质就是就是在“频次差值树”上找第2小
    【r - (l-1)】
    cntL = tree[4].l.sum - tree[2].l.sum 
    //去左子树(小值域 <-> 第k小)看,sum维护次数,如果cnt >= k,说明有k个数字,可能存在第k小,否则,就去另外一边查找第k-cntL小
*/
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 200000 + 5;
// 最坏节点数 ≈ (N + N·logN) * 2 ≈ N*25
const int MAXNODE = MAXN * 25;

int n, q;
int a[MAXN], b[MAXN], m;
int root[MAXN], cntNode;

// 一个节点封装了 左/右 子指针 + 该区间频次和
struct Node {
    int l, r, sum;
} tree[MAXNODE];

// build:建一棵空的值域树,返回根节点编号
int build(int L, int R) {
    int u = ++cntNode;
    tree[u].sum = 0;
    if (L < R) {
        int M = (L + R) >> 1;
        tree[u].l = build(L, M);
        tree[u].r = build(M+1, R);
    }
    return u;
}

// update:在旧版本 pre 的基础上,在位置 x 处 +1,并返回新根
int update(int pre, int L, int R, int x) {
    int u = ++cntNode;
    tree[u] = tree[pre];       // 路径复制:先整体拷贝
    tree[u].sum++;             // 累加频次
    if (L < R) {
        int M = (L + R) >> 1;
        if (x <= M)
            tree[u].l = update(tree[pre].l, L, M, x);
        else
            tree[u].r = update(tree[pre].r, M+1, R, x);
    }
    return u;
}

// query:在版本 u->v 的差上找第 k 小(值域 [L..R])
int query(int u, int v, int L, int R, int k) {
    if (L == R) return L;
    int M = (L + R) >> 1;
    int cntLeft = tree[ tree[v].l ].sum - tree[ tree[u].l ].sum;
    if (cntLeft >= k)
        return query(tree[u].l, tree[v].l, L, M, k);
    else
        return query(tree[u].r, tree[v].r, M+1, R, k - cntLeft);
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> q;
    // 读数组并做离散
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        b[i] = a[i];
    }
    sort(b+1, b+1+n);
    m = unique(b+1, b+1+n) - (b+1);

    // 版本 0:空树
    cntNode = 0;
    root[0] = build(1, m);

    // 构造前缀版本:每插入一个 a[i] 就 update 一次
    for (int i = 1; i <= n; i++) {
        int t = int(lower_bound(b+1, b+1+m, a[i]) - b);
        root[i] = update(root[i-1], 1, m, t);
    }

    // 处理 q 次区间第 k 小查询
    while (q--) {
        int l, r, k;
        cin >> l >> r >> k;
        int idx = query(root[l-1], root[r], 1, m, k);
        cout << b[idx] << "\n";
    }
    return 0;
}

日常水话:
明天又有考试了,究竟是什么让我能花时间来学习这个主席树模板?
我又为什么能花这么就得时间来学这个?还真就给我搞懂了~
这个瘾是真的大啊,网瘾少年该戒了

提一嘴杭电补题工程得问题,因为这周考试,所以需要加紧复习,但是为了保证明天的题目量与学校课程的推进,
最后只在5天内学了2个算法,还只是停留在模板题目阶段(能弄明白已经很艰辛了QWQ)
所以,可能就这么荒废了?后面来补起来吧,蓝桥杯也就那样(真想骂人)还是icpc好玩,争取进校队下半年拿牌!
好了,就这样了,还要复习考试www,run~

posted on 2025-04-25 11:12  xiaowang524  阅读(38)  评论(0)    收藏  举报