可持久化线段树模板/解释(洛谷 P3834)

模板链接

可持久化线段树是一种可持久化结构,需要知道的前导知识有:线段树、权值线段树、离散化、线段树的动态开点。说实话,当蒻苟我做了那么多变态的线段树后发现,可持久化线段树(仅限模板)还是很好写的。
什么叫可持久化?
可以想象,有一个线段树,他除了可以进行普通的查询修改操作,还可以记录历史版本,即,我们可以调取任意修改次数时的状态。我们这时可以想象一个朴素的方法完成这个操作:

对于每次修改,我们都将我们建好的这棵树整体复制一个副本,然后我们在这个副本上修改。

我们知道线段树的空间复杂度为O (n)所以对于m次查询,就是 O(nm),从空间上就爆炸了。时间更不要说了。
这是就靠我们的动态开点技术了。
如果我们想修改3号节点。我们易知,pushup操作遍及下图中的绿色节点,其他节点不涉及。
在这里插入图片描述
依次特性,我们每次创立新节点时候,知创立更改的节点,链接到原树上:

在这里插入图片描述
这样的化,空间复杂度降为O(nlogn)时间复杂度O(logn)。
概念理解完,看题。这个题的意思是让我们查询区间中的第k小的值。按照发明人的意思:对于一个序列,我们每次建立一个前缀的权值线段树。即,我们建立维护[1, r],(1<= r <= n)区间的权值线段树,则我们定每一个权值线段树为本可持久化线段树中的一个历史版本。我们通过前缀和思想。我们想想要查询[l,r]中值为k的数量。则,肯定为[1, r]中k的数量减去[1, l-1]的k的数量。于是,我们每次查询r版本l-1版本的权值线段树的k的数量,然后结合权值线段树基本查询方式。可以解出答案。
详细实现过程请看代码注释。
下面是模板代码:

#include <iostream>
#include <cstring>
#include <string>
#include <algorithm>
#include <vector>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#define ll long long
using namespace std;
const int N = 2e5+5;
int root[N], cnt;
int lisan[N];
int su[N];
int mx;
//                  先看main函数。
struct Node
{
    int l, r, sum;
}tr[N<<5];
inline int getn(int g)
{
    return lower_bound(lisan+1, lisan+mx, g) - lisan;
}
void change(int l, int r, int &p, int pre, int v)
{
//在l至r的值域范围,以pre节点(上个历史版本的当前节点)为原型建立p节点,插入值v
    tr[++cnt] = tr[pre];//整体复制。
    p = cnt;//注意p为引用变量,我们要借机修改root[i]的值。
    tr[p].sum++;//插入。
    //以下,标准权值线段树修改过程。
    if (l == r) return;
    int mid = (l+r) >>1;
    if (v <= mid) change(l, mid, tr[p].l, tr[pre].l, v);
    else change(mid+1, r,tr[p].r, tr[pre].r, v);
}
int ask(int l, int r, int L, int R, int k)
{
//在l至r的值域范围,以L、R版本的当前节点作为差值,寻找第k小的
    if (l == r) return l;
    int mid = (l+r) >>1;
    int te = tr[tr[R].l].sum - tr[tr[L].l].sum;//找出差值,这个te值即为该节点做子树值的数量。
    //标准权值线段树查询过程,注意,L、R统一向左、向右。
    if (k <= te) return ask(l, mid, tr[L].l, tr[R].l, k);
    else return ask(mid+1, r, tr[L].r, tr[R].r, k - te);
}
int main()
{
    int n, m;
    scanf("%d%d", &n, &m);
    int _cnt = 0;
    for (int i = 1; i <= n; i++)
    {
        scanf("%d", &su[i]);
         lisan[++_cnt] = su[i];
    }
    sort(lisan+1, lisan+n+1);
    mx = unique(lisan+1, lisan+n+1) - lisan;//离散化过程
    for (int i = 1; i <= n; i++)
        change(1, mx-1, root[i], root[i-1], getn(su[i]));
    //root[i]是指第i个历史版本的权值线段树的历史版本根节点编号
    //从1到mx-1的值域,以root[i-1]权值线段树为原型,插入su[i],建立root[i]的权值线段树。
    while(m--)
    {
        int l, r, k;
        scanf("%d%d%d", &l, &r, &k);
        printf("%d\n", lisan[ask(1, mx-1, root[l-1], root[r], k)]);
        //从1到mx-1的值域,以root[r]、root[l-1]权值线段树为差值,查询第k小的。
    }
    return 0;
}
//确实比那些变态线段树好写吧 2333333

ps. change ask函数的l,r形参是值域范围,不是该节点的左右孩子,更不是区间的范围!

另一道题。Super Mario HDU - 4417
这个题可以线段树+离线去写。但是如果我们使用主席树,我们可以进行在线操作,而且更好理解。
模板,这次要求变成了求一个区间中不大于k的值。注意特判就可以了。
下面是ac代码:

#include <iostream>
#include <cstring>
#include <string>
#include <algorithm>
#include <vector>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#define ll long long
using namespace std;
const int N = 1e5+5;
struct Node
{
    int l, r, sum;
}tr[N<<5];
int su[N];
int lisan[N];
int root[N];
int mx, _cnt, cnt, n,  m;
inline int getn(int x)
{
    return lower_bound(lisan+1, lisan+mx, x) - lisan;
}
void change(int l, int r, int pre, int &p, int v)
{
    tr[++cnt] = tr[pre];
    p = cnt;
    tr[p].sum++;
    if (l==r) return;
    int mid = (l + r) >> 1;
    if (v <= mid) change(l, mid, tr[pre].l, tr[p].l, v);
    else change(mid+1, r, tr[pre].r, tr[p].r, v);
}
int ask(int l, int r, int R, int L, int k)
{
    if(l==r) return tr[R].sum - tr[L].sum;
    int mid = (l+r) >> 1;
    int te = tr[tr[R].l].sum - tr[tr[L].l].sum;
    if (k <= mid) return ask(l, mid, tr[R].l, tr[L].l, k);
    else return te + ask(mid+1, r, tr[R].r, tr[L].r, k);
}
void print(int p, int l, int r)
{
    cout << l << " " << r << " " << tr[p].sum << endl;
    if (l == r) return;
    int mid = (l+r) >>1;
    print(tr[p].l, l, mid);
    print(tr[p].r, mid+1, r);
}
int main()
{
    int t;
    cin >> t;
    int t0 = 1;
    while(t--)
    {
        cnt = 0;
        _cnt = 0;
        scanf("%d%d", &n, &m);
        for (int i = 1; i <= n; i++)
            scanf("%d", &su[i]), lisan[++_cnt] = su[i];
        sort(lisan+1, lisan+1+n);
        mx = unique(lisan+1, lisan+_cnt+1) - lisan;
        for (int i = 0; i < 5*N; i++)
            tr[i].l = tr[i].r = tr[i].sum = 0;
        for (int i = 1; i <= n; i++)
            change(1, mx-1, root[i-1], root[i], getn(su[i]));
        printf("Case %d:\n", t0++);
        while(m--)
        {
            int l, r, k;
            scanf("%d%d%d", &l, &r, &k);
            int gg = getn(k);
            if (lisan[gg] != k) gg--;
            if (gg == 0)
            {
                puts("0");
                continue;
            }
            printf("%d\n", ask(1, mx-1, root[r+1], root[l], gg));
        }
    }
}

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值