树状数组与线段树

文章目录

  • 一.树状数组
      • 核心函数
  • 二.线段树
        • 修改操作
        • 查询操作
    • 1.动态求连续区间和
        • 树状数组
        • 线段树
    • 2.数星星
    • 3.数列区间最大值
    • 4.小朋友排队
    • 5.油漆面积
    • 6.三体攻击
    • 7.螺旋折线

一.树状数组

  1. 给区间的某个位置上的数加上一个数(单点修改)
  2. 快速动态地求某一个前缀和(区间查询)
    原数组A 数组底数从1开始
    树状数组C:i为奇数 C[i] = A[i]
    i为偶数 C[i] = A[1] +…+A[i]
    树状数组与线段树_第1张图片

lowbit(x) = x& -x;
c[x] = (x - lowbit(x), x);

  • 1、lowbit(x):返回x的最后一位1

  • 2、add(x,v):在x位置加上v,并将后面相关联的位置也加上v

  • 3、query(x):询问x的前缀和

核心函数

int lowbit(int x)
{
	return x & -x;
}
void add(int x, int y)//更新
{
	for(int i = x; i <= n; i += lowbit(i)) tr[i] += y;
}

int query(int x)//求和
{
	int res = 0;
	for(int i = x; i; i -= lowbit(i)) res +=tr[i];
	return res;
}

二.线段树

线段树,英文名称是Segment Tree,其本质也是一个二叉搜索树,区别在于线段树的每一个节点记录的都是一个区间,每个区间都被平均分为2个子区间,作为它的左右儿子。比如说区间[1,10],被分为区间[1,5]作为左儿子,区间[6,10]作为右儿子:
树状数组与线段树_第2张图片
操作1:单点修改(递归+回溯)
操作2:区间查询(某个区间的总和)
pushup:用子节点信息更新当前信息
build:在一段区间上初始化
modify:修改
query:查询

  • 1、pushup(u):用子节点信息来更新当前节点信息(把信息往上传递)
  • 2、build(u,l,r):在一段区间上初始化线段树,其中u表示根结点,l表示左边界,r表示右边界
  • 3、query(u,l,r):查询某段区间的和,其中u表示根结点,l表示左边界,r表示右边界
  • 4、modify(u,x,v):修改操作,在u结点中,x位置加上v

修改操作

树状数组与线段树_第3张图片

查询操作

树状数组与线段树_第4张图片

1.动态求连续区间和

ACWing1264. 动态求连续区间和
给定 n 个数组成的一个数列,规定有两种操作,一是修改某个元素,二是求子数列 [a,b] 的连续和。

输入格式
第一行包含两个整数 n 和 m,分别表示数的个数和操作次数。

第二行包含 n 个整数,表示完整数列。

接下来 m 行,每行包含三个整数 k,a,b (k=0,表示求子数列[a,b]的和;k=1,表示第 a 个数加 b)。

数列从 1 开始计数。

输出格式
输出若干行数字,表示 k=0 时,对应的子数列 [a,b] 的连续和。

数据范围
1≤n≤100000,
1≤m≤100000,
1≤a≤b≤n
输入样例:

10 5
1 2 3 4 5 6 7 8 9 10
1 1 5
0 1 3
0 4 8
1 7 5
0 4 8

输出样例:

11
30
35

树状数组

#include
#include
#include
#include
using namespace std;
const int N = 100010;
int n, m;
int a[N], tr[N]; 
int lowbit(int x)
{
	return x & -x;
}
void add(int x, int y)
{
	for(int i = x; i <= n; i += lowbit(i)) tr[i] += y;
}

int query(int x)
{
	int res = 0;
	for(int i = x; i; i -= lowbit(i)) res +=tr[i];
	return res;
}
int main()
{
  scanf("%d%d", &n , &m);
  for(int i = 1; i <= n; i++) scanf("%d", &a[i]);
  for(int i = 1; i <= n; i++) add(i, a[i]);
  
  while(m--)
  {
  	int k,x,y;
  	scanf("%d%d%d", &k, &x, &y);
  	if(k==0) printf("%d\n", query(y) - query(x - 1));
  	else add(x,y);
  }
  return 0;
} 



线段树

#include 
#include 
#include 
#include 

using namespace std;

const int N = 100010;

int n, m, w[N];

