树状数组及应用

本文详细介绍了树状数组的概念及其在处理区间问题中的应用,包括如何利用树状数组维护区间最大值,以及如何通过树状数组高效计算逆序数。在区间最大值维护中,重点阐述了树状数组如何支持末尾插入修改并保持O(logn)的时间复杂度。而对于逆序数问题,文章解释了如何通过树状数组逐步计算每个新增元素带来的逆序对数量,最终得出序列的逆序数总数。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

树状数组

处理区间问题的常用方法
树状数组入门
树状数组详解

一维树状数组:

//树状数组
//单点修改,区间查询
//单点查询,区间修改
//时空复杂度(O(m*log2(n)),O(n))
#include <bits/stdc++.h>

using namespace std;

const int N=1000000+5;
int a[N];
int c[N];
int lowbit(int x){
    return x&-x;
}
int add(int x,int a)
{
    while(x<N) c[x]+=a,x+=lowbit(x);
}
//log2(n)
/*
归纳法
1.显然,c[1] = [1, 1]
2.假设x < n, c[x]维护都是 [x-lowbit(x)+1, x],证明 n 维护的是 [n-lowbit(n)+1, n]
我们把n写成二进制
X1000 <- x0100 c[x0100] : [x0001, x0100]
      <- x0110 c[x0110] : [x0101, x0110]
      <- x0111 c[x0111] : [x0111, x0111]
      [x0001, x0111] + [x1000, x1000] = [x0001, x1000]
      x1000 - lowbit + 1 = x0001
	  证毕
*/

int sum(int x)
{
    int ans=0;
    while (x) ans+=c[x],x-=lowbit(x);
    return ans;
    //log2n;
}
int main()
{
    int n,m,ty,p,x,L,R;
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]),add(i,a[i]);
    while(m--)
    {
        scanf("%d",&ty);
        if(ty==1)
        {
            scanf("%d%d",&p,&x);
            add(p,x);
        }
        else
        {
            scanf("%d%d",&L,&R);
            printf("%d\n",sum(R)-sum(L-1));//[1,R]-[1,L-1]
        }

    }
    return 0;
}

二维树状数组:

int n,c[N][N];
int lowbit(int x)
{
	return x&(-x);
}
int query_sum(int x, int y)
{
	int res = 0;
	for (int i = x; i > 0; i -= lowbit(i))
	{
		for (int j = y; j > 0; j -= lowbit(j))
		{
			res += c[i][j];
		}
	}
	return res;
}
 
void add(int x, int y, int val)
{
	for (int i = x; i <= n; i += lowbit(i))
	{
		for (int j = y; j <= n; j += lowbit(j))
		{
			c[i][j] += val;
		}
	}
}

树状数组维护区间最大值

树状数组维护区间最大值,这个只支持末尾插入修改,每一次维护和查询的时间复杂度都是 O ( ( l o g n ) 2 ) O((logn)^2) O((logn)2)
一、数组的含义
1、在维护和查询区间和的算法中, h [ x ] h[x] h[x]中储存的是 [ x , x − l o w b i t ( x ) + 1 ] [x,x-lowbit(x)+1] [xxlowbit(x)+1]中每个数的和,
2、在求区间最值的算法中, h [ x ] h[x] h[x]储存的是 [ x , x − l o w b i t ( x ) + 1 ] [x,x-lowbit(x)+1] [xxlowbit(x)+1]中每个数的最大值。
求区间最值的算法中还有一个 a [ i ] a[i] a[i]数组,表示第 i i i个数是多少

在维护区间和的时候只需要求 [ 1 , r ] [1,r] [1,r] [ 1 , l [1,l [1,l- 1 ] 1] 1],这两个前缀和相减即可。
但维护最值时就不太一样需要将 [ l , r ] [l,r] [l,r]分为若干个子区间然后合并

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
using namespace std;

const int MAXN = 3e5;
int a[MAXN], h[MAXN];
int n, m, x;

