首页 > 其他分享 >大型线段树 - 知识点梳理

大型线段树 - 知识点梳理

时间:2023-07-12 21:36:35浏览次数:44  
标签:知识点 cur int 线段 tr son tl sum 梳理

可持久化线段树

可持久化数据结构可以通过不断重复利用节点,在高效且省空间的情况下建立及存储普通数据结构的多个历史版本并进行查询。因为存在时间轴,因此有时可搭配离线算法使用。

实现方法

所有树形数据结构的可持久化处理都和这个差不多
普通的线段树长这样:

假设要对其中一个节点进行修改并建立一个历史版本,朴素的做法是新建立这样一棵树,然后修改一条链上的节点。

这时就能发现一个事实:只有被修改的这条链上的节点发生了变化,其他的节点根本没有发生变化,因此最好不再建一遍。这就是可持久化的本质:尽量重复利用已经建好的节点,新建立最少的节点构成一个新的版本。
我们把不会变的节点的边连向原树上的节点,然后:(如果你平时写普通线段树不习惯动态开点,记住这要写动态开点)

可以看到新的树只有一条链的节点是新建的,形态与原树完全一样。
这样就达到了最大化利用已建好节点的目的。每条链长约 \(\log n\),因此可持久化线段树空间复杂度为 \(O(n + m\log n)\),时间复杂度为 \(O((n + m)\log n)\)。(朴素算法为 \(O(mn)\))
例题:Luogu P3919 【模板】可持久化线段树 1(可持久化数组)
不知道这题为什么没有区间查询。
上代码!

#include<bits/stdc++.h>
using namespace std;

const int MAXN = 1000005;
int cnt, sum[25 * MAXN], n, m, son[25 * MAXN][2], root[MAXN], a[MAXN]; 
int build (int tl, int tr) {
	int cur = ++cnt;
	if (tl != tr) {
		int mid = (tl + tr) >> 1;
		son[cur][0] = build (tl, mid);
		son[cur][1] = build (mid + 1, tr);
		sum[cur] = sum[son[cur][0]] + sum[son[cur][1]];
	}
	else sum[cur] = a[tl];
	return cur;
}
int modify (int pre, int tl, int tr, int p, int x) {
	int cur = ++cnt;
	son[cur][0] = son[pre][0];
	son[cur][1] = son[pre][1];
	sum[cur] = sum[pre];
	if (tl == tr) {
		sum[cur] = x;
	}
	else {
		int mid = (tl + tr) >> 1;
		if (p <= mid) son[cur][0] = modify (son[pre][0], tl, mid, p, x);
		else son[cur][1] = modify (son[pre][1], mid + 1, tr, p, x);
		sum[cur] = sum[son[cur][0]] + sum[son[cur][1]];
	}
	return cur;
}
int query (int cur, int tl, int tr, int p) {
	if (tl == tr) return sum[cur];
	else {
		int mid = (tl + tr) >> 1;
		if (p <= mid) return query (son[cur][0], tl, mid, p);
		else return query (son[cur][1], mid + 1, tr, p);
	}
}
int main () {
    ios::sync_with_stdio(false);
    cin.tie(0);
	cin >> n >> m;
	for (int i = 1;i <= n;i++) {
		cin >> a[i];
	}
	root[0] = build (1, n);
	int v, loc, val, op;
	for (int i = 1;i <= m;i++) {
		cin >> v >> op;
		if (op == 1) {
			cin >> loc >> val;
			root[i] = modify (root[v], 1, n, loc, val);
		}
		else {
			cin >> loc;
			cout << query (root[v], 1, n, loc) << endl;
			root[i] = root[v];
		}
	}
	return 0;
}

常见优化

以可持久化数组搭载其他数据结构

例题:Luogu P3402 可持久化并查集
普通并查集使用 father 数组存储元素所在集合,我们可以将 father 数组可持久化,回滚版本时直接回滚数组的版本即可。
注意:可持久化线段树不能直接修改节点,否则会影响前面的版本,要修改节点只能新建一条链!所以这道题没法写路径压缩优化。这道题按秩合并或者按深度合并都可。
注意:一定不要忘记可持久化线段树需要开 \(\log n\) 倍空间,所以有时简单将任意数据结构的数组全部可持久化得到可持久化的数据结构不可行!(e. g. 不要尝试这样写可持久化平衡树)
上代码!

#include<bits/stdc++.h>
#include<bits/stdc++.h>
using namespace std;

