树状数组
一维树状数组:
//树状数组
//单点修改,区间查询
//单点查询,区间修改
//时空复杂度(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]
[x,x−lowbit(x)+1]中每个数的和,
2、在求区间最值的算法中,
h
[
x
]
h[x]
h[x]储存的是
[
x
,
x
−
l
o
w
b
i
t
(
x
)
+
1
]
[x,x-lowbit(x)+1]
[x,x−lowbit(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[i−1].由此关系式得
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[i−1]大
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) i−getsum(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+=i−1−sum(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;
}