int lowbit(int x)
{
	return x & (-x);
}
void updata(int x)
{
	int lx, i;
	while (x <= n)
	{
		h[x] = a[x];
		lx = lowbit(x);
		for (i=1; i<lx; i<<=1)
			h[x] = min(h[x], h[x-i]);
		x += lowbit(x);
	}
}
int query(int x, int y)
{
	int ans = 0;
	while (y >= x)
	{
		ans = min(a[y], ans);
		y --;
		for (; y-lowbit(y) >= x; y -= lowbit(y))
			ans = min(h[y], ans);
	}
	return ans;
}

int main()
{
    cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i];
    for(int i=1;i<=n;i++) updata(i);
    for(int i=1;i<=n;i++)
    {
        cin>>x;
        a[x]=2*n+1; updata(x);
        for(int i=1;i<=n;i++)
        printf("%d ",h[i]); printf("\n");
    }

}

区间修改
单点查询
a d d ( L , x ) ; add(L, x); add(L,x);
a d d ( R + 1 , − x ) ; add(R+1, -x); add(R+1,x);
s u m ( p ) ; sum(p); sum(p);

首先引入差分数组 d d d,设原数组为 a a a,令 d [ i ] = a [ i ] − a [ i − 1 ] d[i]=a[i]-a[i-1] d[i]=a[i]a[i1].由此关系式得 a [ i ] = c 1 [ 1 ] + c 1 [ 2 ] + … + c 1 [ i ] a[i]=c1[1]+c1[2]+…+c1[i] a[i]=c1[1]+c1[2]++c1[i],以下操作的树状数组都是原数组的差分数组。
也就是 a [ j ] a[j] a[j]等于 d [ j ] d[j] d[j]的前 j j j 项和,即前缀和。于此,我们的树状数组维护的是 d d d 的前缀和。
1、单点查询:
有以上推理得,查询 a [ i ] a[i] a[i]相当于查询 b [ i ] b[i] b[i]的前缀和,用树状数组操作即可。
(注意:树状数组维护的是 b b b数组,不是原数组!)
s u m ( p ) ; sum(p); sum(p);

2、区间修改:
因为对 a a a的区间 [ i , j ] [i,j] [i,j] x x x,就相当于 a [ i ] a[i] a[i] a [ i − 1 ] a[i-1] a[i1] x x x a [ j + 1 ] a[j+1] a[j+1] a [ j ] a[j] a[j] x x x,就相当于对 a [ i ] a[i] a[i] x x x,对 a [ j + 1 ] a[j+1] a[j+1] x x x
因为 a [ i ] a[i] a[i] 等于 d [ i ] d[i] d[i] 的前缀和,所以 a [ i ] + x a[i]+x a[i]+x 就相当于对 d [ i ] d[i] d[i] 的前缀和加 x x x,可以用树状数组操作。
同理, a [ j + 1 ] − x a[j+1]-x a[j+1]x等于 b [ j + 1 ] b[j+1] b[j+1]的前缀和减 x x x,用树状数组操作。
a d d ( L , x ) ; add(L, x); add(L,x);
a d d ( R + 1 , − x ) ; add(R+1, -x); add(R+1,x);

逆序数
1、什么是逆序数?
在一个排列中,如果一对数的前后位置与大小顺序相反,即前面的数大于后面的数,那么它们就称为一个逆序。一个排列中逆序数的总数就是这个排列的逆序数。

2、用树状数组求逆序数的总数
2.1该背景下树状数组的含义
我们假设一个数组 A [ n ] A[n] A[n],当 A [ n ] = 0 A[n]=0 A[n]=0时表示数字n在序列中没有出现过, A [ n ] = 1 A[n]=1 A[n]=1表示数字 n n n 在序列中出现过。 A A A对应的树状数组为 c [ n ] c[n] c[n],则 c [ n ] c[n] c[n]对应维护的是数组 A [ n ] A[n] A[n]的内容,即树状数组 c c c可用于求 A A A中某个区间的值的和。

树状数组的插入函数(假设为 v o i d i n s e r t ( i n t i , i n t x ) ) void insert(int i,int x) ) voidinsert(inti,intx))的含义:在求逆序数这个问题中,我们的插入函数通常使用为 i n s e r t ( i , 1 ) insert( i , 1 ) insert(i,1),即将数组 A [ i ] A[i] A[i]的值加 1 1 1 ( A A A数组开始应该初始化为 0 0 0,所以也可以理解为设置 A [ i ] A[ i ] A[i]的值为 1 1 1,即将数字i 加入到序列的意思 )。,同时维护c数组的值。

