心血来潮 把这个基础算法结构补了
呐 先了解一下 可持久化线段树 是什么
自然是 可持久化 + 线段树 啦 多用于询问第m次修改后 某 节点 || 区间 的 值
线段树自然是很好理解的(这个不知道就去补一下吧)
然而可持久化怎么弄呢 总不能每次都copy整棵树吧 不然时空复杂度都打得要死
Imagination:公元XXXX年 出题人跑了三个月得出该题数据 放到考场上给你233s/6666MB
NG!!!
(好吧 如果这种算法没有发明出来的话 是可以的 但是现在是21世纪 而且没有这种算法 怎么会有这种题目)
因此 聪明的三微生物——裸猿人类们啊 发现
在修改一个 节点 || 区间 时啊 改变的只有他的祖先们
因此 我们只需要将该 节点 || 区间涉及的点 和 他们的祖先 复制一遍 赋上修改后该 节点 || 区间 的值 即可
扫描二维码关注公众号,回复:
1848030 查看本文章
首先是 单点修改(区间修改未补) 具体细节见代码 题目仍旧是洛谷的模板
Tip1:单点 修改 && 查询 建树只需将序列二分下去 区间记录 左儿子 右儿子 节点上记录 点权值
Tip2:储存 节点 和 区间 的 数组 大概开 树上节点数的20倍(虽然本题我看有人开10倍)
下面是美酒加咖啡代码加注释 超详细的=w=
#include <iostream>
#include <cstdio>
using namespace std;
const int MAX = 20000010;
struct Persistable_Segment_tree
{
int ls,rs,v;//分别是该位置代表的节点或区间的左儿子右儿子和权值
} tr[MAX];
int edit[1 << 20] = {1},w[1 << 20],tot;//edit储存节点版本 注意第一个(下标0)赋值为1表第一个版本 w储存点初始权值
int build(int l,int r)
{//此处不能直接用++tot代替pos 因为跳转到子程序中继续搜索下去 tot值会增加 而此处值代表当前节点编号 应不变
int pos = ++tot;//tot是当前数组末尾的位置 ++tot则是在末尾处新建储存节点或区间的相关信息 充分利用空间OwO真是太厉害了
if (l == r)//区间左右相等 即只包括一个点 则只存点权
{
tr[pos].v = w[l];//记录初始点权
return pos;//pos是当前节点的编号 需返回 用以让其父亲节点记录他
}
int mid = (l + r) >> 1;//二分存中点
tr[pos].ls = build(l,mid);//记录当前节点的左儿子编号
tr[pos].rs = build(++mid,r);//记录当前节点的右儿子编号
return pos;//返回当前节点编号 需返回 用以让其父亲节点记录他
}
int update(int ed,int l,int r,int p,int k)//在er版本的基础上 修改p点权值为k 记录当前区间最左&&最右端的点l&&r
{//此处不能直接++tot代替pos 因为跳转到子程序中继续搜索下去 tot值会增加 而此处值代表当前节点编号 应不变
int pos = ++tot;//记录当前节点编号 充分利用空间OwO真是太奇妙了
if (l == r)//当搜索到单个节点了
{
tr[pos].v = k;//记录修改后节点权值
return pos;//返回当前节点编号 让当前版本的父亲记录他
}
tr[pos].ls = tr[ed].ls;//将之前的该节点左儿子复制 (引用-->「把子节点指向前驱节点以备复用」)
tr[pos].rs = tr[ed].rs;//将之前的该节点右儿子复制 因为之后会改变该点一儿子的值 这样子可以确定该节点位置(是吧??)
int mid = (l + r) >> 1;//二分存中点
if (p <= mid) tr[pos].ls = update(tr[ed].ls,l,mid,p,k);//向下寻找 逼近p点 更改pos点的左儿子
else tr[pos].rs = update(tr[ed].rs,++mid,r,p,k);//向下寻找 逼近p点 更改pos点的右儿子 用tr[ed]的原因是此时tr[pos]只有1深度的孩子的值
return pos;//返回pos pos作为该点父亲的某个儿子的位置 用以记录
}
int found(int ed,int l,int r,int p)
{//ed是 某版本 储存区间1~n的值 的位置
if (l == r) return tr[ed].v;//找到该点 此时ed已经变为 记录当前版本的p点的位置了 其v则是当前版本的p点的权值 返回
int mid = (l + r) >> 1;
if (p <= mid) return found(tr[ed].ls,l,mid,p);//向下寻找 逼近p点 ed变为ed的左儿子
else return found(tr[ed].rs,++mid,r,p);//向下寻找 逼近p点 ed变为ed的右儿子
}
int main()
{
int n,m,edition,mode,node,weight;//恪尽职守的变量定义
scanf("%d%d",&n,&m);//发人深省的范围输入
for (int a = 1 ; a <= n ; a ++) scanf("%d",&w[a]);//循规蹈矩的节点输入
build(1,n);//建树 从区间 1 ~ n 开始递归 找左右儿子
for (int a = 1 ; a <= m ; a ++)//循序渐进的命令处理
{
scanf("%d%d%d",&edition,&mode,&node);//五花八门的命令输入
if (mode % 2)//巧妙绝伦的判断
{
scanf("%d",&weight);//扑朔迷离的补充输入
edit[a] = update(edit[edition],1,n,node,weight);//update解释见子程序
}//以update此时求出tr数组的末尾 edit[a]意为在第a个版本时修改的点为edit[a-1]到edit[a]的点(上面那行程序让本人想了很久很久)
else//机智无比的转折
{
edit[a] = edit[edition];//因为复制没有创建新节点 因此当前版本的所有点等于当前版本(不是第a-1的版本)之前的所有点
printf("%d\n",found(edit[edition],1,n,node));//输出查询某edition的某node的值
}
}
return 0;//逢考必备的结尾
}
其实这行数似乎比线段树的代码还少=-=这世道究竟......
大概就这样先~区间的还在学~