首页 > 其他分享 >【数据结构】- 线段树

【数据结构】- 线段树

时间:2023-10-04 20:16:41浏览次数:47  
标签:his int 线段 tr rid 数据结构 id

线段树

简介

线段树是可以维护 区间信息 的数据结构。线段树将每个长度不为 \(1\) 的区间划分成左右两个区间递归求解,故把整个线段划分为一个树形结构,通过合并左右两区间信息来求得该区间的信息。
image

根据建树方式可分为普通线段树和动态开点线段树。

根据区间信息可分为普通线段树、权值线段树和李超线段树。

根据维护信息可分为普通线段树、吉司机线段树和可持久化线段树。

普通线段树

结构

用 \(2id\) 表示左儿子, \(2id+1\) 表示右儿子,建成完全二叉树。

对于树上每个节点用结构体维护管辖区间端点(写了可减小码量)、区间信息。

#define lid (id<<1)
#define rid (id<<1|1)
#define mid (tr[id].l+tr[id].r>>1)
struct seg_tree{
    int l, r;
    int sum, add, mul;
} tr[N*4];

性质

需要开到 \(4n-5\) 的数组存树。

操作

建树

对左右递归建树。

void build(int id, int l, int r) {
    tr[id].l = l; tr[id].r = r;
    if (l == r) {
        tr[id].sum = a[l];
        return;
	}
    build(lid, l, mid);
  	build(rid, mid+1, r);
    pushup(id);
}

上传

递归回来后用子区间信息更新父区间。

void pushup(int id) {
    tr[id].sum = tr[lid].sum + tr[rid].sum;
}

查询

将查询区间拆到各个最大分管区间后合并各区间信息。

int query(int id, int l, int r) { //如果不存管辖区间就要带着当前区间的s、t
    if (tr[id].l >= l && tr[id].r <= r) return tr[id].sum;
    pushdown(id);
    int res = 0;
    if (l <= mid) res += query(lid, l, r);
    if (r > mid) res += query(rid, l, r);
    return res;
}

修改&下传

对一段区间操作时不难发现可以找到几个极长区间做到其并集刚好覆盖这个区间而其交集为空。然后在这些区间上打上懒标记(\(lazy\))表示对这些区间及其下辖区间有修改操作。这样就不用递归到长度为 \(1\) 的区间再向上维护了,只有需要访问下辖区间时再下传修改操作影响。所以对于懒标记的理解其实就是一段操作序列集成在几个标记上。将子节点的标记与父节点标记合并后就等于将父节点修改序列接在了子节点已有序列之后以计算总影响。

void pushdown(int id) {
	tr[lid].sum = tr[lid].sum * tr[id].mul + (tr[lid].r - tr[lid].l + 1) * tr[id].add;
	tr[rid].sum = tr[rid].sum * tr[id].mul + (tr[rid].r - tr[rid].l + 1) * tr[id].add;
	tr[lid].mul = tr[lid].mul * tr[id].mul;
	tr[rid].mul = tr[rid].mul * tr[id].mul;
	tr[lid].add = tr[lid].add * tr[id].mul + tr[id].add;
	tr[rid].add = tr[rid].add * tr[id].mul + tr[id].add;
	tr[id].add = 0;tr[id].mul = 1;
}
void modify(int id, int l, int r, int add, int mul) {
	if (tr[id].l >= l && tr[id].r <= r) {
		tr[id].sum = tr[id].sum * mul + (tr[id].r - tr[id].l + 1) * add;
		tr[id].mul = tr[id].mul * mul;
		tr[id].add = tr[id].add * mul + add;
		return;
	}
	pushdown(id);
	if (l <= mid) modify(lid, l, r, add, mul);
	if (r > mid) modify(rid, l, r, add, mul);
	pushup(id);
	return;
}

动态开点线段树

结构

为了节省空间,我们只对访问到的节点建点。此时就应该更改左右儿子表示方式,直接记录左右儿子的下标。代码变动很小,将#definelid、rid改为ls[id]、rs[id]表示即可。有时需要进行节点回收,防止重复占用。

