线段树初步-可持久化线段树

本文详细介绍了可持久化线段树的概念及其在解决区间查询问题中的应用。通过具体的例题,阐述了如何利用该数据结构高效地解决特定区间内的第k大数的查询问题,并提供了完整的代码实现。

        人的知识就好比一个圆圈,圆圈里面是已知的,圆圈外面是未知的。你知道得越多,圆圈也就越大,你不知道的也就越多。——芝诺

        线段树以其特点能被用来解决许多的问题,其拓展性极强。故学好、用好线段树对增加你的代码长度有显著作用。这篇简小的文章,就来讲一讲线段树的一种变式——可持久化线段树(又作主席树、函数式线段树等)。

        先来说一下思想。线段树作为一个二叉树,在其高效的时间效率之外,空间冗余显得不可忽视。一些时候,由于题目中需要寻找数据的区间如火般跳动,线段树常常不是一个,而是连续的很多个。它们出现的顺序普遍是下标,形象一点就是时间顺序。常常是后面的包含前面,但每往后一个,它都会增加一些内容。这时,创建多个是不可免的,空间的需求也快速增加。常常,简单的思想成就了伟大事物的出现。讲到这里,前缀和当然就迫不及待地从意识中钻了出来。可持久化的意义也随之而来:我们将每一棵新的线段树建在其前辈的高台上。最终办法是:用相同的子树表示相同的部分,即将一条链连过去;用不同的新的小的子树表示不同的部分,即增加一条不大的链。

        例题-Easy

                Source Poj2104

                给出长度为10w的序列及5k个对于指定区间内第k大的数的询问。

                Solution

                小学老师曾经说过,对于找规律的题目,不要慌张,不要着急,要从最简单的开始找。所以我们先抛开跳动的区间,对于这输入样例整个地来看。首先,要找第k大的数,必定会想到权值线段树。这样的话,如果右子节点的权值大于等于k,就过去找;反之,就去左边找。找到的根节点就是第k大的数。

                现在,我们脚踏家园,放眼世界。对于跳动的区间,根据我们伟大的思想,有这样的发现:要某区间的权值线段树,每个节点的值就是它在[1,r]这棵树中的值减去它在[1,l-1]这棵树中的值。每次查找减一下就是我们需要的这棵树的值了。尽管我们并没建这棵树,却可以用极高的效率间接得出它的信息。

                在实现的时候,由于数据的不可预测性,建议使用离散化的数据。建树的过程是这样的:首先,对于每个[1,n]的树,分配一个根节点,它直接复制上一个根节点的信息;其次,分配时相当于我们添加了一个数,由于这个节点连接的是上个版本可用的信息,我们只需替换修改的那一条链,具体操作和普通权值线段树相似。

                Code for reference

#include<iostream>

#include<cstdio>

#include<algorithm>

using namespace std;

const int N=100005;

struct node

{

int sum,l,r;

};

struct num

{

int n,loc;

};

node tree[N*20];

int tot=0;

num a[N];

int n,m;

int b[N];

int root[N];

bool cmp(num x,num y)

{

return x.n<y.n;

}

void insert(int num,int &now,int l,int r)

{

tree[++tot]=tree[now];

now=tot;

tree[now].sum++;

if(l==r)return;

int mid=l+r>>1;

if(num<=mid)

insert(num,tree[now].l,l,mid);

else

insert(num,tree[now].r,mid+1,r);

}

int query(int i,int j,int k,int l,int r)

{

if(l==r)return l;

int ans=tree[tree[j].l].sum-tree[tree[i].l].sum,mid=l+r>>1;

if(k<=ans)

return query(tree[i].l,tree[j].l,k,l,mid);

else

return query(tree[i].r,tree[j].r,k-ans,mid+1,r);

}

int main()

{

scanf("%d%d",&n,&m);

for(int i=1;i<=n;++i)

{

scanf("%d",&a[i].n);

a[i].loc=i;

}

sort(a+1,a+n+1,cmp);

for(int i=1;i<=n;++i)

{

b[a[i].loc]=i;

}

for(int i=1;i<=n;++i)

{

root[i]=root[i-1];

insert(b[i],root[i],1,n);

}

int x,y,z;

for(int i=1;i<=m;++i)

{

scanf("%d%d%d",&x,&y,&z);

printf("%d\n",a[query(root[x-1],root[y],z,1,n)].n);

}

return 0;

}

                Hint

                这个庞大的树的空间大概要开数十倍之数据。

                我的读入优化出现了小问题,导致无数次Runtime Error。

        思考题-Easy

                Source CQOI2015 Luogu3168

        例题-Normal

                Source Bzoj1901/Zoj2112

                给出长度为5w的序列及1w个对于指定区间内第k大的数的询问或对指定数的修改。

                Solution

                未完待续。首先给出一些关键词:离散化、树状数组、可持久化线段树。 

