首页 > 其他分享 >线段树 Ⅰ

线段树 Ⅰ

时间:2024-08-21 10:07:28浏览次数:7  
标签:node int 线段 tree 区间 节点

前言

线段树用于维护数列区间上的值,可以在 \(O(\log n)\) 的时间复杂度内实现单点,区间修改及查询。并且所有满足结合律的运算都可以用线段树进行加速。

基本操作

建树

给你长度为 \(n\) 的正整数序列 \(A\),要你实现单点修改与区间查询。(加和)

这就是线段树的基本题目。线段树,字面上来看就是把线段映射到树上,可以想到每个节点代表一个区间。

那么我们可以把每个区间分成两半,作为节点的左右子节点。

最常见的树左右子节点就是 \(rt \times 2\) 和 \(rt \times 2 + 1\)(堆式存储)。可以直接开 \(4\) 倍空间。

可以知道,如果有 \(n\) 个叶子节点,最大值是 \(2 ^ {\lfloor \log n \rfloor + 1}\) 的。当 \(n = 2^x + 1\) 时,\(\dfrac{2 ^ {\lfloor \log n \rfloor + 1}}{n}\) 取得最大值 \(4n - 5\)。

如图就是 \(n = 5, A = \{1, 2, 3, 4, 5\}\) 时的线段树(加和)。

我们可以发现,对于每个区间 \([x,y]\),都被分成了 \([x, \lfloor \dfrac{x + y}{2} \rfloor]\) 和 \([\lfloor \dfrac{x + y}{2} \rfloor + 1, y]\) 这两个区间。如果我们令根节点的深度为 \(0\),那么可以发现整棵树的深度不超过 \(\log n + 1\),这也就意味着我们至多经过 \(\log n\) 条边就可以找到一个区间。

建树时我们可以递归建树,如果 \(l = r\),也就意味着到了单点,这时直接赋值。返回时再更新节点值即可。

void pushup (int node) { tree[node] = tree[node << 1] + tree[(node << 1) + 1]; }

void build (int node, int l, int r){
	if (l == r){
		tree[node] = ta[l];
		return ;
	}
	
	int mid = l + ((r - l) >> 1);
	build ((node << 1), l, mid);
	build ((node << 1) + 1, mid + 1, r);
  	pushup (node);
}

因为深度不超过 \(\log n +1\),所以复杂度应该是 \(O(\log n)\) 的。

单点修改

当我们单点修改时,只需要想建树那样遍历,如果遍历到单点就直接修改,然后返回,更新。

void modify (int node, int l, int r, int a, int c){
	if (l == r){
		tree[node] = c;
		return ;
	}
	
	int mid = l + ((r - l) >> 1);
	if (a <= mid) modify ((node << 1), l, mid, a, b);
	else modify ((node << 1) + 1, mid + 1, r, a, b);
  	pushup (node);
}

区间查询

单点查询很好做,关键就是区间查询。

对于查询区间,我们一定可以把它分成几个序列上的区间,对应树上的节点。关键就是这些节点。

设当前遍历到的区间为 \([l,r]\),要查询的区间为 \([s,t]\)。

如果要查询的区间在左边,可以发现一定是 \(s \le \lfloor \dfrac{x + y}{2} \rfloor\);如果在右边,那么一定是 \(t \gt \lfloor \dfrac{x + y}{2} \rfloor\)。没有等于的情况是因为向下取整,如果等于就已经被左边的区间包含了,不能算重,所以没有等于。

那么把左右区间累加起来,就是这个区间的贡献,直接返回即可。

long long query (int node, int l, int r, int s, int t) {
	if (s <= l && r <= t) return tree[node];
	
	int mid = l + ((r - l) >> 1);
	long long ret = 0;
	if (s <= mid) ret += query (node << 1, l, mid, s, t);
	if (t > mid) ret += query ((node << 1) + 1, mid + 1, r, s, t);
	return ret;
}

至此,基本的,没有任何其他东西的一个纯粹的线段树我们就打完了。可以发现每个操作都是基于线段树的层数来定的复杂度,所以都是 \(O(\log n)\) 的。

但是拓展操作远远不止于此,上题!

区间修改

P3372 【模板】线段树 1

你长度为 \(n\) 的正整数序列 \(A\),要你实现区间修改与区间查询。

这题唯一的不同就在于区间修改。如果每个暴力单点修改的话,复杂度 \(O(n \log n)\),会爆炸。

这时就要引入一种叫懒惰标记的东西。