//线段树结点,l,r 分别表示区间的左右端点, sum表示该区间的和
struct Knode{
    int l, r;
    int sum;
}tr[4 * N];

//函数更新结点信息,即求和
void pushup(int u){
    tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;
}

//建立线段树,即建树
void build(int u, int l, int r){//l, r 表示当前结点区间, u表示当前结点的实际存储位置
    if(l == r) tr[u] = {l, r, w[l]};//若达到叶结点
    else{
        tr[u] = {l, r};
        int mid = l + r >> 1;
        build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);//左右递归
        pushup(u);//左右递归
    }
}

//区间查询,即求区间l,r的和
int query(int u, int l, int r){//l, r 表示当前查询区间,u表示当前结点编号
    if(tr[u].l >= l && tr[u].r <= r) return tr[u].sum;
    int mid = tr[u].l + tr[u].r >> 1;
    int sum = 0;
    if(l <= mid) sum += query(u << 1, l, r);//左子区间与l, r有重叠,递归
    if(r > mid) sum += query(u << 1 | 1, l, r);//右子区间与l, r 有重叠,递归
    return sum;
}

//单点修改,即给某个数加上一个数
void modify(int u, int x, int v){
    if(tr[u].l == tr[u].r) 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);
    }
}

int main(){
    cin >> n >> m;
    for(int i = 1; i <= n; i ++) scanf("%d", &w[i]);

    build(1, 1, n);//在区间l——n建树

    while(m --){
        int k, a, b;
        scanf("%d%d%d", &k, &a, &b);
        if(k == 0) printf("%d\n",query(1, a, b));
        else modify(1, a, b);
    }
    return 0;
}

2.数星星

ACWing1265. 数星星
天空中有一些星星,这些星星都在不同的位置,每个星星有个坐标。

如果一个星星的左下方(包含正左和正下)有 k 颗星星,就说这颗星星是 k 级的。

树状数组与线段树_第5张图片

例如,上图中星星 5 是 3 级的(1,2,4 在它左下),星星 2,4 是 1 级的。

例图中有 1 个 0 级,2 个 1 级,1 个 2 级,1 个 3 级的星星。

给定星星的位置,输出各级星星的数目。

换句话说,给定 N 个点,定义每个点的等级是在该点左下方(含正左、正下)的点的数目,试统计每个等级有多少个点。

输入格式
第一行一个整数 N,表示星星的数目;

接下来 N 行给出每颗星星的坐标,坐标用两个整数 x,y 表示;

不会有星星重叠。星星按 y 坐标增序给出,y 坐标相同的按 x 坐标增序给出。

输出格式
N 行,每行一个整数,分别是 0 级,1 级,2 级,……,N−1 级的星星的数目。

数据范围
1≤N≤15000,
0≤x,y≤32000
输入样例:

5
1 1
5 1
7 1
3 3
5 5

输出样例:

1
2
1
1
0
#include
#include
#include
#include
using namespace std;
const int N = 32020;
int n;
int tr[N], level[N];
int lowbit(int x)
{
	return x & -x;
}
int add(int x)
{
	for(int i = x; i <= N; i+= lowbit(i)) tr[i]++;
}
int sum(int x)
{
	int res = 0;
	for(int i = x; i ; i -= lowbit(i)) res += tr[i];
	return res;
}
int main()
{
  scanf("%d", &n);
  for(int i = 0; i < n; i++)
  {
  	int x, y;
  	cin >> x >> y;
  	level[sum(x)]++;
  	add(x);
  }
  for(int i = 0;i < n; i++) printf("%d\n", level[i]);
  return 0;
} 



3.数列区间最大值

ACWing1270. 数列区间最大值
输入一串数字,给你 M 个询问,每次询问就给你两个数字 X,Y,要求你说出 X 到 Y 这段区间内的最大数。

输入格式
第一行两个整数 N,M 表示数字的个数和要询问的次数;

接下来一行为 N 个数;

接下来 M 行,每行都有两个整数 X,Y。

输出格式
输出共 M 行,每行输出一个数。

数据范围
1≤N≤105,
1≤M≤106,
1≤X≤Y≤N,
数列中的数字均不超过231−1
输入样例:

10 2
3 2 4 5 6 8 1 2 9 7
1 4
3 8

输出样例:

