可持久化数组入门

# 洛谷题目链接:[可持久化数组](https://www.luogu.org/problemnew/show/P3919)

题目描述

如题,你需要维护这样的一个长度为 NN 的数组,支持如下几种操作

  1. 在某个历史版本上修改某一个位置上的值

  2. 访问某个历史版本上的某一位置的值

此外,每进行一次操作(对于操作2,即为生成一个完全一样的版本,不作任何改动),就会生成一个新的版本。版本编号即为当前操作的编号(从1开始编号,版本0表示初始状态数组)

输入输出格式

输入格式:

输入的第一行包含两个正整数 N, M , 分别表示数组的长度和操作的个数。

第二行包含N个整数,依次为初始状态下数组各位的值(依次为 \(a_i ,1 \leq i \leq N\))。

接下来 M 行每行包含3或4个整数,代表两种操作之一(i 为基于的历史版本号):

  1. 对于操作1,格式为\(v_i \ 1 \ {loc}_i \ {value}_i\) ,即为在版本\(v_i\)​ 的基础上,将 \(a_{{loc}_i}\)​ 修改为 \({value}_i\)

  2. 对于操作2,格式为\(v_i \ 2 \ {loc}\)​ ,即访问版本\(v_i\) 中的 \(a_{{loc}_i}\) 的值

输出格式:

输出包含若干行,依次为每个操作2的结果。

输入输出样例

输入样例#1:

5 10
59 46 14 87 41
0 2 1
0 1 1 14
0 1 1 57
0 1 1 88
4 2 4
0 2 5
0 2 4
4 2 1
2 2 2
1 1 5 91

输出样例#1:

59
87
41
87
88
46

说明

数据规模:

对于30%的数据:\(1 \leq N, M \leq {10}^3,1≤N,M≤10^3\)

对于50%的数据:\(1 \leq N, M \leq {10}^4,1≤N,M≤10^4\)

对于70%的数据:\(1 \leq N, M \leq 10^5,1≤N,M≤10^5\)

对于100%的数据:\(1 \leq N, M \leq {10}^6, 1 \leq {loc}_i \leq N, 0 \leq v_i < i, {-10}^9 \leq a_i, {value}_i \leq {10}^9\)

经测试,正常常数的可持久化数组可以通过,请各位放心

数据略微凶残,请注意常数不要过大

另,此题I/O量较大,如果实在TLE请注意I/O优化

询问生成的版本是指你访问的那个版本的复制

样例说明:

一共11个版本,编号从0-10,依次为:

  • 0 : 59 46 14 87 41

  • 1 : 59 46 14 87 41

  • 2 : 14 46 14 87 41

  • 3 : 57 46 14 87 41

  • 4 : 88 46 14 87 41

  • 5 : 88 46 14 87 41

  • 6 : 59 46 14 87 41

  • 7 : 59 46 14 87 41

  • 8 : 88 46 14 87 41

  • 9 : 14 46 14 87 41

  • 10 : 59 46 14 87 91

概述一下题意:给出一个序列,支持单点修改,以及单点查询历史版本.
如果直接用数组统计,很显然是很不好保存修改的历史版本的.

于是我们可以考虑如何记录历史版本.很显然,最暴力的办法就是直接开数组存下每个历史版本的所有值,然后在这些数组中查询.但是这样的空间复杂度是O(\(N^2\))的.对于大数据是存不下来的.

既然存不下来,那么我们就考虑一下如何优化空间的使用.
如果直接用数组一个下标一个下标的存,很显然是没有空间可以挤出来的.这里我们考虑用线段树的方式存储节点,将原数组将入到一颗线段树中.

看到这里可能有人会说:线段树的空间复杂度不是比数组模拟要高吗?

但是,我们在修改历史版本的时候,每修改一次,事实上就是在上一个版本中修改了一个值,那么我们可以通过共用节点的方式,将没有被修改的节点共用,这样就可以极大限度的省出空间.这样可以被证明空间复杂读是O(\(NlogN\))的.由于线段树形的结构,时间复杂度同样也是O(\(NlogN\))的.

然后在找一个修改版本的时候直接通过该版本的线段树的根节点来找.

下面看一下代码的注释吧.

#include<bits/stdc++.h>
using namespace std;
const int N=1000000+5;

int n, m, cnt = 0;//cnt记录节点编号
int w[N], root[N];//w记录权值,root[i]记录第i个修改版本

struct persistent_tree{
    int ls, rs, val;//记录左右儿子,权值只有叶子节点才需要记录
}t[N*21];

int gi(){
    int ans = 0 , f = 1; char i = getchar();
    while(i<'0'||i>'9'){if(i=='-')f=-1;i=getchar();}
    while(i>='0'&&i<='9'){ans=ans*10+i-'0';i=getchar();}
    return ans * f;
}

void build(int &node,int l,int r){//建树,在到新节点的时候要记录,所以这里要传地址
    node = ++cnt;
    if(l == r){t[node].val = w[l]; return;}
    int mid = (l+r>>1);
    build(t[node].ls,l,mid);
    build(t[node].rs,mid+1,r);
}

void updata(int &node,int last,int l,int r,int pos,int val){
    node = ++cnt; t[node] = t[last];//复制上一个版本
    if(l == r){t[node].val = val; return;}//权值只需要记录在叶子节点,所以只需要在找到叶子节点的时候修改
    int mid = (l+r>>1);
    if(pos <= mid) updata(t[node].ls,t[last].ls,l,mid,pos,val);//左右递归找到修改的位置
    else updata(t[node].rs,t[last].rs,mid+1,r,pos,val);
}

int query(int node,int l,int r,int pos){//线段树基本操作
    if(l == r) return t[node].val;
    int mid = (l+r>>1);
    if(pos <= mid) return query(t[node].ls,l,mid,pos);
    else return query(t[node].rs,mid+1,r,pos);
}

int main(){
    //freopen("data.in","r",stdin);
    int last, flag, pos, val; n = gi(); m = gi();
    for(int i=1;i<=n;i++) w[i] = gi();
    build(root[0],1,n);
    for(int i=1;i<=m;i++){
	last = gi(); flag = gi();
	if(flag == 1){
	    pos = gi(); val = gi();
	    updata(root[i],root[last],1,n,pos,val);
	}
	else printf("%d\n",query(root[last],1,n,gi())) , root[i] = root[last];//第i次是修改操作直接保存上一次修改的操作
    }
    return 0;
}
posted @ 2018-04-10 17:27  Brave_Cattle  阅读(1230)  评论(0编辑  收藏  举报