#define m (s+t>>1)
int ls[N], rs[N], sum[N], tot;
int rub[N], cnt; //回收站
void del(int &x) {
    ls[x] = rs[x] = sum[x] = 0;
    rub[++ cnt] = x; x = 0;
}
int ins() {return cnt ? rub[cnt --] : ++ tot;}

性质

单次操作的时间复杂度是不变的,为 \(O(\log n)\)。由于每次操作都有可能创建并访问全新的一系列结点,因此次 \(m\) 单点操作后结点的数量规模是 \(O(m\log n)\)。最多也只需要个 \(2n-1\) 结点,没有浪费。

操作

修改

void modify(int& id, int s, int t, int l, int r, int v) {  // 一切以空间为主,所以就带着管辖区间端点递归了
    if (!id) id = ins();  // 当结点为空时,创建一个新的结点
    if (s >= l && t <= r) {sum[id] += v; return;}
    if (l <= m) modify(ls[id], s, m, l, r, v);
    if (r > m) modify(rs[id], m+1, t, l, r, v);
    pushup(id);
}

查询

int query(int id, int s, int t, int l, int r) {
    if (!id) return 0;
    if (s >= l && t <= r) return sum[id];
    int ans = 0;
    if (l <= m) ans += query(ls[id], s, m, l, r);
    if (r > m) ans += query(rs[id], m+1, t, l, r);
    return ans;
}

有时要对同一值域维护多棵线段树,然后操作这些线段树里的信息。常在权值线段树上进行,需要先记录下每棵线段树的根。

int rt[N], idx;

合并

形象地说,合并过程就是将两棵线段树「重叠」起来得到一棵新的线段树。

//rt[a] = merge(rt[a], rt[b], 1, n);
int merge(int a, int b, int s, int t) {
	if (!a || !b) return a | b;
	if (l == r) {sum[a] += sum[b]; del(b); return a;}
	ls[a] = merge(ls[a], ls[b], l, m);
	rs[a] = merge(rs[a], rs[b], m+1, r);
	pushup(a); del(b);
	return a;
}

分裂

可以合并就能分裂。即将一段区间从总区间里「抽出」,从 \(1\) 节点往下递归找交集即可。

//split(rt[a], rt[++ idx], 1, n, l, r);
void split(int &a, int &b, int s, int t, int l, int r) {
    if (!a) return;
	if (t < l || r < s) return; //判交集
	if (l <= s && t <= r) {b = a; a = 0; return;}
  	if (!b) b = ins();
  	if (l <= m) split(ls[a], ls[b], s, m, l, r);
  	if (r > m) split(rs[a], rs[b], m+1, t, l, r);
  	pushup(a); pushup(b);
}

猫树