const int MAXN = 200005, MAXM = 2e5 + 5;
int cnt, fa[55 * MAXN], dep[55 * MAXN], n, m, son[55 * MAXN][2], root[MAXM]; 
int build (int tl, int tr) {
	int cur = ++cnt;
	if (tl != tr) {
		int mid = (tl + tr) >> 1;
		son[cur][0] = build (tl, mid);
		son[cur][1] = build (mid + 1, tr);
	}
	else {
		fa[cur] = tl;
		dep[cur] = 1;
	}
	return cur;
}
int modifydep (int pre, int tl, int tr, int p, int x) {
	int cur = ++cnt;
	son[cur][0] = son[pre][0];
	son[cur][1] = son[pre][1];
	dep[cur] = dep[pre];
	fa[cur] = fa[pre];
	if (tl == tr) {
		dep[cur] = x;
	}
	else {
		int mid = (tl + tr) >> 1;
		if (p <= mid) son[cur][0] = modifydep (son[pre][0], tl, mid, p, x);
		else son[cur][1] = modifydep (son[pre][1], mid + 1, tr, p, x);
	}
	return cur;
}
int modifyfa (int pre, int tl, int tr, int p, int x) {
	int cur = ++cnt;
	son[cur][0] = son[pre][0];
	son[cur][1] = son[pre][1];
	dep[cur] = dep[pre];
	fa[cur] = fa[pre];
	if (tl == tr) {
		fa[cur] = x;
	}
	else {
		int mid = (tl + tr) >> 1;
		if (p <= mid) son[cur][0] = modifyfa (son[pre][0], tl, mid, p, x);
		else son[cur][1] = modifyfa (son[pre][1], mid + 1, tr, p, x);
	}
	return cur;
}
int querydep (int cur, int tl, int tr, int p) {
	if (tl == tr) return dep[cur];
	else {
		int mid = (tl + tr) >> 1;
		if (p <= mid) return querydep (son[cur][0], tl, mid, p);
		else return querydep (son[cur][1], mid + 1, tr, p);
	}
}
int queryfa (int cur, int tl, int tr, int p) {
	if (tl == tr) return fa[cur];
	else {
		int mid = (tl + tr) >> 1;
		if (p <= mid) return queryfa (son[cur][0], tl, mid, p);
		else return queryfa (son[cur][1], mid + 1, tr, p);
	}
}
int find (int v, int x) {
	int fx = queryfa (root[v], 1, n, x);
	return fx == x ? x : find (v, fx);
}
void merge (int pv, int cv, int x, int y) {
	int fx = find(pv, x), fy = find(pv, y);
	if (fx == fy) {
		root[cv] = root[pv];
		return;
	}
	int sx = querydep (root[pv], 1, n, fx), sy = querydep (root[pv], 1, n, fy);
	if (sx > sy) swap(fx, fy), swap(sx, sy);
	root[m + 1] = modifyfa (root[pv], 1, n, fx, fy);
	root[cv] = modifydep (root[m + 1], 1, n, fy, max(sy, sx + 1));
}
int main () {
    ios::sync_with_stdio(false);
    cin.tie(0);
	cin >> n >> m;
	root[0] = build (1, n);
	int op, a, b, k;
	for (int i = 1;i <= m;i++) {
		cin >> op;
		if (op == 1) {
			cin >> a >> b;
			merge (i - 1, i, a, b);
		}
		else if (op == 2) {
			cin >> k;
			root[i] = root[k];
		}
		else {
			cin >> a >> b;
			cout << (find (i - 1, a) == find (i - 1, b) ? 1 : 0) << endl;
			root[i] = root[i - 1];
		}
	}
	return 0;
}

离线算法

作为一种可持久化数据结构,可以维护时间轴。
例题:Luogu P3834 【模板】可持久化线段树 2 我知道这不是离线,但原理一样的
我们可以将数组下标抽象为时间轴,以此来建版本,用可持久化线段树维护桶的前缀和(即第 \(i\) 个版本的 \(sum_j\) 表示 \(a_{1\dots i}\) 中 \(j\) 出现的次数)。
查询时我们将两个版本的数据相减就得到了 \(a_{l\dots r}\) 中每个数的出现次数,线段树上二分即可。
代码屎山

#include<bits/stdc++.h>
using namespace std;

const int MAXN = 200005, MAXM = 200005;
struct node {
    node *ls, *rs;
    int lb, rb, sum;
    node () {
        sum = lb = rb = 0;
        ls = rs = NULL;
    }
};
node *build(int lb, int rb) {
    node *newnode = new node;
    newnode -> lb = lb;
    newnode -> rb = rb;
    if (lb != rb) {
        int mid = (lb + rb) >> 1;
        newnode -> ls = build(lb, mid);
        newnode -> rs = build(mid+1, rb);
    }
    return newnode;
}
node *modify (node *base, int p, int x) {
    node *cur = new node;
    (*cur) = (*base);
    cur -> sum += x;
    if (cur -> lb != cur -> rb) {
        int mid = (cur -> lb + cur -> rb) >> 1;
        if (p <= mid) {
            cur -> ls = modify (base -> ls, p, x);
        }
        else cur -> rs = modify (base -> rs, p, x);
    }
    return cur;
}
int query (node *lr, node *rr, int k) {
    if (lr -> lb == lr -> rb) return lr -> lb;
    else {
        int lct = rr -> ls -> sum - lr -> ls -> sum;
        if (lct < k) {
            return query (lr -> rs, rr -> rs, k - lct);
        }
        else return query (lr -> ls, rr -> ls, k);
    }
}
int n, m, a[MAXN], b[MAXN];
node *root[MAXN];
int main () {
    cin >> n >> m;
    for (int i = 1;i <= n;i++) {
        cin >> a[i];
        b[i] = a[i];
    }
    sort(b+1, b+n+1);
    int l = unique(b+1, b+n+1) - b - 1;
    for (int i = 1;i <= n;i++) a[i] = lower_bound(b+1, b+l+1, a[i]) - b;
    root[0] = build(1, n);
    for (int i = 1;i <= n;i++) root[i] = modify(root[i - 1], a[i], 1);
    int lb, rb, k;
    while (m--) {
        cin >> lb >> rb >> k;
        cout << b[query(root[lb - 1], root[rb], k)] << endl;
    }
    return 0;
}