树状数组中区间求和函数(假设函数定义为: i n t g e t s u n ( i n t i ) ) int getsun(int i ) ) intgetsun(inti))的含义:该函数的作用是用于求序列中小于等于数字 i 的元素的个数。这个是显而易见的,因为树状数组c 维护的是数组A的值,则该求和函数即是用于求下标小于等于 i 的数组A的和,而数组A中元素的值要么是0要么是1,所以最后求出来的就是小于等于i的元素的个数。

所以要求序列中比元素a大的数的个数,可以用 i − g e t s u m ( a ) i - getsum(a) igetsum(a)即可( i 表示此时序列中元素的个数)。

2.2如何使用树状数组求逆序数总数
首先来看如何减小问题的规模:
要想求一个序列 a b c d,的逆序数的个数,可以理解为先求出a b c的逆序数的个数k1,再在这个序列后面增加一个数d,求d之前的那个序列中值小于d的元素的个数 k 2 k2 k2,则 k 1 + k 2 k1+k2 k1+k2即为序列 a b c d a b c d abcd的逆序数的个数。

举个例子加以说明:
假设给定的序列为 4 3 2 1,我们从左往右依次将给定的序列输入,每次输入一个数temp时,就将当前序列中大于 t e m p temp temp的元素的个数计算出来,并累加到 a n s ans ans中,最后 a n s ans ans就是这个序列的逆序数个数。

序列的变化(下划线为新增加元素)
序列中大于新增加的数字的个数
操作
{ ____ } 0
初始化时序列中一个数都没有
{4___ } 0
往序列中增加4,统计此时序列中大于4的元素个数
{4 3__ } 1
往序列中增加3,统计此时序列中大于3的元素个数
{4 3 2_ } 2
往序列中增加2,统计此时序列中大于2的元素个数
{4 3 2 1} 3
往序列中增加1,统计此时序列中大于1的元素个数
当所有的元素都插入到序列后,即可得到序列{4 3 2 1}的逆序数的个数为 1 + 2 + 3 = 6. 1+2+3=6. 1+2+3=6.
a n s = 0 ; ans = 0; ans=0;
f o r ( i n t for (int for(int i = 1 ; i < = n ; i + + ) i = 1; i <= n; i++) i=1;i<=n;i++) a d d ( a [ i ] , 1 ) , a n s + = i − 1 − s u m ( a [ i ] − 1 ) ; add(a[i], 1), ans +=i-1-sum(a[i]-1); add(a[i],1),ans+=i1sum(a[i]1);

树状数组求逆序数+离散化

#include <iostream>
#include <string.h>
#include <algorithm>
#include <stdio.h>
 
using namespace std;
const int N = 500005;
 
struct Node
{
    int v,order;
};
 
int n;
int c[N];
int aa[N];    //离散化后的数组
Node a[N];    //树状数组
 
int Lowbit(int x)
{
    return x & (-x);
}
 
void Update(int t,int val)
{
    for(int i=t; i<=n; i+=Lowbit(i))
        c[i] += val;
}
 
int getSum(int x)
{
    int ans=0;
    for(int i=x; i>0; i-=Lowbit(i))
        ans += c[i];
    return ans;
}
 
bool cmp(Node a,Node b)
{
    return a.v<b.v;
}
 
int main()
{
    while(~scanf("%d",&n),n)
    {
        //离散化
        for(int i=1; i<=n; i++)
        {
            scanf("%d",&a[i].v);
            a[i].order=i;
        }
        sort(a+1,a+1+n,cmp);
        for(int i=1; i<=n; i++)
            aa[a[i].order]=i;
 
        //树状数组求逆序数
        memset(c,0,sizeof(c));
        long long ans=0;
        for(int i=1; i<=n; i++)
        {
            Update(aa[i],1);
            ans+=i-getSum(aa[i]);
        }
        printf("%I64d\n",ans);
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值