数据结构 —— 线段树基本操作

可供练习 总题单 week 3 [kuangbin带你飞] 题单 最小生成树 + 线段树 Click here ~~
https://blog.csdn.net/m0_46272108/article/details/108980362

概念

线段树的概念:线段树的本质是一种二叉树。
在这里插入图片描述
将每个区间 [ L , R ] [L, R] [L,R]分解成 [ L , M i d ] [L, Mid] [L,Mid] [ M i d + 1 , R ] [ Mid + 1, R] [Mid+1,R] (其中Mid = [ L + R 2 ] [\frac{L+R}{2}] [2L+R])这里的 [ x ] [x] [x] 表示不大于X的整数,【即向下取整】一直到 左儿子 == 右儿子

假设线段树的开始区间为[1, n] 那么线段树最大高度为 : l o g 2 ( n − 1 ) + 2 log_2(n-1)+2 log2(n1)+2 即时间复杂度为: O ( l o g n ) O(logn) O(logn)

线段树的作用

对于区间(或者线段)的修改、维护,从 O ( n ) O(n) O(n) 的时间复杂度变成 O ( l o g n ) O(logn) O(logn)

线段树的储存

因为线段树是一棵二叉树,并且除了最后一层,是一颗满二叉树,且,堆也是除了最后一层是一棵满二叉树,
所以一般情况下:用堆(一维数组)来存满整棵树。
存树的时候:

在这里插入图片描述
用一维数组存线段树需要开4n的空间:

  • 如果开始的区间长度为n,那么最终叶节点一定为n个(倒数第二层最多有n个点)
  • 整棵树(除了最后一层)有2n-1个点
  • (倒数第二层最多有n个点时)最后一层是倒数第二层的两倍,则最多有2n个点
  • 最坏的情况下,整棵树有4n-1个结点,那么我们存线段树的时候,要开4倍空间,即如果区间开始长度为n,那么存树的这个数组就要开4n的空间。

解题步骤

线段树日常操作:

定义

const int N = 50010;
int num[N << 2];	//存储每一个工兵营的人数(根据题目所需,我以下面的模板题为例)

struct Node{
    
    
    int l, r;//左端点,右端点
    int val;//区间[l, r]的最大值
}tr[N << 2];

由子节点的信息来计算父节点的信息:pushup()

//由子节点的信息来计算父节点的信息
void pushup(int cur){
    
    
    //父节点是两个子节点的和,一直往上传值,就能使根节点是所有人的个数
    tr[cur].val = tr[cur << 1].val + tr[cur << 1 | 1].val;
}

由子节点的信息来计算父节点的信息:pushdown()

pushdown 待更新 ……

建树:build()

//cur代表当前节点,
void build(int cur, int l, int r){
    
    
    //建树的初始值
    tr[cur].l = l, tr[cur].r = r, tr[cur].val = 0;
    //如果已经是叶结点return
    if(l == r) {
    
    
        tr[cur].val = num[l];//如果到达叶节点就把人数赋给该节点
        return;
    }
    //否则求一下当前区间的中点
    int mid = l + r >> 1;
    //如果不是叶节点,就一直递归找左右孩子,然后知道找到叶节点进行传值
    //递归建立左边区间
    build(cur << 1, l, mid);
    //递归建立右边区间
    build(cur << 1 | 1, mid + 1, r);
    //将子节点的人数进行汇总,给父节点
    pushup(cur);
}

查询:query()

//[l, r]查询区间   cur代表当前线段树里面的端点。
int query(int cur, int ql, int qr)  {
    
    
    int l = tr[cur].l, r = tr[cur].r;
    //情况 ① [TL,TR] ? [L,R]
    //树中节点,已经被完全包含在[l, r]中了。
    if(ql <= l && qr >= r)  
        return tr[cur].val;
    int mid = l + r >> 1;
    int val = 0;
    //如果不能完全包含,就去找他的左右孩子
    //判断与左边有交集
    if (ql <= mid) {
    
    
        val += query(cur << 1, ql, qr);//求和
    }
    //这里为什么是 qr > mid,因为划分的区间是[l, mid][mid + 1, r],所以要用>而不能=
    //判断与右边有交集
    if (qr > mid) {
    
    
        val += query(cur << 1 | 1, ql, qr);//求和
    }
    //返回结果
    return val;
}