总结

可持久化线段树是很多可持久化数据结构的基础,常用于在其上方搭载其他可持久化数据结构。也常用于辅助基于时间轴的离线算法。

例题

Luogu P1972 [SDOI2009] HH的项链
\(\bf\color{red}{Luogu\ P2617\ Dynamic\ Rankings}\)
Luogu P4216 [SCOI2015] 情报传递
Luogu P7357 「PMOI-1」中位数

标签:知识点,cur,int,线段,tr,son,tl,sum,梳理
From: https://www.cnblogs.com/mindeveloped/p/fusion-segtrees.html

相关文章

  • 线段树模板 洛谷P3374 【模板】树状数组 1
    题目传送门题目描述如题,已知一个数列,你需要进行下面两种操作:1.将某一个数加上x2.求出某区间每一个数的和输入格式第一行包含两个整数N、M,分别表示该数列数字的个数和操作的总个数。第二行包含N个用空格分隔的整数,其中第i个数字表示数列第i项的初始值。接下来M行每行包含3......
  • 扫描线 - 知识点梳理
    扫描线算法可解决平面内平行坐标轴的线段有关的问题,例如求平面上平行于坐标轴的矩形的面积并,其原理在于模拟一条扫描线从下往上扫描。线段树是一种灵活的LeafyTree,可以灵活地扫描线上统计线段的分布情况,将一部分信息储存在分支节点上,另一部分信息下传至叶子节点,因此线段树是扫描......
  • qml知识点概括一
    目录1.qml语言是什么?有什么优点?2.qml的语言的本质是什么3.qml文件文档结构以及界面布局元素4.qml常见的对象类型(1)Item(2)addImportPath设置qml模块路径(3)setContextProperty设置qobject对应的qml文档属性5.window下IDE的选择6pro文件的TEMPLE(1)QT工程pro文件模板变量(TEMPLA......
  • 答疑知识点
    1.re_path和path有什么区别1.表象上的区别pathpath里面支持固定,还有动态参数int,str,uuid,path re_pathre_path支持正则表达式2.源码上的区别 底层都是偏函数,对应的都是_path函数,本质上传递的Pattern不同,而day03源码里面分析,匹配时会找到外部resolver方法,再......
  • 【做题笔记】线性dp——线段树优化
    线段树优化是用来对于\(DP\)数组区间赋值的。主要是区间取最值来优化线性dp真没什么可写的了挂两个题目:P4644[USACO05DEC]CleaningShiftsSP1545[USACO04DEC]DividingthePathGUSACO的小清新线段树优化dp好题......
  • 【线段树】【leetcode 729. 我的日程安排表 I】
    classMyCalendar{classSeg{intl;intr;booleanval;Segleft;Segright;publicSeg(intx,inty){this.l=x;this.r=y;this.val=false;this.l......
  • 线段树 算法笔记
    已知一个长度为\(n\)的序列\(a\),共有\(m\)次操作,每次操作如下:将某区间每一个数加上\(k\)。求出某区间每一个数的和。Luogu-P3372【模板】线段树1之前学过一个算法叫做树状数组,它的本质就是将一个\([1,x]\)的区间二进制拆分装化成若干个区间,数组里的每一个元素......
  • Trie树 - 知识点梳理
    介绍Trie树,又名字典树,顾名思义就是为多个字符串的存贮与查找而生的,和现实中的字典差不多,其实就是一种字符查找自动机。通过对被查找串预处理,梳理为树形结构,在每次查找\(S\)时复杂度可以达到\(O(|S|)\)(而朴素查找复杂度为\(O(|S|+\sum_i|t_i|)\)),且占的空间比单独存贮字符......
  • jvm学习-垃圾回收的一些知识点
    部分图片和描述来自参考资料,非原创对象回收处理过程如何标定对象是否存活两种方法:引用计数方法可达性分析算法引用计数方法就和ReentrantLock可重入锁一样,内部维系着一个state,当同个线程重入结束后就会归零,但是这种方法有点问题publicstaticvoidte......
  • 线段树
    代码思路主体部分:初始化,修改,查询(即build,update,query三个函数)辅助部分:区间值维护,打懒标记,消懒标记(即push_up,add_tag,push_down三个函数)简化部分:自定义数据类型,左右儿子自助计算(structTree,ls&rs函数)原理(极简)树+分块=线段树,无尽的二分代码注释#include<bits/stdc++.h......