5
8
#include 
#include 
#include 
#include 
#include 
using namespace std;

const int N = 100010;

int n, m, w[N];

//线段树结点,l,r 分别表示区间的左右端点, sum表示该区间的和
struct Knode{
    int l, r;
    int maxv;
}tr[4 * N];

//函数更新结点信息,即求和
void pushup(int u){
    tr[u].maxv = max(tr[u<<1].maxv, tr[u<<1|1].maxv);
}

//建立线段树,即建树
void build(int u, int l, int r){//l, r 表示当前结点区间, u表示当前结点的实际存储位置
    if(l == r) tr[u] = {l, r, w[l]};//若达到叶结点
    else{
        tr[u] = {l, r};
        int mid = l + r >> 1;
        build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);//左右递归
        pushup(u);//左右递归
    }
}

//区间查询,即求区间l,r的和
int query(int u, int l, int r){//l, r 表示当前查询区间,u表示当前结点编号
    if(tr[u].l >= l && tr[u].r <= r) return tr[u].maxv;
    int mid = tr[u].l + tr[u].r >> 1;//树中曲线的中点 
    int maxv = -INT_MIN;
    if(l <= mid) maxv = max(maxv,query(u << 1, l, r));//左子区间与l, r有重叠,递归
    if(r > mid) maxv = max(maxv, query(u << 1 | 1, l, r));//右子区间与l, r 有重叠,递归
    return maxv;
}

int main(){
    cin >> n >> m;
    for(int i = 1; i <= n; i ++) scanf("%d", &w[i]);

    build(1, 1, n);//在区间l——n建树
    int l, r;
    while(m --){
        scanf("%d%d", &l, &r);
        printf("%d\n",query(1, l, r));
    }
    return 0;
}

4.小朋友排队

AcWing1215. 小朋友排队
n 个小朋友站成一排。

现在要把他们按身高从低到高的顺序排列,但是每次只能交换位置相邻的两个小朋友。

每个小朋友都有一个不高兴的程度。

开始的时候,所有小朋友的不高兴程度都是 0。

如果某个小朋友第一次被要求交换,则他的不高兴程度增加 1,如果第二次要求他交换,则他的不高兴程度增加 2(即不高兴程度为 3),依次类推。当要求某个小朋友第 k 次交换时,他的不高兴程度增加 k。

请问,要让所有小朋友按从低到高排队,他们的不高兴程度之和最小是多少。

如果有两个小朋友身高一样,则他们谁站在谁前面是没有关系的。

输入格式
输入的第一行包含一个整数 n,表示小朋友的个数。

第二行包含 n 个整数 H1,H2,…,Hn,分别表示每个小朋友的身高。

输出格式
输出一行,包含一个整数,表示小朋友的不高兴程度和的最小值。

数据范围
1≤n≤100000,
0≤Hi≤1000000
输入样例:

3
3 2 1

输出样例:

9

样例解释
首先交换身高为3和2的小朋友,再交换身高为3和1的小朋友,再交换身高为2和1的小朋友,每个小朋友的不高兴程度都是3,总和为9。

冒泡排序 时间太久,可以考虑归并排序和线段树的方法,记录每个数前面比它大的数的个数,以及后面比它小是数的个数,就是他们应该要交换的最小数,需要考虑分配问题,平均分配,不高兴程度比较小。

贪心的想法:每个数的交换次数是固定的;记录每个数前面比它大的数的个数,以及后面比它小是数的个数,就是他们应该要交换的最小数。

#include
#include
#include
#include
using namespace std;
typedef long long LL;
const int N = 1000010;
int n;
int h[N], tr[N];
int sum[N];
int lowbit(int x)
{
	return x & -x;
	
}

void add(int x, int v)
{
	for(int i = x; i < N; i += lowbit(i)) tr[i] += v;
 } 
 
 int query(int x)
 {
 	int res = 0;
 	for(int i = x; i; i -= lowbit(i)) res += tr[i];
 	return res;
 }
 int main()
 {
 	scanf("%d", &n);
 	for(int i = 0;i < n; i++) scanf("%d", &h[i]);
    //求每个数前面有多少个数比它大 
 	for(int i = 0; i <n; i++) 
 	{
 		sum[i] = query(N-1) - query(h[i]);
 		add(h[i], 1);
	 }
	 
	 //求每个数后面有多少个数比它小
	 memset(tr,0,sizeof tr);//记得初始化
	 for(int i = n -1; i >= 0; i--)
	 {
	 	sum[i] += query(h[i] -1);
	 	add(h[i], 1);
	  } 
	  
	  LL res = 0;
	  for(int i = 0; i <n; i++) res +=(LL)sum[i]*(sum[i]+1)/2;
	  cout << res << endl;
	  return 0;
 }

