SCP认证复习——线段树与树状数组

Posted by Linexus on Monday, March 6, 2023

线段树与树状数组

线段树

题目描述

如题,已知一个数列,你需要进行下面两种操作:

  1. 将某区间每一个数加上 k。
  2. 求出某区间每一个数的和。

输入格式

第一行包含两个整数n*,*m,分别表示该数列数字的个数和操作的总个数。

第二行包含 nn 个用空格分隔的整数,其中第 ii 个数字表示数列第 ii 项的初始值。

接下来 mm 行每行包含 33 或 44 个整数,表示一个操作,具体如下:

  1. 1 x y k:将区间[x,y]内每个数加上 k
  2. 2 x y:输出区间[x,y]内每个数的和。

输出格式

输出包含若干行整数,即为所有操作 2 的结果。

输入输出样例

输入 #1

5 5
1 5 4 2 3
2 2 4
1 2 3 2
2 3 4
1 1 5 1
2 1 4

输出 #1

11
8
20

线段树的关键数据结构:

num[MAX]
ans[MAX << 2]
lazy[MAX << 2]

其中num作为输入的数字集合,ans作为线段树的集合,lazy作为懒标记,懒记录了当前区间的更新值。

update操作

//其中x-y是当前更新区间,l-r是当前划分区间的两端,k是更新值,p是区间段值
void update(int x, int y, int l, int r, int k, int p)
{
    if (x <= l && y >= r)
    {
        //更新当前区间值,并阻断下方的更新,做到懒更新
        ans[p] += (ll)k * (r - l + 1);
        lazy[p] += k;
        return;
    }
    //伸展操作,如果当前层不是更新的最底层,则向下伸展更新下一层
    spread(p, r, l);
    int mid = r + l >> 1;
    if (x <= mid)
        update(x, y, l, mid, k, p << 1);
    if (y > mid)
        update(x, y, mid + 1, r, k, p << 1 | 1);
    //更新时只有最底层在更新,递归返回后更新上层
    ans[p] = ans[p << 1] + ans[(p << 1) + 1];
}

spread操作

//伸展操作,将lazy[p]传递给子结点,并更新子结点
void spread(int p, int r, int l)
{
    if (lazy[p] != 0)
    {
        int mid = (r + l) >> 1;
        ans[p << 1] += lazy[p] * (mid - l + 1);
        ans[(p << 1) | 1] += lazy[p] * (r - mid);
        lazy[p << 1] += lazy[p];
        lazy[(p << 1) | 1] += lazy[p];
        lazy[p] = 0;
    }
}

read操作

ll read(int x, int y, int l, int r, int p)
{
    if (x <= l && y >= r)
    {
        return ans[p];
    }
    //有时候需要读入未更新的底层,所以需要伸展
    spread(p, r, l);
    int mid = r + l >> 1;
    ll sum = 0;
    if (x <= mid)
        sum += read(x, y, l, mid, p << 1);
    if (y >= mid + 1)
        sum += read(x, y, mid + 1, r, p << 1|1);
    return sum;
}

建树操作

//建立线段树
void build(int p, int l, int r)
{
    if (l == r)
    {
        ans[p] = num[l];
        return;
    }
    int mid = l + r >> 1;
    build((p << 1), l, mid);
    build((p << 1) + 1, mid + 1, r);
    ans[p] = ans[p << 1] + ans[(p << 1) + 1];
}

树状数组

题目描述

如题,已知一个数列,你需要进行下面两种操作:

  • 将某一个数加上 x
  • 求出某区间每一个数的和

输入格式

第一行包含两个正整数 n,m,分别表示该数列数字的个数和操作的总个数。

第二行包含 n个用空格分隔的整数,其中第 i个数字表示数列第 i项的初始值。

接下来 m行每行包含 33 个整数,表示一个操作,具体如下:

  • 1 x k 含义:将第 x个数加上 k
  • 2 x y 含义:输出区间 [x,y][x,y] 内每个数的和

输出格式

输出包含若干行整数,即为所有操作 22 的结果。

输入输出样例

输入 #1

5 5
1 5 4 2 3
1 1 3
2 2 5
1 3 -1
1 4 2
2 1 4

输出 #1

14
16

参考博客,讲的非常好

https://blog.csdn.net/TheWayForDream/article/details/118436732

#include<iostream>
#include<cstring>
#define lowbit(x) (x&(-x)) 
using namespace std;
typedef long long ll;
const int Maxn=1e6+5;
int a[500005];
int d[Maxn]={0};//d[i]的值,d[i]表示第i和i-1个数的差值
ll c[Maxn]; 
int n,m;
int update(int pos,int k)//pos表示修改点的位置,K表示修改的值也即+K操作
{
	for(int i=pos;i<=n;i+=lowbit(i))
	c[i]+=k;
	return 0;
}
ll ask_qujian(int pos)//返回区间pos到1的总和
{
	ll ans=0;
	for(int i=pos;i;i-=lowbit(i)) ans+=c[i];
	return ans;
} 
int main()
{
	memset(c,0,sizeof(c));
	scanf("%d %d",&n,&m);
	a[0]=0;
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
		d[i]=a[i]-a[i-1];
		update(i,d[i]);
	}
	for(int i=1;i<=m;i++)
	{
		int choice,x,y,k;
		scanf("%d",&choice);
		if(choice==1)
		{
			scanf("%d %d %d",&x,&y,&k);
			update(x,k);
			update(y+1,-k);
		}
		else
		{
			scanf("%d",&x);
			printf("%lld\n",ask_qujian(x));
		}
	}
	return 0;
}

#include<iostream>
#define lowbit(x) (x&(-x))
typedef long long ll; 
using namespace std;
int c[2000006];
int n,m;
ll ans;
int add_dandian(int x,int k)
{
	for(int i=x;i<=n;i+=lowbit(i))
	c[i]+=k;
}
int search(int begin,int end)
{
	for(int i=end;i;i-=lowbit(i))
	ans+=c[i];
	for(int i=begin-1;i;i-=lowbit(i))
	ans-=c[i];
	return 0;
}
int main()
{
	scanf("%d %d",&n,&m);
	for(int i=1;i<=n;i++)
	{
		int number;
		scanf("%d",&number);
		add_dandian(i,number);
	}
	for(int i=1;i<=m;i++)
	{
		int choice,x,y;
		scanf("%d %d %d",&choice,&x,&y);
		if(choice==1) add_dandian(x,y);
		else
		{
			ans=0;
			search(x,y);
			printf("%lld\n",ans);
		}
	}
	return 0;
}