单点修改 modify()

modify() 跟build()差不多,一直找叶节点,找到之后,修改

//cur代表当前线段树里面的端点。tar代表要修改的位置,即目标位置
void modify(int cur, int tar, int val) {
    
    
    
    int l = tr[cur].l, r = tr[cur].r;
    //如果当前节点就是叶节点,那么直接修改就可以了
    if (l == r) {
    
    
        tr[cur].val = tr[cur].val + val;
        return;
    }
    int mid = l + r >> 1;
    if (tar <= mid) {
    
    
        modify (cur << 1, tar, val);
    } else {
    
    
        modify (cur << 1 | 1, tar, val);
    }
    //递归完之后,要更新到父节点。
    //pushup就是更新父节点的信息
    pushup(cur);    
}

区间修改 (待更新……)

模板题 (HDU 1166)

一个人养水仙花,每盆水仙花都有一个价值,水仙花是排成一行。有三个操作:有时某盆水仙花的价值会上升,有时某盆水仙花的价值会下降。有时他想知道某段连续的水仙花的价值之和是多少,你能快速地告诉她结果吗?

输入格式
第一行一个整数 T T T,表示有 T T T 组测试数据。
每组测试数据的第一行为一个正整数 N N N ( N < = 50000 ) (N<=50000) (N<=50000),表示有N盆水仙花。
接下来有 N N N 个正整数,第 i i i 个正整数 a i a_i ai ( 1 < = a i < = 50 ) (1<=ai<=50) (1<=ai<=50) 表示第i盆水仙花的初始价值。
接下来每行有一条命令,命令有4种形式:
(1) A d d Add Add i i i j j j, i i i j j j 为正整数,表示第 i i i盆水仙花价值增加 j ( j < = 30 ) j (j<=30) j(j<=30)
(2) S u b Sub Sub i i i j j j, i i i j j j 为正整数,表示第 i i i 盆水仙花价值减少 j ( j < = 30 ) j (j<=30) j(j<=30)
(3) Q u e r y Query Query i i i j j j, i i i j j j 为正整数, i < = j i<=j i<=j,表示询问第 i i i 盆水仙花到第 j j j 盆水仙花的价值之和
(4) E n d End End,表示结束,这条命令在每组数据最后出现
每组数据的命令不超过40000条

输出格式
对于第 i i i 组数据,首先输出"Case i:"和回车。
对于每个"Query i j"命令,输出第 i i i盆水仙花到第 j j j 盆水仙花的美观值之和。
Sample Input

1
10
1 2 3 4 5 6 7 8 9 10
Query 1 3
Add 3 6
Query 2 7
Sub 10 2
Add 6 3
Query 3 10
End 

Sample Output

Case 1:
6
33
59

AC代码

#include<cstdio>
#include<iostream>
#include<cstring>
#include<string>
#include<cmath>
#include<map>
#include<algorithm>

#define IOS ios::sync_with_stdio(false); cin.tie(0); cout.tie(0)
#define ll long long
//#define int ll
#define inf 0x3f3f3f3f
using namespace std;
int read()
{
    
    
	int w = 1, s = 0;
	char ch = getchar();
	while (ch < '0' || ch>'9') {
    
     if (ch == '-') w = -1; ch = getchar(); }
	while (ch >= '0' && ch <= '9') {
    
     s = s * 10 + ch - '0';    ch = getchar(); }
	return s * w;
//最大公约数
}int gcd(int x,int y) {
    
    
    if(x<y) swap(x,y);//很多人会遗忘,大数在前小数在后
    //递归终止条件千万不要漏了,辗转相除法
    return x % y ? gcd(y, x % y) : y;
}
int lcm(int x,int y)//计算x和y的最小公倍数
{
    
    
    return x * y / gcd(x, y);//使用公式
}
//------------------------ 以上是我常用模板与刷题几乎无关 ------------------------//
const int N = 50010;
int num[N * 4];	//存储每一个工兵营的人数