还是遍历,当这个区间在查询区间中,我们就先修改它的值,给它挂上懒惰标记,但不修改它子节点的值。

你可能已经想到了,再一次遍历的时候就可以下传懒惰标记,以达到区间修改的目的。

void pushdown (int node, int l, int r) {
	int ls = node << 1, rs = (node << 1) + 1, mid = l + ((r - l) >> 1);
	if (lazy[node]) {
		int x = lazy[node];
		tree[ls] += (mid - l + 1) * x; tree[rs] += (r - mid) * x;
		lazy[ls] += x; lazy[rs] += x;
		lazy[node] = 0;
	}
}

void modify (int node, int l, int r, int s, int t, long long c) {
	if (s <= l && r <= t) {
		tree[node] += (r - l + 1) * c;
		lazy[node] += c;
		return ;
	}
	
	int mid = l + ((r - l) >> 1);
	pushdown (node, l, r);
	if (s <= mid) modify (node << 1, l, mid, s, t, c);
	if (t > mid) modify ((node << 1) + 1, mid + 1, r, s, t, c);
	pushup (node);
}

动态开点线段树

线段树的时间复杂度虽然小,但是空间复杂度大,有 \(4n\),如何来优化呢?

我们可以把懒惰标记的思想用在节点上面,就成了动态开点。也就是访问到这个节点时再创建这个节点,但不能用 \(rt \times 2\) 这种来存储了,必须另外专门存储。

就拿单点修改来举例吧。

int n, tot, rt;
int tree[N << 1], ls[N << 1], rs[N << 1];

void modify (int &node, int l, int r, int s, int c) {
	if (!node) node = ++tot;
	if (l == r) {
		tree[node] = c;
		return ;
	}
  
	int mid = l + ((r - l) >> 1);
	if (s <= mid) modify (ls[node], l, mid, s, c);
	else modify (rs[node], mid + 1, r, s, c);
	pushup (node);
}

小清新线段树

上题。

P4145 上帝造题的七分钟 2 / 花神游历各国

给你一个长度为 \(n\) 的正整数序列 \(A\),要实现区间开方与区间求和

注意到,区间开方是从未有过的操作,只能一个一个单点修改,复杂度肯定爆炸。

但是 \(\sqrt 1 = 1,\sqrt 0 = 0\),我们可以另存一个最大值。如果这个区间的最大值小于等于 \(1\) 的话,就不用修改了。

数列里的数不超过 \(10^{12}\)。

\[\sqrt{10^{12}} = 10^6 \to \sqrt{10^6} = 1000 \to \sqrt{1000} \approx 31.62 \to \sqrt{31.62} \approx 5.62 \to \sqrt{5.62} \approx 2.37 \to \sqrt{2.37} \approx \approx 1.53 \]

也就是说至多 \(6\) 次就可以让数列里面的数成为 \(1\),所以这个优化效果拔群。

void modify (int node, int l, int r, int s, int t) {
	if (l == r) {
		tree[node] = sqrt (tree[node]);
		mx[node] = sqrt (mx[node]);
		return ;
	}
	
	int mid = l + ((r - l) >> 1);
	if (s <= mid && mx[node << 1] > 1) modify (node << 1, l, mid, s, t);
	if (t > mid && mx[(node << 1) + 1] > 1) modify ((node << 1) + 1, mid + 1, r, s, t);
	pushup (node);
}

权值线段树

我们都知道,线段树用来维护的是序列上的元素。那如果维护序列上的元素个数呢?这就是权值线段树。

P1908 逆序对

给你一个长度为 \(n\) 的正整数序列 \(A\),需要你找出 \(A\) 中所有满足 \(i \lt j\) 且 \(A_i \gt A_j\) 的 \((i,j)\) 二元组个数。

这道题维护的就是元素个数,而不是元素值。

用权值线段树解题的话,我们需要离散化,然后把每个下标看做元素值,元素值则看做下标对应元素个数。

就像这样,然后我们就可以在上面做操作了。

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

const int N = 5e5 + 5;
int n;
long long ans;
long long A[N], a[N];
long long tree[N << 2];

void pushup (int node) {tree[node] = tree[node << 1] + tree[(node << 1) + 1];}

void modify (int node, int l, int r, int c) {
	if (l == r) {
		tree[node]++;
		return ;
	}
	
	int mid = l + ((r - l) >> 1);
	if (c <= mid) modify (node << 1, l, mid, c);
	else modify ((node << 1) + 1, mid + 1, r, c);
	pushup (node);
}