5.油漆面积

AcWing1228. 油漆面积
X星球的一批考古机器人正在一片废墟上考古。

该区域的地面坚硬如石、平整如镜。

管理人员为方便,建立了标准的直角坐标系。

每个机器人都各有特长、身怀绝技。

它们感兴趣的内容也不相同。

经过各种测量,每个机器人都会报告一个或多个矩形区域,作为优先考古的区域。

矩形的表示格式为 (x1,y1,x2,y2),代表矩形的两个对角点坐标。

为了醒目,总部要求对所有机器人选中的矩形区域涂黄色油漆。

小明并不需要当油漆工,只是他需要计算一下,一共要耗费多少油漆。

其实这也不难,只要算出所有矩形覆盖的区域一共有多大面积就可以了。

注意,各个矩形间可能重叠。

输入格式
第一行,一个整数 n,表示有多少个矩形。

接下来的 n 行,每行有 4 个整数 x1,y1,x2,y2,空格分开,表示矩形的两个对角顶点坐标。

输出格式
一行一个整数,表示矩形覆盖的总面积。

数据范围
1≤n≤10000,
0≤x1,x2,y2,y2≤10000
数据保证 x1

输入样例1:

3
1 5 10 10
3 1 20 20
2 7 15 17

输出样例1:

340

输入样例2:

3
5 2 10 6
2 7 12 10
8 1 15 15

输出样例2:

128


#include 
#include 

using namespace std;

const int N = 10010;

int n;

struct Segment {
    int x, y1, y2;
    int k;

    bool operator<(const Segment &t) const {
        return x < t.x;
    }
} seg[N * 2];

struct Node {
    int l, r;
    int cnt, len;
} tr[N * 4];

void pushup(int u) {
    if (tr[u].cnt > 0) tr[u].len = tr[u].r - tr[u].l + 1;
    else if (tr[u].l == tr[u].r) tr[u].len = 0;
    else tr[u].len = tr[u << 1].len + tr[u << 1 | 1].len;
}

void build(int u, int l, int r) {
    tr[u] = {l, r};
    if (l == r) return;
    int mid = l + r >> 1;
    build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
}

void modify(int u, int l, int r, int k) {
    if (tr[u].l >= l && tr[u].r <= r) {
        tr[u].cnt += k;
        pushup(u);
    } else {
        int mid = tr[u].l + tr[u].r >> 1;
        if (l <= mid) modify(u << 1, l, r, k);
        if (r > mid) modify(u << 1 | 1, l, r, k);
        pushup(u);
    }
}

int main() {
    scanf("%d", &n);
    int m = 0;
    for (int i = 0; i < n; i++) {
        int x1, y1, x2, y2;
        scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
        seg[m++] = {x1, y1, y2, 1};
        seg[m++] = {x2, y1, y2, -1};
    }

    sort(seg, seg + m);

    build(1, 0, 10000);

    int res = 0;
    for (int i = 0; i < m; i++) {
        if (i > 0) res += tr[1].len * (seg[i].x - seg[i - 1].x);
        modify(1, seg[i].y1, seg[i].y2 - 1, seg[i].k);
    }

    printf("%d\n", res);

    return 0;
}

6.三体攻击

AcWing1232. 三体攻击
树状数组与线段树_第6张图片
树状数组与线段树_第7张图片

输入样例:

2 2 2 3
1 1 1 1 1 1 1 1
1 2 1 2 1 1 1
1 1 1 2 1 2 1
1 1 1 1 1 1 2

输出样例:

2

样例解释
在第 2 轮攻击后,战舰 (1,1,1) 总共受到了 2 点伤害,超出其防御力导致爆炸。

#include 
#include 
#include 
#include 

using namespace std;

typedef long long LL;

const int N = 2000010;

