线段树
线段树简介 线段树是一种能把一些对于区间(或者线段)的修改、维护,从$O(n)$的时间复杂度优化为$O(logn)$的一种高级数据结构,线段树除了最后一层以外是满二叉树,可以使用堆来存,使用一个一维数组进行存储,一个简单存储[1,10]的一个线段树如下图所示:
线段树常见问题 线段树为什么开4倍区间大小的空间 将树看成是一个满二叉树来分析,假设有n个叶子节点,树的深度是k,那么$2^{k-1} \ge n$,则$k \ge 2^{log _2 n+1}$,最后一层节点数是$2^{\left \lfloor log _2 n \right \rfloor+1}$,根据满二叉树的性质,总节点数是$2n-1$,即$2^{\left \lfloor log _2 n \right \rfloor} \times 2 - 1$,即$4n$
线段树的时间复杂度为什么是$O(logn)$ 假设待查找区间是$[L,R]$,当前节点的区间范围是$[T_L,T_R]$,需要分情况讨论:
$T_L \le L \le T_R \le R$,这种情况下有分为两种,$mid = (T_L + T_R)/2$,一种$L < mid$只需要递归右边,另一种情况需要递归左右,但是如果这样的话不就不是$O(logn)$了吗?对这种情况进一步分析后,发现递归左边的区间是一定被查询区间所包含的,这样的话相当于多了长度是0的一条链
$L \le T_L \le R \le T_R$,这种情况和1是对称的,不做讨论了
$T_L \le L \le R \le T_R$,这种情况下分三种情况,$R \le mid$,只递归左边,$L > mid$,只递归右边,其他情况下,左右都要递归,但是这种情况下只会发生在根节点,也就是所最多发生一次
从上面的情况分析总的搜索次数是$4logn$,总时间复杂度是$O(logn)$
线段树常用操作 线段树的存储可以使用堆的存储方式,即使用一个一维数组从下标1开始存储,假设某个节点是$x$,$2x$表示该节点的左儿子节点,$2x+1$表示该节点的右儿子节点,$\left \lfloor x/2 \right \rfloor$ 表示该节点的父亲节点
pushup操作 pushup操作在根据题目中数据初始化线段树、修改操作的回溯修改父亲节点中使用,pushup只适用于对于线段树数据的单点修改操作,此处以求区间和为例,下面的模板代码不加说明都是求区间和的操作,下面是模板代码:
1 2 3 void pushup (int u) { tr[u].sum = tr[u << 1 ].sum + tr[u << 1 | 1 ].sum; }
build操作 build操作是建立线段树的操作,一般是依赖题目中给定的数据对线段树进行的初始化操作,build操作是一个递归建树操作,下面是具体模板代码:
1 2 3 4 5 6 7 8 9 void build (int u, int l, int r) { tr[u] = {l, r}; if (l == r) { } int mid = l + r >> 1 ; build (u << 1 , l, mid), build (u << 1 | 1 , mid + 1 , r); pushup (u); }
query操作 query操作是查询线段树某个区间的属性的操作,一般是题目中要求的线段树所维护的属性,具体的代码因题目而定,下面是求区间和模板代码:
1 2 3 4 5 6 7 8 9 int query (int u, int l, int r) { if (tr[u].l >= l && tr[u].r <= r) return tr[u].sum; int mid = tr[u].l + tr[u].r >> 1 ; int v = 0 ; if (l <= mid) v = query (u << 1 , l, r); if (r > mid) v = v + query (u << 1 | 1 , l, r); return v; }
modify操作 modify操作是对线段树数据进行动态修改的操作,此操作比较简单不在赘述,以下是模板代码:
1 2 3 4 5 6 7 8 9 10 void modify (int u, int x, int v) { if (tr[u].l == x && tr[u].r == x) tr[u].sum = v; else { int mid = tr[u].l + tr[u].r >> 1 ; if (x <= mid) modify (u << 1 , x, v); else modify (u << 1 | 1 , x, v); pushup (u); } }
pushdown操作 pushdown操作是一个线段树的懒标记操作,使得线段树可以进行区间操作和区间查询,一般是在需要进行区间分裂的时候使用pushdown操作,在进行区间修改的最后使用pushup操作。
题目引入 吃个桃桃 Description
小陶玩了《黑神话 悟空》, 晚上做梦梦见自己在游戏内的世界遇到了一片神秘的森林, 森林里有N棵树, 每棵树上都有一些桃子, 并且还在不断的长出新的桃子.
天命人这时突然出现, 问小陶: “我要是随意划一片范围,你可能告诉我这一片当前总共有多少个桃子?”, 说罢随手摘了几个桃.
小陶即刻回道: “这有何难!”
Input
第一行输入为一个 int64 N, 表示接下来有 N 棵树, 之后 N 行每行一个 int64 整数 M, 第 i 行整数表示第 i 棵树当前的桃子数量. 第 N+2 行输入为一个 int64 J, 表示接下来有 J 个操作, 接下来 J 行每行三个 int64 整数 A B C, A 表示操作类型, B C 为对应操作的参数, A 共有三种可能输入:
0: 询问当前 [B, C] 区间上的桃子总数.
1: 第 B 棵树上长出了 C 个桃子.
2: 天命人吃了 B 棵树上 C 个桃子.
Output
输出为每行一个 int64 数字, 输出询问总数的结果.
数据规模:1 <= N, J <= 10^5
Sample Input 1
1 2 3 4 5 6 7 8 9 10 11 12 5 1 2 3 4 5 5 0 0 4 1 0 1 0 0 1 2 0 1 0 0 4
Simple Output 1
AC代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 #include <iostream> #include <algorithm> #include <cstring> typedef unsigned long long ull;using namespace std;const int N = 1e5 + 10 ;int n, m, a, b, c, nums[N];struct Node { int l, r; long s; } tr[N * 4 ]; void pushup (int u) { tr[u].s = tr[u << 1 ].s + tr[u << 1 | 1 ].s; } void build (int u, int l, int r) { tr[u] = {l, r}; if (l == r) { tr[u].s = nums[l]; return ; } int mid = (l + r) >> 1 ; build (u << 1 , l, mid); build (u << 1 | 1 , mid + 1 , r); pushup (u); } long query (int u, int l, int r) { if (tr[u].l >= l && tr[u].r <= r) return tr[u].s; int mid = (tr[u].l + tr[u].r) >> 1 ; long sum = 0 ; if (l <= mid) sum += query (u << 1 , l, r); if (r > mid) sum += query (u << 1 | 1 , l, r); return sum; } void modify (int u, int x, int v) { if (tr[u].l == x && tr[u].r == x) { tr[u].s += v; } else { int mid = (tr[u].l + tr[u].r) >> 1 ; if (x <= mid) modify1 (u << 1 , x, v); else modify1 (u << 1 | 1 , x, v); pushup (u); } } int main () { cin >> n; for (int i = 1 ; i <= n; i++) { cin >> nums[i]; } build (1 , 1 , n); cin >> m; while (m--) { cin >> a >> b >> c; if (a == 0 ) { cout << query (1 , b + 1 , c + 1 ) << endl; } else if (a == 1 ) { modify1 (1 , b + 1 , c); } else if (a == 2 ) { modify2 (1 , b + 1 , -c); } } return 0 ; }
一个简单的整数问题 给定一个长度为 N 的数列 A,以及 M 条指令,每条指令可能是以下两种之一:
C l r d,表示把 A[l],A[l+1],…,A[r]𝐴[𝑙],𝐴[𝑙+1],…,𝐴[𝑟] 都加上 d。
Q l r,表示询问数列中第 l∼r 个数的和。
对于每个询问,输出一个整数表示答案。
输入格式
第一行两个整数 N,M。
第二行 𝑁 个整数 𝐴[𝑖]。
接下来 𝑀 行表示 𝑀 条指令,每条指令的格式如题目描述所示。
输出格式
对于每个询问,输出一个整数表示答案。
每个答案占一行。
数据范围
给定一个长度为 N𝑁 的数列 𝐴,以及 𝑀 条指令,每条指令可能是以下两种之一:
C l r d,表示把 A[l],A[l+1],…,A[r]𝐴[𝑙],𝐴[𝑙+1],…,𝐴[𝑟] 都加上 𝑑。
Q l r,表示询问数列中第 𝑙∼𝑟 个数的和。
对于每个询问,输出一个整数表示答案。
输入格式
第一行两个整数 𝑁,𝑀。
第二行 𝑁 个整数 𝐴[𝑖]。
接下来 𝑀 行表示 𝑀 条指令,每条指令的格式如题目描述所示。
输出格式
对于每个询问,输出一个整数表示答案。
每个答案占一行。
数据范围
$1\le N,M \le10^5$ $|d|\le10000$ $|A[i]|\le10^9$
输入样例:
1 2 3 4 5 6 7 10 5 1 2 3 4 5 6 7 8 9 10 Q 4 4 Q 1 10 Q 2 4 C 3 6 3 Q 2 4
输出样例:
1≤N,M≤1051≤𝑁,𝑀≤105, |d|≤10000|𝑑|≤10000, |A[i]|≤109|𝐴[𝑖]|≤109
输入样例:
1 2 3 4 5 6 7 10 5 1 2 3 4 5 6 7 8 9 10 Q 4 4 Q 1 10 Q 2 4 C 3 6 3 Q 2 4
输出样例:
AC代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 #include <iostream> #include <cstring> #include <algorithm> using namespace std;const int N = 100010 ;typedef long long LL;struct Node { int l, r; LL sum; int add; }tr[N * 4 ]; int n, m, l, r, d;int w[N];void pushup (int u) { tr[u].sum = tr[u << 1 ].sum + tr[u << 1 | 1 ].sum; } void pushdown (int u) { auto &root = tr[u], &left = tr[u << 1 ], &right = tr[u << 1 | 1 ]; if (root.add) { left.sum += (LL)(left.r - left.l + 1 ) * root.add, left.add += root.add; right.sum += (LL)(right.r - right.l + 1 ) * root.add, right.add += root.add; root.add = 0 ; } } void build (int u, int l, int r) { if (l == r) { tr[u] = {l, r}; tr[u].sum = w[r]; tr[u].add = 0 ; } else { tr[u] = {l, r}; int mid = l + r >> 1 ; build (u << 1 , l, mid), build (u << 1 | 1 , mid + 1 , r); pushup (u); } } void modify (int u, int l, int r, int d) { if (tr[u].l >= l && tr[u].r <= r) { tr[u].sum += (LL)(tr[u].r - tr[u].l + 1 ) * d; tr[u].add += d; } else { pushdown (u); int mid = tr[u].l + tr[u].r >> 1 ; if (r > mid) modify (u << 1 | 1 , l, r, d); if (l <= mid) modify (u << 1 , l, r, d); pushup (u); } } LL query (int u, int l, int r) { if (tr[u].l >= l && tr[u].r <= r) return tr[u].sum; else { pushdown (u); int mid = tr[u].l + tr[u].r >> 1 ; LL v = 0 ; if (l <= mid) v = query (u << 1 , l, r); if (r > mid) v += query (u << 1 | 1 , l, r); pushup (u); return v; } } int main () { scanf ("%d%d" , &n, &m); for (int i = 1 ; i <= n; i ++) scanf ("%d" , &w[i]); build (1 , 1 , n); char op[2 ]; while (m --) { scanf ("%s%d%d" , op, &l, &r); if (*op == 'C' ) { scanf ("%d" , &d); modify (1 , l, r, d); } else printf ("%lld\n" , query (1 , l, r)); } return 0 ; }