能高速查询,接触不多,不忙着学,先鸽了(

李超线段树

结构

李超线段树作为一种特殊的线段树,其维护信息为平面上横坐标在一定范围内的一段崎岖不平的「表面」。

image

形式化描述为:给定一个数 \(k\),维护与直线 \(x=k\) 相交的线段中,交点纵坐标最大的线段的编号。

线段树结构与普通线段树差别不大,需要存储的信息如下:

const double eps = 1e-9;
struct seg_tree{
    int l, r, f;
} tr[N*4];
double calc(int line, int x) {return k[line] * x + b[line];}
int cmp(double x, double y) {return x - y > eps ? 1 : (y - x > eps ? -1 : 0);}

性质

这种问题有如下解法:记录懒标记为除开祖先节点已做标记的线段外,完全覆盖此节点区间且与区间的「中线」交点的纵坐标最大的线段,表示该线段可能作为询问其下辖区间时的答案。

这其实是一种标记永久化技巧,在查询至「底部」的过程中不断比较、更新答案线段即可。

而在插入一条线段时,要对一个区间进行标记。没有标记就将这条线段打上就好。否则就需要处理这样两条线段:已作标记的 \(f\) 和待做处理的 \(g\)。当然,这样定义的懒标记无法合并,其实也无需合并,得益于标记永久化,我们现在只需要考虑如何正确地向下递归更新标记。大体思路如下图:

image

如果在中点处旧标记 \(f\) 不如 \(g\) 就 \(swap\) 一下,使得我们总是在用更「糟」,可覆盖区间更小的线段讨论向下更新。

对于现在的 \(f\) 和 \(g\),按照定义,先将 \(f\) 置为当前区间懒标记,再考虑 \(g\) 如何向下递归:

如果两条线段相交,呈“✕”形,则 \(g\) 只能在区间一侧更优(上图例子中为右侧)。具体在哪一侧取决于区间哪一个端点上 \(f\) 比 \(g\) 更劣。递归到对应子区间即可。

如果两条线段不交,呈“⇉”形,则 \(g\) 不可能成为答案,直接返回。

操作

修改&下传

void update(int id, int g) {
    int res = cmp(calc(g, mid), calc(tr[id].f, mid));
    if (res == 1 || (!res && g < tr[id].f)) swap(g, tr[id].f);
    int bl = cmp(calc(g, tr[id].l), calc(tr[id].f, tr[id].l)),
        br = cmp(calc(g, tr[id].r), calc(tr[id].f, tr[id].r));
    if (bl == 1 || (!bl && g < tr[id].f)) update(lid, g);
    if (br == 1 || (!br && g < tr[id].f)) update(rid, g);
}
void modify(int id, int l, int r, int g) {
    if (tr[id].l >= l && tr[id].r <= r) {
        update(id, g); return;
    }
    if (l <= mid) modify(lid, l, r, g);
    if (r > mid) modify(rid, l, r, g);
}

查询

pair<double, int> Max(pdi x, pdi y) {
    if (cmp(x.first, y.first) == -1) return y;
    if (cmp(x.first, y.first) == 1) return x;
    return x.second < y.second ? x : y;
}
pair<double, int> query(int id, int x) {
	double res = calc(tr[id].f, x);
	if (tr[id].l == tr[id].r) return {res, tr[id].f};
	if (x <= mid) return Max({res, tr[id].f}, query(lid, x));
	else return Max({res, tr[id].f}, query(rid, x));
}

吉司机线段树

结构

其实就是比维护加、求和操作的普通线段树多用了亿些懒标记。

struct seg_tree {
	int l, r;
	int mx, se, sum, his, cnt;
	int lzy_mx, lzy_se, lzy_his, lzy_res;
} tr[N*4];

在历史最值中还有区间赋值与区间加操作同时出现的情况。节点可以维护如下信息:

struct seg_tree{
	int l, r;
	int mx, his;
	int lzy, lzy_his, tag, tag_his;
	bool vis;
} tr[N*4];

性质

是支持区间最值与历史最值操作的线段树。

区间最值是将区间 \([l,r]\) 的元素对给定的 \(x\) 取 \(\min/\max\)。如区间取 \(\min\),意味着操作的对象是这个区间中大于的 \(x\) 数。

则有如下解法:每个结点维护该区间的最大值 \(mx\)、次大值 \(se\)、区间和 \(sum\) 以及最大值的个数 \(cnt\)。每次递归直至找到 \(se<x\le mx\) 的区间,将区间和减去 \(cnt\times (mx-x)\),将 \(mx\) 赋值为 \(x\);或者 \(mx\le x\) 后直接返回。

历史最值是指定的区间 \([l,r]\) 里所出现过的最大的数。解决这类问题需要在维护了最大值及其懒标记 \(lzy\_mx\) 的基础上再维护历史最大值 \(his\) 及其懒标记 \(lzy\_his\)。解法即每次尝试用当前节点的 \(mx\)(表示这个节点当前的操作序列最终结果)加上父亲节点下传的 \(Fa(lzy\_his)\)(表示父亲节点记录的操作过程中前缀和达到的最值)来更新这个节点的 \(his\)。同理,用 \(lzy\_mx\) 加上下传的 \(Fa(lzy\_his)\) 尝试更新 \(lzy\_his\)。

image

同时存在区间最值操作时维护非最大值的懒标记同理。

在历史最值问题中有时区间赋值会与区间加一同出现。此时我们自然需要维护一个赋值懒标记 \(tag\)。但同时维护两种懒标记有些复杂,于是有了如下思路:将赋过一次值后的操作都看作赋值,只用赋值懒标记 \(tag\) 进行维护。因为一旦对某区间赋过了值 \(x\),这个区间再加一个数 \(y\) 就相当于重新赋值成了 \(x+y\)。所以我们在节点处维护一个 \(bool\) 变量判断是否进行过赋值操作。自然从父节点接过来操作序列时也得分讨是接入的区间加标记还是赋值标记。这里以子节点已赋值为例。

image

操作

历史最值

void modify_add / chg(int id, int l, int r, int v) {
	if (tr[id].l >= l && tr[id].r <= r) {
		add / chg(id, v, v); return;
	}
	pushdown(id);
	if (l <= mid) modify(lid, l, r, v);
	if (r > mid) modify(rid, l, r, v);
	pushup(id);
}

区间最值

void modify_min(int id, int l, int r, int v) {
	if (tr[id].mx <= v) return;
	if (tr[id].l >= l && tr[id].r <= r && tr[id].se < v) {
		update(id, v - tr[id].mx, v - tr[id].mx, 0, 0); return;
	}
	pushdown(id);
	if (l <= mid) modify_min(lid, l, r, v);
	if (r > mid) modify_min(rid, l, r, v);
	pushup(id);
}

下传&上传&更新标记

既然懒标记这么多,下传函数就显得尤其重要。并且合并时最大值较小的子区间自动降级为次大值不会被下传到最大值专享的懒标记,更新时注意分讨。上传同理,分讨上传 \(mx\) 与对应 \(cnt\)。区间与历史最值缝合时更为繁琐。可分为pushdownupdate函数专门处理懒标记。对于update,结合上图,我们只需再明确更新顺序即可。

void pushup(int id) {
	tr[id].sum = tr[lid].sum + tr[rid].sum;
	tr[id].mx = max(tr[lid].mx, tr[rid].mx);
	tr[id].his = max(tr[lid].his, tr[rid].his);
	if (tr[lid].mx > tr[rid].mx) {
		tr[id].cnt = tr[lid].cnt;
		tr[id].se = max(tr[lid].se, tr[rid].mx);
	}
	else if (tr[lid].maxst < tr[rid].maxst) {
		tr[id].cnt = tr[rid].cnt;
		tr[id].se = max(tr[lid].mx, tr[rid].se);
	}
	else {
		tr[id].cnt = tr[lid].cnt + tr[rid].cnt;
		tr[id].se = max(tr[lid].se, tr[rid].se);
	}
}
void update(int id, int l1, int l2, int l3, int l4) {
    //l1对应下传的Fa(lzy_mx),l2对应下传的Fa(lzy_his),l3对应下传的Fa(lzy_se),l4对应下传的Fa(lzy_res)
    //lzy1、lzy2、lzy3、lzy4定义即一一对应于当前区间懒标记
	tr[id].sum += l1*tr[id].cnt + l3*(tr[id].r-tr[id].l-tr[id].cnt+1);
	tr[id].his = max(tr[id].his, tr[id].mx + l2);
	tr[id].mx += l1;
	if (tr[id].se != -INF) tr[id].mx += l3;
	lzy2(id) = max(lzy2(id), lzy1(id) + l2); 
	lzy4(id) = max(lzy4(id), lzy3(id) + l4); 
	lzy1(id) += l1; lzy3(id) += l3;
}
void pushdown(int id) {
	int tmp = max(tr[lid].mx, tr[rid].mx);
	if (tr[lid].mx == tmp) update(lid, lzy1(id), lzy2(id), lzy3(id), lzy4(id));
	else update(lid, lzy3(id), lzy4(id), lzy3(id), lzy4(id));
	if (tr[rid].mx == tmp) update(rid, lzy1(id), lzy2(id), lzy3(id), lzy4(id));
	else update(rid, lzy3(id), lzy4(id), lzy3(id), lzy4(id));
	lzy1(id) = lzy2(id) = lzy3(id) = lzy4(id) = 0;
}
-----------------------------------------------------------------------------------------------------
void chg(int id, int l1, int l2) {//l1是Fa(tag),l2是Fa(tag_his)
	if (!tr[id].vis) tr[id].vis = 1, tr[id].tag_his = l2;
	else tr[id].tag_his = max(tr[id].tag_his, l2);
    tr[id].his = max(tr[id].his, l2);
    tr[id].tag = tr[id].mx = l1;
}
void add(int id, int l1, int l2) {//l1是Fa(lzy),l2是Fa(lzy_his)
    if (tr[id].vis) tr[id].tag_his = max(tr[id].tag_his, tr[id].tag + l2), tr[id].tag += l1;
	else tr[id].lzy_his = max(tr[id].lzy_his, tr[id].lzy + l2), tr[id].lzy += l1;
	tr[id].his = max(tr[id].his, tr[id].mx + l2);
    tr[id].mx += l1;
}
void pushdown(int id) {
	add(lid, tr[id].lzy, tr[id].lzy_his);
	add(rid, tr[id].lzy, tr[id].lzy_his);
	tr[id].lzy = tr[id].lzy_his = 0;
	if (!tr[id].vis) return;
	chg(lid, tr[id].tag, tr[id].tag_his);
	chg(rid, tr[id].tag, tr[id].tag_his);
	tr[id].vis = tr[id].tag = tr[id].tag_his = 0;
}

修改&查询

modify区间加函数大致不变。可直接调用updatequery区间求和、取最值函数大致不变。

标签:his,int,线段,tr,rid,数据结构,id
From: https://www.cnblogs.com/Arson1st/p/17742641.html

相关文章

  • 【数据结构】- 堆
    堆简介堆是可以维护最值的数据结构。其每个节点有一个键值\(val\),堆将节点按键值确定父亲/儿子关系,故把所有节点连为一棵树,通过根找到最值。根据祖先关系可分为两类——大根堆以根节点键值最大,向叶节点递减。小根堆以根节点键值最小,向叶节点递增。根据支持操作可分为堆......
  • 数据结构-并查集
    并查集的使用范围:1.合并集合2.查询两元素是否属于同一集合   高级用法:  3.进行集合划分<带权并查集>  4.连通块块数查询&块内元素个数统计<连通图>  5.撤销合并<可持久化并查集>//本文暂不涉及,我还不会并查集基本操作:#definerep(i,n)for(int......
  • 点赞功能改进-Redis数据结构设计
        ......
  • note 线段树
    适用场景:不断区间修改、区间询问。假设我们要区间求和,\(tree\)的含义:区间的和,其两个子节点为这个区间分为两半的和。我们把一个数组\(a\)看作一颗树\(tree\),例:112333对应的\(tree\)(\(()\)里是编号,\([]\)里是对应的区间):(1)13[1,6]/......
  • 线段树专题复习
    今天的主题是线段树专题复习!(什么?是昨天的?不听不听,只要我不说都不知道我鸽了一天!)好了,言归正传,我们来看一下今天的知识点们吧。Part1线段树自己不想讲了,想看的移步其他博客想看踢我,今天没时间了Part2一些优化ZKW线段树俗称重口味线段树,是一种不用递归实现的线段树,常数和......
  • 数据结构之"顺序表"
    前言......
  • 第03章 Python的数据结构、函数和文件
    本章讨论Python的内置功能,这些功能本书会用到很多。虽然扩展库,比如pandas和Numpy,使处理大数据集很方便,但它们是和Python的内置数据处理工具一同使用的。我们会从Python最基础的数据结构开始:元组、列表、字典和集合。然后会讨论创建你自己的、可重复使用的Python函数。最后,会学习P......
  • 高级数据结构--树状数组
    一维树状数组单点修改-区间查询输入32123120213输出6数据范围对于所有数据,\(1≤n,q≤10^6,∣a[i]∣≤10^6,1≤l≤r≤n,∣x∣≤10^6\)。点击查看代码#include<bits/stdc++.h>#defineIOSios::sync_with_stdio(false);cin.tie(nullptr),cout.tie(nu......
  • 算法:线段树
    算法:线段树哦吼!终于来学线段树啦~~拖了好久都没有敢学,主要是基础知识点不熟,代码能力太弱。但是现在已经是时候了。来看:线段树(SegmentTree)几乎是算法竞赛最常用的数据结构了,它主要用于维护区间信息(要求满足结合律)。与树状数组相比,它可以实现\(O(log⁡\n)\)的区间修改,......
  • 【数据结构】3.跳表和散列
    1.顺序链表字典1.1字典抽象父类#pragmaonceusingnamespacestd;template<classK,classE>classdictionary{public:virtual~dictionary(){}//返回字典是否为空virtualboolempty()const=0;//返回有多少键值对virtualintsize()co......