int query (int node, int l, int r, int s, int t) {
	if (s <= l && r <= t) return tree[node];
	
	int mid = l + ((r - l) >> 1), ret = 0;
	if (s <= mid) ret += query (node << 1, l, mid, s, t);
	if (t > mid) ret += query ((node << 1) + 1, mid + 1, r, s, t);
	pushup (node);
	return ret;
}

int main () {
	
	scanf ("%d", &n);
	for (int i = 1; i <= n; ++i) {
		scanf ("%lld", &a[i]);
		A[i] = a[i];
	}
	sort (A + 1, A + n + 1);
	int ret = unique (A + 1, A + n + 1) - A - 1;
	for (int i = 1; i <= n; ++i) a[i] = lower_bound (A + 1, A + ret + 1, a[i]) - A;
	
	for (int i = 1; i <= n; ++i) {
		ans += query (1, 1, N, a[i] + 1, N);
		modify (1, 1, N, a[i]);
	}
	
	printf ("%lld", ans);
	
	return 0;
}

可持久化线段树

可持久化也分为部分可持久化与完全可持久化。

一般可持久化的题目都会让你记录一个历史版本,这个历史版本就是每一个操作所产生的新的线段树。

也就是说,如果我在原本的线段树上做了一个单点修改操作,那么就会多一个历史版本是操作后的线段树,版本与操作对应。

部分可持久化就是可以访问所有历史版本,但是只有最新版本可以修改。

完全可持久化就是可以访问,修改所有历史版本。

如果我们对每一个操作都开一个线段树呢?

ver. 0 就是未做修改的线段树,ver. 1 是单点修改。

都知道,这样肯定会爆空间,但是我把它们与原线段树比较,发现修改经过的节点是条链。

那么我们为什么不考虑把这条链加到原线段树上面呢?

这时候,主席树(可持久化权值线段树)便应运而生了。

可以发现,新增的节点不仅构成了链,还连接了对应的节点。并且节点数是不超过 \(\log n\) 的。

但是主席树必须要用动态开点线段树,不然空间就会像前面一样爆掉。

我们可以对每一颗新增出来的链开一个数组来记录它的根节点,方便遍历,操作基本跟线段树是一样的。


P3834 【模板】可持久化线段树 2

静态区间第 \(k\) 小。

我们可以把这个问题拆成区间 \([1,r]\) 的静态第 \(k\) 小,就可以用主席树来维护。那么如果我们要求区间 \([l,r]\),只需要用一下前缀和的思想,用 \([1,r] - [1,l-1]\) 就可以了。

注意离散化。

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

const int N = 2e5 + 5;
int n, m, tot;
int a[N], b[N], len;
int rt[N];

struct Seg {
	int ls, rs;
	int sum;
} tree[N * 20];

int build (int l, int r) {
	int node = ++tot;
	if (l == r) return node;
	
	int mid = l + ((r - l) >> 1);
	tree[node].ls = build (l, mid);
	tree[node].rs = build (mid + 1, r);
	return node;
}

int modify (int v, int l, int r, int s) {
	int node = ++tot;
	tree[node] = tree[v];
	tree[node].sum++;
	
	if (l == r) return node;
	
	int mid = l + ((r - l) >> 1);
	if (s <= mid) tree[node].ls = modify (tree[node].ls, l, mid, s);
	else tree[node].rs = modify (tree[node].rs, mid + 1, r, s);
	return node;
}

int query (int l, int r, int s, int t, int k) {
	if (l == r) return l;
	
	int mid = l + ((r - l) >> 1), x = tree[tree[t].ls].sum - tree[tree[s].ls].sum;
	if (k <= x) return query (l, mid, tree[s].ls, tree[t].ls, k);
	else return query (mid + 1, r, tree[s].rs, tree[t].rs, k - x);
}

int main () {
	
	ios::sync_with_stdio (false);
	cin.tie (0); cout.tie (0);
	
	cin >> n >> m;
	for (int i = 1; i <= n; ++i) {
		cin >> a[i];
		b[i] = a[i];
	}
	
	sort (b + 1, b + n + 1);
	len = unique (b + 1, b + n + 1) - b - 1;
	for (int i = 1; i <= n; ++i) a[i] = lower_bound (b + 1, b + len + 1, a[i]) - b;
	
	
	rt[0] = build (1, len);
	for (int i = 1; i <= n; ++i) rt[i] = modify (rt[i - 1], 1, len, a[i]);
	
	while (m--) {
		int l, r, k;
		cin >> l >> r >> k;
		cout << b[query (1, len, rt[l - 1], rt[r], k)] << endl;
	}
	
	return 0;
}