struct Node{
    
    
    int l, r;//左端点,右端点
    int val;//区间[l, r]的最大值
}tr[N * 4];


//由子节点的信息来计算父节点的信息
void pushup(int cur){
    
    
    //父节点是两个子节点的和,一直往上传值,就能使根节点是所有人的个数
    tr[cur].val = tr[cur << 1].val + tr[cur << 1 | 1].val;
}

//cur代表当前节点,
void build(int cur, int l, int r){
    
    
    
    //建树的初始值
    tr[cur].l = l, tr[cur].r = r, tr[cur].val = 0;
    //如果已经是叶结点return
    if(l == r) {
    
    
        tr[cur].val = num[l];//如果到达叶节点就把人数赋给该节点
        return;
    }
        
    //否则求一下当前区间的中点
    int mid = l + r >> 1;
    //递归建立左边区间
    build(cur << 1, l, mid);
    //递归建立右边区间
    build(cur << 1 | 1, mid + 1, r);
    
    //将子节点的人数进行汇总,给父节点
    pushup(cur);
}

//[l, r]查询区间   cur代表当前线段树里面的端点。
int query(int cur, int ql, int qr)  {
    
    
    int l = tr[cur].l, r = tr[cur].r;
    //①情况[TL,TR] ? [L,R]
    //树中节点,已经被完全包含在[l, r]中了。
    if(ql <= l && qr >= r)  
        return tr[cur].val;

    int mid = l + r >> 1;
    int val = 0;
    
    //判断与左边有交集
    if (ql <= mid) {
    
    
        val += query(cur << 1, ql, qr);//求和
    }
        
    
    //这里为什么是 qr > mid,因为划分的区间是[l, mid][mid + 1, r],所以要用>而不能=
    //判断与右边有交集
    if (qr > mid) {
    
    
        val += query(cur << 1 | 1, ql, qr);//求和
    }
    //返回结果
    return val;
}

//cur代表当前线段树里面的端点。tar代表要修改的位置,即目标位置
void modify(int cur, int tar, int val) {
    
    
    
    int l = tr[cur].l, r = tr[cur].r;
    //如果当前节点就是叶节点,那么直接修改就可以了
    if (l == r) {
    
    
        tr[cur].val = tr[cur].val + val;
        return;
    }
    int mid = l + r >> 1;
    if (tar <= mid) {
    
    
        modify (cur << 1, tar, val);
    } else {
    
    
        modify (cur << 1 | 1, tar, val);
    }
    //递归完之后,要更新到父节点。
    //pushup就是更新父节点的信息
    pushup(cur);    
}

int main()
{
    
    
    int t = read();
    for(int i = 1; i <= t; i++)
    {
    
    
        int n = read();
        for(int j = 1; j <= n; j++) 
            num[j] = read();
        //建树
        build(1, 1, n);
        int flag = 1;
        while (1) {
    
    
            string str;
            cin >> str;
            //Case 1:
            if (flag) {
    
    
                printf("Case %d:\n", i);
                flag = 0;
            }
            //询问
            if (str == "Query") {
    
    
                int ql = read(), qr = read();
                printf("%d\n", query(1, ql, qr));
            }
            //添加
            if (str == "Add") {
    
    
                int c = read(), m = read();
                modify(1, c, m);
            }
            //减去
            if (str == "Sub") {
    
    
                int c = read(), m = read();
                modify(1, c, -m);
            }
            //结束
            if (str == "End")
                break;
        }
    }
    return 0;
}

猜你喜欢

转载自blog.csdn.net/m0_46272108/article/details/108955623