线段树
Posted xuan01
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了线段树相关的知识,希望对你有一定的参考价值。
线段树--解决区间问题的数据结构,相比于树状数组,更具有普适性;
完全二叉树的性质:根节下标为1,,节点为 i 的节点,左子节点为2*i,右子节点为2*i+1;
代表nums中单个元素的节点tree[x]应当在树的最底层,即叶子节点;更大的区间从叶子节点开始向上构成;
代表区间【L,R】的节点 tree【i】,左子节点tree【2*i】表示区间【L,(L+R)/2】的区间和;右子节点tree【2*i+1】表示区间 【(L+R)/2 +1, R】的区间和;
初始化tree数组的大小 总是 令 其 为 4*n;类似于归并排序、快排的分治算法,将原问题不断的划分为左右子问题;
代码摘自:线段树从入门到急停 - 力扣(LeetCode)非常详细;
核心:一个就是注意单点修改时,递归结束条件的判断,是查询单点的话,就是 左右边界相等终止;查询区间和的话, 判断当前区间是否落在 所求范围内,是的话就加上这个区间;
另一个就是拓展:区间和数组可以替换为区间最值问题;求区间最大值、区间最小值等;
再一个就是区间修改问题,有两种,一种是增量式修改,因为注意到我们的区间和,若不关注每个具体的值,只是区间和的大小,我们无需每次都向下更新到叶子节点,否则会达到O(N)时间复杂度;因此 需要增加一个 数组,判断当前增量是否已经向下更新,我们称之为懒惰标记;; 第二种就是 覆盖式修改:将区间的值都修改为同一值,此时,我们不能依据增量式修改的方法,因为可能修改为0,而向下更新的标记 也是检查是否为0,从而 会产生冲突,所以另起一个数组,判断是否已经向下覆盖;;
代码实现:(此处将 两种修改方法写在一起了,应用时分开两个类 写);
#include <bits/stdc++.h> using namespace std; class Segement_tree private: vector<int>nums; vector<int>tree; vector<int>lazy; vector<int>is_updated; int n; void pushUp(int i) tree[i] = tree[2*i] + tree[2*i + 1]; void build(int left, int right, int i) if(left == right) tree[i] = nums[left]; int mid = (left + right) / 2; build(left, mid, 2*i); build(mid + 1, right, 2*i + 1); pushUp(i); /*单点修改*/ void add(int index, int x, int left, int right, int i) if(left == right) tree[i] += x; return; int mid = (left + right)/2; if(index <= mid) add(index,x,left,mid,2*i); else add(index,x,mid+1,right,2*i+1); pushUp(i); void update(int index,int x,int left, int right,int i) if(left == right) tree[i] = x; return; int mid = (left + right) /2; if(index <= mid) update(index,x,left,mid,2*i); else update(index,x,mid+1,right,2*i+1); pushUp(i); int query(int index,int left,int right, int i) if(left == right) return tree[i]; int mid = (left + right) /2; if(index <= mid) return query(index,left,mid,2*i); else return query(index,mid+1,right,2*i+1); /*区间求和*/ int sum(int left, int right, int s, int t, int i) if(left <= s && t <= right) return tree[i]; int mid = (s + t) /2; int res = 0; if(left <= mid) res += sum(left,right, s, mid, 2*i); if(right > mid) res += sum(left,right, mid+1, t, 2*i+1); return res; /*区间修改: 增量式 */ void add(int left, int right, int x,int s, int t, int i) if(left <= s && t <= right) tree[i] += (t-s+1)*x; if(s != t) lazy[i] += x; return; int mid = (s + t)/2; if(lazy[i] != 0) pushDown(s,mid,t,i); if(left <= mid) add(left,right,x,s,mid,2*i); if(right > mid) add(left,right,x,mid+1,t,2*i+1); pushUp(i); void pushDown(int s,int mid, int t,int i) tree[2*i] += (mid-s+1)*lazy[i]; lazy[2*i] += lazy[i]; tree[2*i+1] += (t-mid)*lazy[i]; lazy[2*i+1] += lazy[i]; lazy[i] = 0; /*区间修改: 覆盖式 */ void update(int left, int right,int x,int s,int t,int i) if(left <= s && t <= right) tree[i] = (t-s+1)*x; if(s != t) lazy[i] = x; is_updated[i] = true;//未推送 return; int mid = (s+t)/2; if(is_updated[i]) pushDown1(s,mid,t,i); if(left <= mid) update(left,right,x,s,mid,2*i); if(right > mid) update(left,right,x,mid+1,t,2*i+1); pushUp(i); void pushDown1(int s,int mid,int t,int i) tree[2*i] = (mid - s + 1)* lazy[i]; lazy[2*i] = lazy[i]; is_updated[2*i] = true; tree[2*i+1] = (t - mid) * lazy[i]; lazy[2*i+1] = lazy[i]; is_updated[2*i+1] = true; is_updated[i] = false; lazy[i] = 0; public: Segement_tree(vector<int>& nums) this->n = nums.size(); this->nums = nums; this->tree.resize(4*n, 0); this->lazy.resize(4*n, 0); this->is_updated.resize(4*n, 0); build(0, n-1, 1); /*单点修改*/ void add(int index, int x) add(index,x,0,n-1,1); void update(int index,int x) update(index,x,0,n-1,1); int query(int index) return query(index, 0, n-1,1); /*区间求和*/ int sum(int left,int right) return sum(left,right,0, n-1,1); /*区间修改 : 增量式*/ void add(int left,int right,int x) add(left,right,x,0,n-1,1); /*区间修改: 覆盖式 */ void update(int left,int right, int x) update(left,right,x,0,n-1,1); ; int main() system("pause"); return 0;
动态开点:需要增加一个addnode方法;
后续更新;
一般线段树与权值线段树
一般线段树与权值线段树
1.算法分析
- 一般还要开4N的数组
- 一般做单点修改、区间查询,加上懒标记后,可以做区间修改、区间查询
1.1 一般线段树
可以处理:区间加、区间乘、区间max/min、区间覆盖等问题
1.2 权值线段树
- 维护全局的值域信息,每个节点记录的是该值域的值出现的总次数。
- 使用二分的思想(离散化的时候,需要用到)
- 支持查询全局K小值,全局rank,前驱,后继等。
- 单词操作时间复杂度为O(logn)
- 空间复杂度为O(n)
- 相对于平衡树的优势:代码简单,速度快
- 劣势:值域较大时,我们需要离散化,变成离线数据结构
2.板子
2.1 线段树入门
2.1.1 单点修改+区间查询
// 该板子是求区间和
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
int const N = 5e5 + 10;
LL dat[N << 2]; // 4倍空间
int n, m, a[N];
// 上传操作
void pushup(int rt) {
dat[rt] = dat[rt << 1] + dat[rt << 1 | 1];
}
// 建树
void build (int rt, int l, int r) {
if (l == r) { // 如果当前到达叶节点,那么赋值
dat[rt] = a[l]; // 赋值是a[l],表示那个叶节点
return ;
}
int mid = (l + r) >> 1;
// 递归建立左右子树
build(rt << 1, l, mid);
build(rt << 1 | 1, mid + 1, r);
// 上传
pushup(rt);
}
// 单点修改
void modify (int rt, int l, int r, int x, int y) {
if (l == x && r == x) { // 递归到叶节点且叶节点刚好为x节点
dat[rt] += y; // 修改
return;
}
int mid = (l + r) >> 1;
if (x <= mid) modify(rt << 1, l, mid, x, y); // 如果在左子树
else modify(rt << 1 | 1, mid + 1, r, x, y); // 不在左子树,比在右子树
pushup(rt); // 上传
}
// 区间查询
LL query(int rt, int l, int r, int L, int R) {
if (L <= l && r <= R) return dat[rt]; // 如果当前rt管辖的点能够被[L, R]完全包含,返回
int mid = (l + r) >> 1;
LL res = 0;
if (L <= mid) res += query(rt << 1, l, mid, L, R); // 如果和左子树有关
if (mid < R) res += query(rt << 1 | 1, mid + 1, r, L, R); // 如果可能和右子树有关
return res;
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
build(1, 1, n); // 建树
for (int i = 1, op, x, y; i <= m; ++i) {
scanf("%d%d%d", &op, &x, &y);
if (op == 1) modify(1, 1, n, x, y); // 单点修改a[x]+=y
else printf("%lld
", query(1, 1, n, x, y)); // 区间查询,求[x, y]的区间和
}
return 0;
}
2.1.2 区间修改+区间查询
// 该板子是求区间和
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
int const N = 5e5 + 10;
LL dat[N << 2], lazy[N << 2];
int a[N], n, m;
// 上传标记,每次左右子树建树/区间修改完都需要上传
void pushup(int rt) {
dat[rt] = dat[rt << 1] + dat[rt << 1 | 1];
}
// 建树
void build(int rt, int l, int r) {
if (l == r) { // 递归到叶节点
dat[rt] = a[l];
lazy[rt] = 0;
return;
}
// 递归建立左右子树
int mid = (l + r) >> 1;
build(rt << 1, l, mid);
build(rt << 1 | 1, mid + 1, r);
pushup(rt); // 上传
}
// 下传,下传标记,同时改变dat数组
void pushdown(int rt, int l, int r) {
if (lazy[rt]) { // 如果有标记
int mid = (l + r) >> 1;
// 把标记给左右子树
lazy[rt << 1] += lazy[rt];
lazy[rt << 1 | 1] += lazy[rt];
// 改变dat
dat[rt << 1] += (mid - l + 1) * lazy[rt];
dat[rt << 1 | 1] += (r - mid) * lazy[rt];
// rt标记清空
lazy[rt] = 0;
}
return;
}
// 区间修改: [L, R] += x
void modify(int rt, int l, int r, int L, int R, int x) {
if (L <= l && r <= R) { // 如果当前区间被完全包含
dat[rt] += (r - l + 1) * x; // 修改当前区间的dat值
lazy[rt] += x; // 改变懒标记
return ;
}
pushdown(rt, l, r); // 下传
// 递归左右子树修改区间
int mid = (l + r) >> 1;
if (L <= mid) modify(rt << 1, l, mid, L, R, x);
if (mid < R) modify(rt << 1 | 1, mid + 1, r, L, R, x);
pushup(rt); // 上传
return;
}
// 区间查询:获得[L, R]的区间和
LL query(int rt, int l, int r, int L, int R) {
if (L <= l && r <= R) return dat[rt]; // 如果[l, r]被完全包含于[L, R]
pushdown(rt, l, r); // 标记下传
// 递归加上左右子树
int mid = (l + r) >> 1;
LL res = 0;
if (L <= mid) res += query(rt << 1, l, mid, L, R);
if (mid < R) res += query(rt << 1 | 1, mid + 1, r, L, R);
return res;
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; ++i) scanf("%d", &a[i]); // 读入数组
build(1, 1, n); // 建树
for (int i = 1, a, b, x, op; i <= m; ++i) {
scanf("%d", &op);
if (op == 1) {
scanf("%d%d%d", &a, &b, &x);
modify(1, 1, n, a, b, x); // 区间修改, [a, b] += x
}
else {
scanf("%d%d", &a, &b);
printf("%lld
", query(1, 1, n, a, b)); // 区间查询,查询[a, b]的区间和
}
}
return 0;
}
2.1.3 区间加乘操作
// 加乘模板
// x点原来的乘、加法标记为:mul1、add1,后来要加上的乘、加法标记为:mul2、add2
// 可以证明先乘后加最优方法
// x的值变为: x.dat => (x.dat * mul2) + (x.r - x.l + 1) * add2;
// x的乘法标记变为: x.mul1 => x.mul1 * mul2
// x的加法标记变为: x.add1 => x.add1 * mul2 + add2
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
int const N = 1e5 + 10;
LL dat[N << 2], mul[N << 2], add[N << 2];
int n, p, a[N], m;
// 上传,根的值为左子树的值和右子树的值之和
void pushup(int rt) {
dat[rt] = (dat[rt << 1] + dat[rt << 1 | 1]) % p;
}
// 建树
void build(int rt, int l, int r) {
if (l == r) { // 如果是叶子
dat[rt] = a[l] % p;
add[rt] = 0;
mul[rt] = 1;
return;
}
// 如果不是叶子,那么乘法标记必须为1,加法标记为0
mul[rt] = 1;
add[rt] = 0;
int mid = (l + r) >> 1;
build(rt << 1, l, mid), build(rt << 1 | 1, mid + 1, r); // 建立左子树和右子树
pushup(rt);
}
// 加成的结果
void eval(int rt, int l, int r, LL add2, LL mul2) {
dat[rt] = ((dat[rt] * mul2 % p) + ((r - l + 1) % p) * add2 % p) % p;
mul[rt] = mul[rt] * mul2 % p;
add[rt] = (add[rt] * mul2 % p + add2) % p;
}
// 标记下移
void pushdown(int rt, int l, int r) {
int mid = (l + r) >> 1;
eval(rt << 1, l, mid, add[rt], mul[rt]), eval(rt << 1 | 1, mid + 1, r, add[rt], mul[rt]); // 左右子树分别得到根的标记
add[rt] = 0, mul[rt] = 1; // 清空根的标记
return;
}
// 区间修改
void modify(int rt, int l, int r, int L, int R, LL add2, LL mul2) {
if (L <= l && r <= R) { // 如果[L, R]在[l, r]内,直接修改
eval(rt, l, r, add2, mul2);
return;
}
pushdown(rt, l, r); // 如果不在[l, r]内,那么分裂,首先要把标记下移
int mid = (l + r) >> 1;
if (L <= mid) modify(rt << 1, l, mid, L, R, add2, mul2); // 修改左子树
if (mid < R) modify(rt << 1 | 1, mid + 1, r, L, R, add2, mul2); // 修改右子树
pushup(rt); // 修改完子树需要把标记上移
return;
}
// 询问区间和[L, R]
LL query(int rt, int l, int r, int L, int R) {
if (L <= l && r <= R) return dat[rt] % p;
pushdown(rt, l, r); // 如果[L, R]不在[l, r]内,那么需要分裂,首先要把标记下移
LL res = 0;
int mid = (l + r) >> 1;
if (L <= mid) res = query(rt << 1, l, mid, L, R) % p; // 左子树
if (mid < R) res = (res + query(rt << 1 | 1, mid + 1, r, L, R) % p) % p; // 右子树
return res;
}
int main() {
cin >> n >> p; // 输入数字的个数和模数
for (int i = 1; i <= n; ++i) scanf("%d", &a[i]); // 输入数字
build(1, 1, n); // 建树
cin >> m; // 输入操作数
for (int i = 1, op, t, g, c; i <= m; ++i) { // 输入每次的具体操作
scanf("%d", &op);
if (op == 1) { // 区间乘
scanf("%d%d%d", &t, &g, &c);
modify(1, 1, n, t, g, 0, c);
}
else if (op == 2) { // 区间加
scanf("%d%d%d", &t, &g, &c);
modify(1, 1, n, t, g, c, 1);
}
else { // 询问区间和
scanf("%d%d", &t, &g);
cout << query(1, 1, n, t, g) << endl;
}
}
return 0;
}
2.1.4 区间染色
/*本题是区间覆盖问题,求指定区间内有多少的颜色数目,因为颜色的数目比较少,因此
可以使用一个int整数来表示所有的颜色数目,而后就是线段树的常规操作*/
#include <bits/stdc++.h>
using namespace std;
int const N = 1e5 + 10;
typedef long long LL;
LL add[N << 2], sum[N << 2]; // add为记录颜色的懒标记,sum为当前区间的颜色
// 向下传递操作
void pushup(int u) {
sum[u] = sum[u << 1] | sum[u << 1 | 1]; // 当前颜色由子区间颜色得到
}
// 向上传递操作
void pushdown(int u) {
if (add[u]) { // 如果当前u节点有颜色的话
// 给左右子节点标记都赋值
add[u << 1] = add[u];
add[u << 1 | 1] = add[u];
// 给左右节点的sum赋值,记录颜色
sum[u << 1] = add[u];
sum[u << 1 | 1] = add[u];
// 去掉懒标记
add[u] = 0;
}
}
// 建树
void build(int u, int l, int r) {
add[u] = 0; // 初始每个节点都没有懒标记
if (l == r) { // 如果递归到叶节点
sum[u] = 1; // 叶节点的颜色赋值
return;
}
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 c, int L, int R) {
if (L <= l && r <= R) { // 如果[l, r]完全被包含在要赋值的区间[L, R]的话,那么直接修改
add[u] = 1 << (c - 1);
sum[u] = 1 << (c - 1);
return;
}
pushdown(u); // 下传标记,因为因为标记要分裂
int mid = l + r >> 1;
if (L <= mid) modify(u << 1, l, mid, c, L, R); // 递归修改左右子树
if (mid < R) modify(u << 1 | 1, mid + 1, r, c, L, R);
pushup(u); // 上传操作
}
// 区间查询多少个颜色
LL query(int u, int l, int r, int L, int R) {
if (L <= l && r <= R) return sum[u]; // 如果[l, r]完全被包含在要赋值的区间[L, R]的话,那么返回
pushdown(u); // 标记下移
int mid = l + r >> 1;
LL res = 0;
// 递归查询左右子树
if (L <= mid) res |= query(u << 1, l, mid, L, R);
if (mid < R) res |= query(u << 1 | 1, mid + 1, r, L, R);
return res;
}
int main() {
int L, T, O, a, b, c;
cin >> L >> T >> O; // 读入节点数、颜色总数、操作数
build(1, 1, L);
while (O--) {
char op[2];
scanf("%s", op); // 读入操作类型
if (op[0] == ‘P‘) {
scanf("%d %d", &a, &b);
if (a > b) swap(a, b); // 保证a要比b小
LL ans = query(1, 1, L, a, b); // 查询a到b的颜色总数,颜色总数用一个int型数表示
LL res = 0;
while (ans) { // 记录这个int型数有多少个1
if (ans & 1) res++;
ans >>= 1;
}
printf("%lld
", res);
}
else {
scanf("%d%d%d", &a, &b, &c); // 读入[a, b]和修改为的值
if (a > b) swap(a, b);
modify(1, 1, L, c, a, b); // 修改操作
}
}
return 0;
}
2.2 权值线段树
2.2.1 求第k大、前驱、后继等
/*
本题由于一开始dat维护的全为0,所以不需要建树的操作。dat维护每个数出现的次数
数据较大,需要先离散化,然后在每个离散化后的数字上建立线段树维护每个数出现的次数。
1. 插入数值x:x的次数加一
2. 删除数值x(若有多个相同的数,应只删除一个):x的次数减一
3. 查询数值x的排名(若有多个相同的数,应输出最小的排名):区间查询[l, x - 1]的次数,然后加一
4. 查询排名为x的数值:看x是否小于等于左子树的次数,如果小于在左子树;否则就算右子树的k-左子树次数
5. 求数值x的前驱(前驱定义为小于x的最大的数):求出x的排名t,然后查询排名为t-1的数
6. 求数值x的后继(后继定义为大于x的最小的数):求出x的排名t,然后查询排名为t+1的数
*/
#include <bits/stdc++.h>
using namespace std;
const int N=100005;
int num[N];
struct A{
int opt, x;
}q[N];
int dat[N << 2];
void pushup(int rt){
dat[rt] = dat[rt << 1] + dat[rt << 1 | 1];
}
// 单点修改
void modify(int rt, int l, int r, int p, int c){
if(l == r){
dat[rt] += c;
return;
}
int mid = (l + r) >> 1;
if(p <= mid) modify(rt << 1, l, mid, p, c);
else modify(rt << 1 | 1, mid + 1, r, p, c);
pushup(rt);
}
// 区间查询
int query1(int rt, int l, int r, int L, int R){//区间求和
if (L <= l && r <= R) return dat[rt];
int mid = (l + r) >> 1;
int res = 0;
if(L <= mid) res += query1(rt << 1, l, mid, L, R);
if (mid < R) res += query1(rt << 1 | 1, mid + 1, r, L, R);
return res;
}
// 查询排名为k的数
int query2(int rt, int l, int r, int k)
{
if(l == r) return l;
int mid = (l + r) >> 1;
if(k <= dat[rt << 1]) return query2(rt << 1, l, mid, k);
else return query2(rt << 1 | 1, mid + 1, r, k-dat[rt<<1]);
}
int main(){
int m, k=0;
scanf("%d", &m);
for(int i = 0; i < m; i++){
scanf("%d%d", &q[i].opt, &q[i].x);
if(q[i].opt != 4) num[k++] = q[i].x;
}
sort(num, num+k);
int n = unique(num, num+k) - num;
for(int i = 0; i < m; i++){
int x = lower_bound(num, num+n, q[i].x) - num + 1;
if(q[i].opt == 1){//插入
modify(1, 1, n, x, 1);
}
if(q[i].opt == 2){//删除
modify(1, 1, n, x, -1);
}
if(q[i].opt == 3){//查询x的排名
if(x - 1 == 0) printf("1
");
else printf("%d
", query1(1, 1, n, 1, x - 1) + 1);
}
if(q[i].opt == 4){//查询排名为x的数
printf("%d
", num[query2(1, 1, n, q[i].x) - 1]);
}
if(q[i].opt == 5){//求小于x的最大的数的值
int rk = query1(1, 1, n, 1, x - 1);
printf("%d
", num[query2(1, 1, n, rk) - 1]);
}
if(q[i].opt == 6){//求大于x的最小的数的值
int sum = query1(1, 1, n, 1, x);
printf("%d
", num[query2(1, 1, n, sum + 1) - 1]);
}
}
return 0;
}
3. 例题
3.1 线段树入门
luogu P1047 校门外的树
题意: 有一个数轴,长度为l+1,从0~l上每个点都种树。现在有m个操作,每个操作输入a和b,表示要把[a, b]上的树砍掉,问m次操作后,数轴上还剩下多少棵树?
题解: 只需要改区间修改+区间查询的板子即可,当砍掉[a,b]上的树时,就算把[a, b]赋值为0,最后统计还剩多少棵树,就算计算[1,n]的区间求和。
代码:
// 该板子是求区间和
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
int const N = 5e5 + 10;
LL dat[N << 2], lazy[N << 2];
int a[N], n, m;
// 上传标记,每次左右子树建树/区间修改完都需要上传
void pushup(int rt) {
dat[rt] = dat[rt << 1] + dat[rt << 1 | 1];
}
// 建树
void build(int rt, int l, int r) {
if (l == r) { // 递归到叶节点
dat[rt] = a[l];
lazy[rt] = 1;
return;
}
lazy[rt] = 1;
// 递归建立左右子树
int mid = (l + r) >> 1;
build(rt << 1, l, mid);
build(rt << 1 | 1, mid + 1, r);
pushup(rt); // 上传
}
// 下传,下传标记,同时改变dat数组
void pushdown(int rt, int l, int r) {
if (lazy[rt] == 0) { // 如果有标记
// 把标记给左右子树
lazy[rt << 1] = 0;
lazy[rt << 1 | 1] = 0;
// 改变dat
dat[rt << 1] = 0 ;
dat[rt << 1 | 1] = 0;
// rt标记清空
lazy[rt] = 1;
}
return;
}
// 区间修改: [L, R] += x
void modify(int rt, int l, int r, int L, int R) {
if (L <= l && r <= R) { // 如果当前区间被完全包含
dat[rt] = 0; // 修改当前区间的dat值
lazy[rt] = 0; // 改变懒标记
return ;
}
if (lazy[rt] == 0) pushdown(rt, l, r); // 下传
// 递归左右子树修改区间
int mid = (l + r) >> 1;
if (L <= mid) modify(rt << 1, l, mid, L, R);
if (mid < R) modify(rt << 1 | 1, mid + 1, r, L, R);
pushup(rt); // 上传
return;
}
// 区间查询:获得[L, R]的区间和
LL query(int rt, int l, int r, int L, int R) {
if (L <= l && r <= R) return dat[rt]; // 如果[l, r]被完全包含于[L, R]
if (lazy[rt] == 0) pushdown(rt, l, r); // 标记下传
// 递归加上左右子树
int mid = (l + r) >> 1;
LL res = 0;
if (L <= mid) res += query(rt << 1, l, mid, L, R);
if (mid < R) res += query(rt << 1 | 1, mid + 1, r, L, R);
return res;
}
int main() {
cin >> n >> m;
n ++;
for (int i = 1; i <= n; ++i) a[i] = 1;
build(1, 1, n); // 建树
// cout << query(1, 1, n, 1, n) << endl;
for (int i = 1, a, b; i <= m; ++i) {
scanf("%d%d", &a, &b);
a++, b++;
modify(1, 1, n, a, b);
}
cout << query(1, 1, n, 1, n) << endl;
return 0;
}
luogu P5057 [CQOI2006]简单题
题意: 有一个 n 个元素的数组,每个元素初始均为 0。有 m 条指令,要么让其中一段连续序列数字反转——0 变 1,1 变 0(操作 1),要么询问某个元素的值(操作 2)。 1 ≤ n ≤ 10^5^, 1 ≤ m ≤ 5 × 10^5^
题解: 线段树维护,每次给定反转区间[a, b],那么把[a, b]区间中每个数字加1,而后每次询问x的时候,只需要query(1,1,n,x,x),而后判断这个值是奇数还是偶数,奇数输出1,偶数输出0即可
代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
int const N = 1e5 + 10;
LL dat[N << 2], lazy[N << 2];
int n, m;
// 上传标记,每次左右子树建树/区间修改完都需要上传
void pushup(int rt) {
dat[rt] = dat[rt << 1] + dat[rt << 1 | 1];
}
// 建树
void build(int rt, int l, int r) {
if (l == r) { // 递归到叶节点
dat[rt] = 0;
lazy[rt] = 0;
return;
}
// 递归建立左右子树
int mid = (l + r) >> 1;
build(rt << 1, l, mid);
build(rt << 1 | 1, mid + 1, r);
pushup(rt); // 上传
}
// 下传,下传标记,同时改变dat数组
void pushdown(int rt, int l, int r) {
if (lazy[rt]) { // 如果有标记
int mid = (l + r) >> 1;
// 把标记给左右子树
lazy[rt << 1] += lazy[rt];
lazy[rt << 1 | 1] += lazy[rt];
// 改变dat
dat[rt << 1] += (mid - l + 1) * lazy[rt];
dat[rt << 1 | 1] += (r - mid) * lazy[rt];
// rt标记清空
lazy[rt] = 0;
}
return;
}
// 区间修改: [L, R] += x
void modify(int rt, int l, int r, int L, int R, int x) {
if (L <= l && r <= R) { // 如果当前区间被完全包含
dat[rt] += (r - l + 1) * x; // 修改当前区间的dat值
lazy[rt] += x; // 改变懒标记
return ;
}
pushdown(rt, l, r); // 下传
// 递归左右子树修改区间
int mid = (l + r) >> 1;
if (L <= mid) modify(rt << 1, l, mid, L, R, x);
if (mid < R) modify(rt << 1 | 1, mid + 1, r, L, R, x);
pushup(rt); // 上传
return;
}
// 区间查询:获得[L, R]的区间和
LL query(int rt, int l, int r, int L, int R) {
if (L <= l && r <= R) return dat[rt]; // 如果[l, r]被完全包含于[L, R]
pushdown(rt, l, r); // 标记下传
// 递归加上左右子树
int mid = (l + r) >> 1;
LL res = 0;
if (L <= mid) res += query(rt << 1, l, mid, L, R);
if (mid < R) res += query(rt << 1 | 1, mid + 1, r, L, R);
return res;
}
int main() {
cin >> n >> m;
build(1, 1, n); // 建树
for (int i = 1, a, b, x, op; i <= m; ++i) {
scanf("%d", &op);
if (op == 1) {
scanf("%d%d", &a, &b);
modify(1, 1, n, a, b, 1); // 区间修改, [a, b] += 1
}
else {
scanf("%d", &a);
printf("%lld
", (query(1, 1, n, a, a) & 1) == 1); // 区间查询,查询[a, b]的区间和
}
}
return 0;
}
luogu P4588 [TJOI2018]数学计算
题意: 小豆现在有一个数x,初始值为1.小豆有Q次操作,操作有两种类型:
1 m: x = x * m, 输出x%mod;
2 pos:x= x = x / 第pos次操作所乘的数(保证第pos次操作一定为类型1,对于每一个类型1的操作至多会被除一次)输出x % mod;Q <= 10^5^
题解: 使用线段树维护1~Q这Q个数字的区间乘,如果当前是1类型操作,那么进行单点修改modify(1, 1, n, i, x);如果是2类型操作,那么进行单点修改modify(1, 1, n, pos, 1); 每次输出都是所有的成绩, 即dat[1];
代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
int const N = 1e5 + 10;
LL dat[N << 2];
int n, p, t;
void pushup(int rt) {
dat[rt] = dat[rt << 1] * dat[rt << 1 | 1] % p;
}
void build(int rt, int l, int r) {
if (l == r) {
dat[rt] = 1;
return;
}
int mid = (l + r) >> 1;
build(rt << 1, l, mid), build(rt << 1 | 1, mid + 1, r);
pushup(rt);
}
void modify(int rt, int l, int r, int x, int y) {
if (l == r && l == x) {
dat[rt] = y;
return;
}
int mid = (l + r) >> 1;
if (x <= mid) modify(rt << 1, l, mid, x, y);
else modify(rt << 1 | 1, mid + 1, r, x, y);
pushup(rt);
}
int main() {
cin >> t;
while (t--) {
cin >> n >> p;
for (int i = 1; i <= n * 4; ++i) dat[i] = 0;
build(1, 1, n);
for (int i = 1, op, x; i <= n; ++i) {
scanf("%d%d", &op, &x);
if (op == 1) modify(1, 1, n, i, x % p);
else modify(1, 1, n, x, 1);
printf("%lld
", dat[1] % p);
}
}
return 0;
}
3.2 权值线段树
luogu P1908 逆序对
题意: 求出一个数列的逆序对.数列长度n ≤ 5×10^5^
题解: 权值线段树维护每个数字出现的次数,然后每个数字x出现的时候只需要区间查询[1, x - 1]的出现次数即可
代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
int const N = 500050;
int n;
LL a[N], b[N];
int dat[N << 2];
LL ans = 0;
void modify(int rt, int l, int r, int x)
{
if(l == r)
{
dat[rt]++;
return;
}
int mid = (l + r) >> 1;
if(x <= mid) modify(rt << 1, l, mid, x);
else modify(rt << 1 | 1, mid + 1, r, x);
dat[rt] = dat[rt << 1] + dat[rt << 1 | 1];
}
int query(int rt,int l,int r,int L,int R)
{
if(L <= l && r <= R) return dat[rt];
int mid = (l + r) >> 1;
int res = 0;
if(L <= mid) res += query(rt << 1, l, mid, L, R);
if(mid < R) res += query(rt << 1 | 1, mid + 1, r, L, R);
return res;
}
int main()
{
cin >> n;
for(int i = 1; i <= n; i++)
{
cin >> a[i];
b[i] = a[i];
}
sort(b + 1, b + 1 + n);
int len = unique(b + 1, b + n + 1) - b - 1;
for(int i = 1; i <= n; i++)
{
int pos = lower_bound(b + 1, b + n + 1, a[i]) - b;
a[i] = pos;
}
for(int i = 1; i <= n; i++)
{
int x = a[i];
ans += query(1, 1, n, x + 1, n);
modify(1, 1, n, x);
}
printf("%lld", ans);
return 0;
}
以上是关于线段树的主要内容,如果未能解决你的问题,请参考以下文章