标签:node,int,线段,tree,区间,节点
From: https://www.cnblogs.com/Xeanin/p/18371025

相关文章

  • 权值线段树与动态开点线段树
    权值线段树(维护一段值域)用线段树维护桶实质上是维护一段值域中数字出现次数例:\(1,5,4,6,7,3,8,4,5,6\);根:\(1-8\);左儿子:\(1-4\);右儿子:\(5-8\);询问目前出现第\(k\)小数字从根节点出发,如果根节点权值\(>k\)则证明存在第\(k\)小;以此类推问:如果值域很大,线段树炸了怎......
  • 「1.1」线段
    问题背景「一本通1.1练习3」题目描述数轴上有n条线段,选取其中k条线段使得这k条线段两两没有重合部分,问k最大为多少。输入格式第一行为一个正整数 n;在接下来的 n 行中,每行有 2 个数 ai,bi,描述每条线段。输出格式输出一个整数,为 k 的最大值。样例输入1......
  • 线段树
    完成时间:\(2024.5.31\sim202?.?.?\)工程表\(日期\)\(完工\)\(2024.6.1\)\(2.3,3.1,3.2,3.3\)\(2024.6.5\)\(2.1,2.2\)\(2024.6.8\)\(3.4\)线段树入门P2068统计和P1531IHateIt打算写,鸽一鸽。线段树基础懒标记线段树P3372【模板】线段树1如果......
  • 洛谷 P3919 可持久化线段树 1 之主席树模板(初级)
    洛谷P3919题解传送锚点摸鱼环节【模板】可持久化线段树1(可持久化数组)题目背景UPDATE:最后一个点时间空间已经放大2021.9.18增添一组hack数据by@panyf标题即题意有了可持久化数组,便可以实现很多衍生的可持久化功能(例如:可持久化并查集)题目描述如题,你需要维护这......
  • 可持久化线段树(主席树)
    区间第k大/小问题洛谷P3834好吧,区间问题,考虑线段树でも,线段树能求解的问题须是大问题的解可以从小问题的解合并而来,显然,第k大/小问题不满足,不能直接用一颗树解决考虑用多颗树怎么想到的?我要是想到了我就成主席了首先,烧烤一下如何用线段树求1-i的第k小值烧烤ing……以......
  • 刍议线段树 3 (扫描线)
    扫描线扫描线是一种另外的思想,只是其中会运用到线段树以求优化。所以不要将扫描线简单的并为线段树的一个小拓展。例题:https://www.luogu.com.cn/problem/P5490大意:求\(n\)个四边平行于坐标轴的矩形的面积并。思路:纵向分割图形我们考虑把这些纵向矩形分割。那么,总面积就......
  • 线段树模板,洛谷原题P3373
    线段树区间乘、加,范围求和,QWQ原题#include<bits/stdc++.h>#definePIIpair<int,int>#defineintlonglong#defineDBdoublenamespaceFastIO{ inlineintread(intMOD,int&ret){ charch=getchar();intngtv=1; if(MOD==0){while(ch<&#......
  • D46 2-SAT+线段树优化+二分 [ARC069F] Flags
    视频链接: [ARC069F]Flags-洛谷|计算机科学教育新生态(luogu.com.cn)//D462-SAT+线段树优化+二分O(nlognlogv)#include<iostream>#include<cstring>#include<algorithm>usingnamespacestd;#definemid((l+r)>>1)#definels(u<<1)#definer......
  • leetcode线段树(2940. 找到 Alice 和 Bob 可以相遇的建筑)
    前言经过前期的基础训练以及部分实战练习,粗略掌握了各种题型的解题思路。现阶段开始专项练习。描述给你一个下标从 0 开始的正整数数组 heights ,其中 heights[i] 表示第 i 栋建筑的高度。如果一个人在建筑 i ,且存在 i<j 的建筑 j 满足 heights[i]<heig......
  • 算法笔记——可持久化线段树
    维护历史值当要修改一个节点时,把跟他有关的线段树中所有节点舍弃,并建立新节点连接.代码如下:#include<bits/stdc++.h>usingnamespacestd;constintN=1e6+5;intn,m,a[N],root[N],top;structnode{ intl,r,val;}t[N*40];intclone(intx)//新建节点{ top++; t......