可持久化线段树是一种支持历史版本查询的数据结构,其核心思想是在每次修改操作时保留完整的旧版本信息。这使得它在某些应用场景中非常有用,例如版本控制系统或需要回溯操作的算法问题。 ### 空间复杂度分析 可持久化线段树的空间复杂度与普通线段树相比有所增加。普通线段树的空间复杂度为 $O(n)$,其中 $n$ 是数据规模。而可持久化线段树由于需要保留历史版本,每次更新操作都会生成新的节点,因此其空间复杂度为 $O(n \log n)$。具体来说,每次更新操作最多会生成 $O(\log n)$ 个新节点,因为线段树的高度为 $O(\log n)$,每个节点最多分裂一次[^1]。 ### 实现原理 可持久化线段树的核心实现原理是**节点复用**和**路径复制**。当对线段树进行更新时,只有从根节点到目标节点的路径上的节点会被复制,其余节点保持不变。这种方式避免了对整个线段树的完全复制,从而节省了内存[^1]。 具体实现中,每个版本的线段树通过一个根节点指针来标识。当进行更新操作时,新版本的根节点指向一个新的节点,而未修改的子树则继续指向旧版本的节点。这种设计使得不同版本之间可以共享未修改的部分,从而减少内存开销。 以下是一个简单的可持久化线段树的实现示例,用于单点更新和区间查询: ```cpp #include <iostream> #include <vector> using namespace std; struct Node { int val; // 节点值,例如区间和 Node* left; Node* right; Node(int v) : val(v), left(nullptr), right(nullptr) {} }; class PersistentSegmentTree { private: vector<int> data; Node* build(Node* node, int l, int r) { if (l == r) { node->val = data[l]; return node; } int mid = (l + r) / 2; node->left = new Node(0); node->right = new Node(0); build(node->left, l, mid); build(node->right, mid + 1, r); node->val = node->left->val + node->right->val; return node; } Node* update(Node* node, int l, int r, int idx, int value) { if (l == r) { Node* new_node = new Node(value); return new_node; } int mid = (l + r) / 2; Node* new_node = new Node(0); if (idx <= mid) { new_node->left = update(node->left, l, mid, idx, value); new_node->right = node->right; } else { new_node->left = node->left; new_node->right = update(node->right, mid + 1, r, idx, value); } new_node->val = new_node->left->val + new_node->right->val; return new_node; } int query(Node* node, int l, int r, int ql, int qr) { if (qr < l || ql > r) return 0; if (ql <= l && r <= qr) return node->val; int mid = (l + r) / 2; return query(node->left, l, mid, ql, qr) + query(node->right, mid + 1, r, ql, qr); } public: vector<Node*> roots; // 存储每个版本的根节点 PersistentSegmentTree(vector<int>& arr) { data = arr; roots.push_back(new Node(0)); build(roots[0], 0, data.size() - 1); } void update(int version, int idx, int value) { Node* new_root = update(roots[version], 0, data.size() - 1, idx, value); roots.push_back(new_root); } int query(int version, int ql, int qr) { return query(roots[version], 0, data.size() - 1, ql, qr); } }; ``` ### 内存占用分析 可持久化线段树的内存占用主要由以下几个部分构成: 1. **节点存储**:每个节点需要存储值、左右子节点指针。通常每个节点的大小为常数级别(例如包含一个整数值和两个指针)。 2. **版本管理**:每个版本通过一个根节点指针进行管理,根节点指针的存储开销为 $O(1)$。 3. **路径复制**:每次更新操作会生成新的节点,这些新节点的总数为 $O(\log n)$,因此总内存占用为 $O(n \log n)$。 在实际应用中,内存占用还可能受到编程语言的内存管理机制影响。例如,在 C++ 中手动管理内存可能导致较高的内存碎片,而在 Java 或 Python 等具有垃圾回收机制的语言中,内存占用可能相对较低,但具体表现取决于实现细节。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值