int A, B, C, m;
LL s[N], b[N], bp[N];
int d[8][4] = {
    {0, 0, 0, 1},
    {0, 0, 1, -1},
    {0, 1, 0, -1},
    {0, 1, 1, 1},
    {1, 0, 0, -1},
    {1, 0, 1, 1},
    {1, 1, 0, 1},
    {1, 1, 1, -1},
};
int op[N / 2][7];


int get(int i, int j, int k)
{
    return (i * B + j) * C + k;
}

bool check(int mid)
{
    memcpy(b, bp, sizeof b);
    for (int i = 1; i <= mid; i ++ )
    {
        int x1 = op[i][0], x2 = op[i][1], y1 = op[i][2], y2 = op[i][3], z1 = op[i][4], z2 = op[i][5], h = op[i][6];
        b[get(x1,     y1,     z1)]     -= h;
        b[get(x1,     y1,     z2 + 1)] += h;
        b[get(x1,     y2 + 1, z1)]     += h;
        b[get(x1,     y2 + 1, z2 + 1)] -= h;
        b[get(x2 + 1, y1,     z1)]     += h;
        b[get(x2 + 1, y1,     z2 + 1)] -= h;
        b[get(x2 + 1, y2 + 1, z1)]     -= h;
        b[get(x2 + 1, y2 + 1, z2 + 1)] += h;
    }

    memset(s, 0, sizeof s);
    for (int i = 1; i <= A; i ++ )
        for (int j = 1; j <= B; j ++ )
            for (int k = 1; k <= C; k ++ )
            {
                s[get(i, j, k)] = b[get(i, j, k)];
                for (int u = 1; u < 8; u ++ )
                {
                    int x = i - d[u][0], y = j - d[u][1], z = k - d[u][2], t = d[u][3];
                    s[get(i, j, k)] -= s[get(x, y, z)] * t;
                }

                if (s[get(i, j, k)] < 0) return true;
            }

    return false;
}

int main()
{
    scanf("%d%d%d%d", &A, &B, &C, &m);

    for (int i = 1; i <= A; i ++ )
        for (int j = 1; j <= B; j ++ )
            for (int k = 1; k <= C; k ++ )
                scanf("%lld", &s[get(i, j, k)]);

    for (int i = 1; i <= A; i ++ )
        for (int j = 1; j <= B; j ++ )
            for (int k = 1; k <= C; k ++ )
                for (int u = 0; u < 8; u ++ )
                {
                    int x = i - d[u][0], y = j - d[u][1], z = k - d[u][2], t = d[u][3];
                    bp[get(i, j, k)] += s[get(x, y, z)] * t;
                }

    for (int i = 1; i <= m; i ++ )
        for (int j = 0; j < 7; j ++ )
            scanf("%d", &op[i][j]);

    int l = 1, r = m;
    while (l < r)
    {
        int mid = l + r >> 1;
        if (check(mid)) r = mid;
        else l = mid + 1;
    }

    printf("%d\n", r);

    return 0;
}

7.螺旋折线

ACWing1237. 螺旋折线
如下图所示的螺旋折线经过平面上所有整点恰好一次。
树状数组与线段树_第8张图片
树状数组与线段树_第9张图片
树状数组与线段树_第10张图片
找规律,先找每个对角线上的规律,然后找每条边的规律,找到偏移量如何计算
(x, y)—> 先判断是在哪个方向上的边—> 找该方向的特殊点(起点)

#include 
#include 
#include 

using namespace std;

typedef long long LL;

int main()
{
    int x, y;
    cin >> x >> y;

    if (abs(x) <= y)  // 在上方
    {
        int n = y;
        cout << (LL)(2 * n - 1) * (2 * n) + x - (-n) << endl;
    }
    else if (abs(y) <= x)  // 在右方
    {
        int n = x;
        cout << (LL)(2 * n) * (2 * n) + n - y << endl;
    }
    else if (abs(x) <= abs(y) + 1 && y < 0)  // 在下方
    {
        int n = abs(y);
        cout << (LL)(2 * n) * (2 * n + 1) + n - x << endl;
    }
    else  // 在左方
    {
        int n = abs(x);
        cout << (LL)(2 * n - 1) * (2 * n - 1) + y - (-n + 1) << endl;
    }

    return 0;
}


来源:AcWing

你可能感兴趣的:(算法基础,算法)