目录
- beginning
- 顺序结构 A+B
- 高精 A+B
- 压位高精 A+B
- 二分A+B
- 树状数组简介(不喜欢啰嗦的请直接跳到这里)
- 基础概念
- 代码实现
- 大体结构
- lowbit
- lowbit的作用
- 总结+代码
- 逆序对
- 离散化
- 方式
- 实现
- 代码
- 树状数组进阶
- 差分
- 区间修改+单点查询
- 主要思想
- 单点查询
- 区间修改
- 区间修改+区间查询
- 差分分析
- 代码
- 2D树状数组
- query
- update
- 区间修改+单点查询
- 区间修改+区间查询
- 时间复杂度
- 罗列例题
- 一维
- beginning
- 顺序结构 A+B
- 高精 A+B
- 压位高精 A+B
- 二分A+B
- 树状数组简介(不喜欢啰嗦的请直接跳到这里)
- 基础概念
- 代码实现
- 大体结构
- lowbit
- lowbit的作用
- 总结+代码
- 逆序对
- 离散化
- 方式
- 实现
- 代码
- 树状数组进阶
- 差分
- 区间修改+单点查询
- 主要思想
- 单点查询
- 区间修改
- 区间修改+区间查询
- 差分分析
- 代码
- 2D树状数组
- query
- update
- 区间修改+单点查询
- 区间修改+区间查询
- 时间复杂度
- 罗列例题
- 一维
---恢复内容开始---
@(浅析树状数组——B.I.T.)
膜拜第
个来访本蒟蒻博客的大佬
beginning
让我们先从这道题开始:P1001 A+B Problem
说明: 我不是在搞笑 !
我们可以探讨一下,用我们所学的知识可以如何解决这道题.
顺序结构 A+B
#include<cstdio>
int a,b;
int main()
{
scanf("%d%d",&a,&b);
printf("%d",a+b);
}
明显没有任何毛病…… 傻子才会说有毛病
高精 A+B
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int S(const char *a) { return strlen(a); }
int mx(int a,int b) { return a>b?a:b; }
struct bigint{
static const int LEN=100001;
static const int ZR='0';
int a[LEN];
bigint() { a[0]=1,a[1]=0; }
void operator=(char*s) {
memset(a,0,sizeof(int)*a[0]);
a[0]=S(s);
for(int i=0;i<a[0];i++)
a[a[0]-i]=s[i]-'0';
}
void operator=(int s) {
char t[LEN];
sprintf(t,"%d",s);
*this=t;
}
bigint(int n) { *this=n; }
bigint(char *s) { *this=s; }
void in() {
char s[LEN];
scanf("%s",s);
a[0]=S(s);
for(int i=0;i<a[0];i++)
a[a[0]-i]=s[i]-'0';
}
void out() {
for(int i=a[0];i>=1;i--)
printf("%d",a[i]);
}
bigint operator+(const bigint&s) {
bigint ans;
ans.a[0]=mx(a[0],s.a[0]);
int x=0;
for(int i=1;i<=ans.a[0];i++) {
ans.a[i]=a[i]+s.a[i]+x;
x=ans.a[i]/10;
ans.a[i]%=10;
}
while(ans.a[ans.a[0]+1]!=0)ans.a[0]++;
return ans;
}
};
bigint a,b,c;
int main() {
a.in();
b.in();
c=a+b;
c.out();
}
压位高精 A+B
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int S(const char *a) { return strlen(a); }
int mx(int a,int b) { return a>b?a:b; }
struct bigint{
static const int LEN=101;
int a[LEN];
bigint(){}
void operator=(char*s) {
memset(a,0,sizeof(a));
int len=S(s);
a[0]=len/4;
if(len%4) a[0]++;
for(int i=0,j;i<len&&(j=(len-i-1)/4+1);i++)
a[j]=a[j]*10+s[i]-'0';
}
void operator=(int s) {
char t[LEN];
sprintf(t,"%d",s);
*this=t;
}
bigint(int n) { *this=n; }
bigint(char *s) { *this=s; }
void in() {
char s[LEN];
scanf("%s",s);
*this=s;
}
void out() {
printf("%d",a[a[0]]);
for(int i=a[0]-1;i>=1;i--)
printf("%04d",a[i]);
}
bigint operator+(const bigint&s) {
bigint ans=0;
ans.a[0]=mx(a[0],s.a[0]);
for(int i=1;i<=ans.a[0];i++) {
ans.a[i]+=a[i]+s.a[i];
if(ans.a[i]>=10000)
ans.a[i]-=10000,ans.a[i+1]++;
}
while(ans.a[ans.a[0]+1]!=0)ans.a[0]++;
return ans;
}
};
bigint a,b,c;
int main() {
a.in();
b.in();
c=a+b;
c.out();
}
二分A+B
#include<cstdio>
inline int read()
{
int a=0; char f=1,c=getchar();
while(c<'0'||c>'9')
{
if(c=='-') f=-f;
if(c==-1) return -1;
c=getchar();
}
while(c>='0'&&c<='9') a=(a<<1)+(a<<3)+(c^48),c=getchar();
return a*f;
}
inline int write(int a)
{
if(a<0) a=(~a)+1;
if(a/10) write(a/10);
return putchar(a%10|48);
}
int a=read(),b=read(),l=-2e9,r=2e9;
int main()
{
while(l+1<r)
{
int mid=(l+r)>>1;
if(mid<a+b) l=mid;
else r=mid;
}
write(r);
}
或许很多人已经觉得二分A+B已经很神奇了,但是我们今天要学习一种新的做A+B的方法:树状数组(B.I.T).
树状数组简介(不喜欢啰嗦的请直接跳到这里)
树状数组(Binary Indexed Tree(B.I.T), Fenwick Tree)是一个查询和修改复杂度都为log(n)的数据结构。主要用于查询任意两位之间的所有元素之和,但是每次只能修改一个元素的值;经过简单修改可以在log(n)的复杂度下进行范围修改,但是这时只能查询其中一个元素的值(如果加入多个辅助数组则可以实现区间修改与区间查询)。
经过了如此一番看不懂的说明,或许你会直接绝望掉,But这东西贼重要,而且 这种东西竟然没有STL!!!气不气 QAQ
基础概念
假设数组$A_{1...n}$,那么查询$\sum_{j=1}^{i}A_j$的时间是$log$级别的,而且这是一个在线的数据结构,支持随时修改某个元素的值,复杂度也为$log$级别。
来观察这个图:
令这棵树的结点编号为$C_1,C_2,C_3......C_n$。令每个结点的值为这棵树的值的总和,那么容易发现:
C1 = A1
C2 = A1 + A2
C3 = A3
C4 = A1 + A2 + A3 + A4
C5 = A5
C6 = A5 + A6
C7 = A7
C8 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8
这里有一个有趣的性质:
设节点编号为x,那么这个节点管辖的区间为$2^k$(其中k为x二进制末尾0的个数)个元素。
这样说是不是就要明确一些了?
根据这个性质,就有一个千古大罪人发明了树状数组
代码实现
大体结构
struct B_I_T{
int C[100001];
int n;
......//函数
};
lowbit
时间复杂度
$O(1)$
实现
还记得上面说的那一个性质吗?借助C++强大的位运算,我们可以在O(1)时间内求出$2^k$
int lowbit(int x) { return x - ( x & (x - 1) ); }
还有一种更简单也更常用的方式,是这样的
int lowbit(int x) { return x & -x; }
struct B_I_T{
int C[100001];
int n;
inline int lowbit(int x) { return x & -x; }
......//函数
};
就可以在结构体中加入这样一个函数了.
不过,更常用的方法不是写在结构体里面,而是写在外面,否则不在结构体当中就只能用B_I_T::lowbit(x)
了,即
inline int lowbit(int x) { return x & -x; }
struct B_I_T{
int C[100001];
int n;
......//函数
};
其实还有一种实现方法,用宏定义:#define lowbit(x) (x & -x)
lowbit的作用
update
时间复杂度
$O(log n)$
意义
$update(x,val)$ => $A_x=A_x+val$
实现
void update(int x,int val) {
for(int i=x;i<=n;i+=lowbit(x)) C[x]+=val;
}
这种使用for循环的做法,和下面使用while循环的原理是一样的.
void update(int x,int val) {
while(x<=n) {
C[x]+=val;
x+=lowbit(x);
}
}
假设n=8
,执行update(3,5)
,则有如下流程
x=3 C[3]+=5 x=3+lowbit(3)=4
x=4 C[4]+=5 x=4+lowbit(4)=8
x=8 C[8]+=5 x=8+lowbit(8)=16
x=16 退出
对照上文的图片 我也不知道有多上文 我们可以知道,每一次C数组中执行加操作的下标,刚好都包括了$A_3$!
作用
作用1——初始化
inline void init() {
scanf("%d",&n);
for(reg int i=1;i<=n;i++) {
scanf("%d",&a);
update(i,a);
}
}
作用2——改变单点的值 话说就是拿来干这件事的就不讲了
query
时间复杂度
$O(log n)$
意义
$query(x)$ => $A_1+A_2+……+A_x$
实现
int query(int x) {
int res=0;
for(reg int i=x;i;i-=lowbit(x)) res+=C[i];
return res;
}
这种使用for循环的做法,和下面使用while循环的原理是一样的.
int query(int x) {
int res=0;
while(x) {
res+=C[x];
x-=lowbit(x);
}
return res;
}
假设n=8
,执行query(7)
,则有如下流程
res=0
x=7 res=C[7]
x=6 res=C[6]+C[7]
x=4 res=C[4]+C[6]+C[7]
x=0 退出
仍然对照上(?)表,可以知道$C_4=A_1+A_2+A_3+A_4$,$C_6=A_5+A_6$,$C_7=A_7$。所以$C_4+C_6+C_7=\sum^7_{i=1}A_i$!
作用
作用1——求单点前缀和 不多赘述,直接$query(x)$即可
作用2——求区间和 $Sum(l,r)=query(r)-query(l-1)$,即r的前缀和减去l-1的前缀和,即为l->r的区间和
总结+代码
将以上的所有总结在一起,可以有如下代码
struct B_I_T{
int n;
int C[MAXN];
inline int lowbit(int x) {
return x & -x;
}
inline void update(int x,int val) {
for(register int i=x;i<=n;i+=lowbit(i))
C[i]+=val;
}
inline int query(int x) {
int s=0;
for(register int i=x;i;i-=lowbit(i))
s+=C[i];
return s;
}
inline void init() {
for(register int i=1;i<=n;i++) {
int a;
scanf("%d",&a);
update(i,a);
}
}
};
如此,我们就可以愉快地完成开头的问题A+B problem了!
逆序对
话说这不一道归并的题吗?拿到B.I.T.这里来干哈的?
先无脑打上mergesort的代码一份
int n,a[500001];
int s[500001];
long long ans;
void mrg(int left,int right,int mid)
{
int i=left,j=mid+1,k=left;
while(i<=mid&&j<=right)
{
if(a[i]>a[j])
{
ans+=mid-i+1;
s[k++]=a[j++];
}
else s[k++]=a[i++];
}
while(i<=mid)
s[k++]=a[i++];
while(j<=right)
s[k++]=a[j++];
for(int i=left;i<=right;i++)
a[i]=s[i];
}
void mergesort(int left,int right)
{
if(left==right)return;
int mid=(left+right)/2;
mergesort(left,mid);
mergesort(mid+1,right);
mrg(left,right,mid);
}
离散化
喂喂喂,这又是什么鬼?又关这道题什么事?不要急,先听我说。
离散化是程序设计中一个常用的技巧,它可以有效的降低时间复杂度。
有些数据本身很大,自身无法作为数组的下标保存对应的属性。如果这时只是需要这堆数据的相对属性,那么可以对其进行离散化处理。当数据只与它们之间的相对大小有关,而与具体是多少无关时,可以进行离散化。比如当你数据个数n很小,数据范围却很大时(超过1e9)就考虑离散化更小的值,能够实现更多的算法。
Set an example:
方式
离散化常见的两种方式: 1、数组离散化 2、用STL+二分离散化
数组法
for(register int i=1;i<=n;i++)
{
cin>>a[i].val;
a[i].id = i;
}
sort(a+1,a+n+1);//定义结构体时按val从小到大重载
for(int i=1;i<=n;i++)
b[a[i].id]=i;//将a[i]数组映射成更小的值,b[i]就是a[i]对应的rank(顺序)值
STL+二分
//n原数组大小 num原数组中的元素 lsh离散化的数组 cnt离散化后的数组大小
int lsh[MAXN],cnt,num[MAXN],n;
for(int i=1;i<=n;i++)
{
scanf("%d",&num[i]);
lsh[i]=num[i];//复制一份原数组
}
sort(lsh+1,lsh+n+1);//排序,unique虽有排序功能,但交叉数据排序不支持,所以先排序防止交叉数据
//cnt就是排序去重之后的长度
cnt=unique(lsh+1,lsh+n+1)-lsh-1;//unique返回去重之后最后一位后一位地址-数组首地址-1
for(int i=1;i<=n;i++)
num[i]=lower_bound(lsh+1,lsh+cnt+1,num[i])-lsh;
//lower_bound返回二分查找在去重排序数组中第一个等于或大于num[i]的值的地址-数组首地址,从而实现离散化
实现
逆序对实际上就是统计当前元素的前面有几个比它大的元素的个数,然后把所有元素比它大的元素总数垒加就是逆序对总数。
代码
#include<map>
#include<set>
#include<list>
#include<queue>
#include<deque>
#include<stack>
#include<ctime>
#include<cmath>
#include<vector>
#include<bitset>
#include<cstdio>
#include<cctype>
#include<string>
#include<cstdlib>
#include<cstring>
#include<climits>
#include<iomanip>
#include<iostream>
#include<algorithm>
using namespace std;
#define reg register
template <typename T>
inline T read() {
T a=0; char c=getchar(),f=1;
while(c<'0'||c>'9') {
if(c=='-') f=-f;
if(c==-1) return c;
c=getchar();
}
while(c>='0'&&c<='9') a=(a<<1)+(a<<3)+(c^48),c=getchar();
return a*f;
}
template <class T>
inline int write(T x) {
if(x<0) x=(~x)+1, putchar('-');
if(x/10) write(x/10);
return putchar(x%10|48);
}
template <class T>
inline int write(T x,char c) {
return write(x)&&putchar(c);
}
template <class T>
inline T Max(T a,T b) { return a>b?a:b; }
template <class T>
inline T Min(T a,T b) { return a<b?a:b; }
template <class T>
inline T Abs(T a) { return a<0?-a:a; }
int n=read<int>();
int C[500001];
long long ans;
int a[500001];
int rnk[500001];
inline int lowbit(int x) {
return x & -x;
}
inline int query(int x) {
int s=0;
while(x>0) {
s+=C[x];
x-=lowbit(x);
}
return s;
}
inline void update(int x,int val) {
while(x<=n) {
C[x]+=val;
x+=lowbit(x);
}
}
int main() {
for(reg int i=1;i<=n;i++) {
a[i]=rnk[i]=read<int>();
}
sort(a+1,a+n+1);
int cnt=unique(a+1,a+n+1)-a-1;
for(reg int i=1;i<=n;i++) {
rnk[i]=lower_bound(a+1,a+cnt+1,rnk[i])-a;
}
for(reg int i=1;i<=n;i++) {
update(rnk[i],1);
ans+=i-query(rnk[i]);
}
write(ans);
}
树状数组进阶
差分
如果你像我一样是个小白,那你也许看不懂这是什么东西。
好吧,先上万能的百度百科
差分,又名差分函数或差分运算,差分的结果反映了离散量之间的一种变化,是研究离散数学的一种工具,常用函数差近似导数。
有没有仍然看不懂?
那就直接来看一下差分的BIT吧
区间修改+单点查询
看起来有点正经的内容了
主要思想
令i~j的区间和为$a_i-a_{j-1}(i>j)$,于是前缀和就为$a_i-a_0$。如果如此,那输入的时候就可以处理为$update(i,a_i-a_{i-1})$。
单点查询
想一想,如果这样,$A_i=a_i-a_{i-1},A_1+A_2+...+A_i=a_i-a_0$,就可以直接这样处理了。至于单点查询,可以很容易地想到,对于一个点i
而言,因为a[0]=0
,所以query(i)=a[i]
区间修改
直接上代码
inline void update(int x,int delta) {
......//同上
}
inline void update(int l,int r,int delta) {
update(l,delta); update(r+1,-delta);
}
想一想,如果$n=6$,$[2-4]$这个区间加上4,那么如下
id 1 2 3 4 5 6
A 0 4 4 4 0 0
C 0 4 0 0 -4 0
震惊! 只有$C_2(+4)$和$C_5(-4)$改变了!
对于$C_3$的分析
C[3]=A[3]-A[2]
C'[3]=A'[3]-A'[2]=(A[3]+4)-(A[2]+4)=A[3]-A[2]=C[3]
所以,按这样推来,只需要改动$C_l$和$C_r$即可。
区间修改+区间查询
差分分析
我们同样引入上面的差分C数组。
$∵C_i=A_i-A_{i-1}$
$∴A_i=C_1+C_2+C_3+......+C_i$
那么可以得到这一坨
$????A_1+A_2+...+A_i$
$=C_1+(C_1+C_2)+...+(C_1+C_2+...+C_i)$
$=iC_1+(i-1)C_2+...+C_i$
$=i(C_i+C_2+...+C_i)-1C_2-...-(i-1)C_i$
所以,我们就可以再用一个毒瘤的差分数组$C1_i$来储存$(i-1)C_i$
接上面的公式
$=i*(C_1+C_2+C_3+......+C_i)-(C1_1+C1_2+C1_3+......+C1_i)$
代码
template <typename T>
inline T read() {
T a=0; char c=getchar(),f=1;
while(c<'0'||c>'9') {
if(c=='-') f=-f;
if(c==-1) return c;
c=getchar();
}
while(c>='0'&&c<='9') a=(a<<1)+(a<<3)+(c^48),c=getchar();
return a*f;
}
typedef long long LL;
#define lowbit(x) (x & -x)
struct BIT{
int n;
LL C1[MAXN],C2[MAXN];
inline BIT(){}
inline void update(LL *C,int x,LL val) {
while(x<=n) {
C[x]+=val;
x+=lowbit(x);
}
}
inline void update(int l,int r,LL val) {
update(C1,l,val); update(C1,r+1,-val);
update(C2,l,val*(l-1)); update(C2,r+1,-val*r);
}
inline LL query(LL *C,int x) {
LL s=0;
while(x) {
s+=C[x];
x-=lowbit(x);
}
return s;
}
inline LL query(int l,int r) {
return (l-1)*query(C1,l-1)-query(C2,l-1)-(r*query(C1,r)-query(C2,r));
}
inline void init(int N=0) {
if(N) n=N;
else n=read<int>();
LL bef=0;
for(reg int i=1;i<=n;i++) {
LL num=read<LL>();
update(C1,i,num-bef);
update(C2,i,(num-bef)*(i-1));
bef=num;
}
}
};
2D树状数组
怎么会有这么毒瘤的题……
int n,m;
struct BIT2D{
int C[1001][1001];
......//函数
};
query
分析——放弃
代码——
inline int query(int x,int y) {
int res=0;
for(register int i=x;i;i-=lowbit(i)) {
for(register int j=y;j;j-=lowbit(j)) {
res+=C[i][j];
}
}
return res;
}
update
分析——同上
代码——
inline void update(int x,int y,int delta) {
for(register int i=x;i<=n;i+=lowbit(i)) {
for(register int j=y;j<=n;j+=lowbit(j)) {
C[i][j]+=delta;
}
}
}
区间修改+单点查询
一句话总结:$C_{i,j}=A_{i,j}-A_{i-1,j}-A_{i,j-1}+A_{i-1,j-1}$
区间修改+区间查询
同样是一句话:维护$C_{i,j}$和$iC_{i,j}$和$jC_{i,j}$和$ijC_{i,j}$
时间复杂度
每一次操作为$O(log^2)$
当然了,更高维的树状数组也可以此类推,每一次操作的时间复杂度为$O(log^k)$(k是树状数组的维度)
罗列例题
一维
- A+B problem(好滑稽啊)
- 统计和(比板题还板题的板题)
- Stars(大板题)
- 【模板】树状数组 1
- 逆序对
- 【模板】树状数组 2(推荐用 区间修改+单点查询 和 区间修改+区间查询 各做一遍)
- 上帝造题的七分钟2(很灵活,注意使用并查集优化常数...附上题解)
多维
- Mobile phones(简直就是二维的板题)
- Matrix
- 上帝造题的七分钟
- Cube(三维虚不虚)
完结撒花(图)!!!!!!!!!
---恢复内容结束---
@TOC
膜拜第
个来访本蒟蒻博客的大佬
beginning
让我们先从这道题开始:P1001 A+B Problem
说明: 我不是在搞笑 !
我们可以探讨一下,用我们所学的知识可以如何解决这道题.
顺序结构 A+B
#include<cstdio>
int a,b;
int main()
{
scanf("%d%d",&a,&b);
printf("%d",a+b);
}
明显没有任何毛病…… 傻子才会说有毛病
高精 A+B
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int S(const char *a) { return strlen(a); }
int mx(int a,int b) { return a>b?a:b; }
struct bigint{
static const int LEN=100001;
static const int ZR='0';
int a[LEN];
bigint() { a[0]=1,a[1]=0; }
void operator=(char*s) {
memset(a,0,sizeof(int)*a[0]);
a[0]=S(s);
for(int i=0;i<a[0];i++)
a[a[0]-i]=s[i]-'0';
}
void operator=(int s) {
char t[LEN];
sprintf(t,"%d",s);
*this=t;
}
bigint(int n) { *this=n; }
bigint(char *s) { *this=s; }
void in() {
char s[LEN];
scanf("%s",s);
a[0]=S(s);
for(int i=0;i<a[0];i++)
a[a[0]-i]=s[i]-'0';
}
void out() {
for(int i=a[0];i>=1;i--)
printf("%d",a[i]);
}
bigint operator+(const bigint&s) {
bigint ans;
ans.a[0]=mx(a[0],s.a[0]);
int x=0;
for(int i=1;i<=ans.a[0];i++) {
ans.a[i]=a[i]+s.a[i]+x;
x=ans.a[i]/10;
ans.a[i]%=10;
}
while(ans.a[ans.a[0]+1]!=0)ans.a[0]++;
return ans;
}
};
bigint a,b,c;
int main() {
a.in();
b.in();
c=a+b;
c.out();
}
压位高精 A+B
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int S(const char *a) { return strlen(a); }
int mx(int a,int b) { return a>b?a:b; }
struct bigint{
static const int LEN=101;
int a[LEN];
bigint(){}
void operator=(char*s) {
memset(a,0,sizeof(a));
int len=S(s);
a[0]=len/4;
if(len%4) a[0]++;
for(int i=0,j;i<len&&(j=(len-i-1)/4+1);i++)
a[j]=a[j]*10+s[i]-'0';
}
void operator=(int s) {
char t[LEN];
sprintf(t,"%d",s);
*this=t;
}
bigint(int n) { *this=n; }
bigint(char *s) { *this=s; }
void in() {
char s[LEN];
scanf("%s",s);
*this=s;
}
void out() {
printf("%d",a[a[0]]);
for(int i=a[0]-1;i>=1;i--)
printf("%04d",a[i]);
}
bigint operator+(const bigint&s) {
bigint ans=0;
ans.a[0]=mx(a[0],s.a[0]);
for(int i=1;i<=ans.a[0];i++) {
ans.a[i]+=a[i]+s.a[i];
if(ans.a[i]>=10000)
ans.a[i]-=10000,ans.a[i+1]++;
}
while(ans.a[ans.a[0]+1]!=0)ans.a[0]++;
return ans;
}
};
bigint a,b,c;
int main() {
a.in();
b.in();
c=a+b;
c.out();
}
二分A+B
#include<cstdio>
inline int read()
{
int a=0; char f=1,c=getchar();
while(c<'0'||c>'9')
{
if(c=='-') f=-f;
if(c==-1) return -1;
c=getchar();
}
while(c>='0'&&c<='9') a=(a<<1)+(a<<3)+(c^48),c=getchar();
return a*f;
}
inline int write(int a)
{
if(a<0) a=(~a)+1;
if(a/10) write(a/10);
return putchar(a%10|48);
}
int a=read(),b=read(),l=-2e9,r=2e9;
int main()
{
while(l+1<r)
{
int mid=(l+r)>>1;
if(mid<a+b) l=mid;
else r=mid;
}
write(r);
}
或许很多人已经觉得二分A+B已经很神奇了,但是我们今天要学习一种新的做A+B的方法:树状数组(B.I.T).
树状数组简介(不喜欢啰嗦的请直接跳到这里)
树状数组(Binary Indexed Tree(B.I.T), Fenwick Tree)是一个查询和修改复杂度都为log(n)的数据结构。主要用于查询任意两位之间的所有元素之和,但是每次只能修改一个元素的值;经过简单修改可以在log(n)的复杂度下进行范围修改,但是这时只能查询其中一个元素的值(如果加入多个辅助数组则可以实现区间修改与区间查询)。
经过了如此一番看不懂的说明,或许你会直接绝望掉,But这东西贼重要,而且 这种东西竟然没有STL!!!气不气 QAQ
基础概念
假设数组$A_{1...n}$,那么查询$\sum_{j=1}^{i}A_j$的时间是$log$级别的,而且这是一个在线的数据结构,支持随时修改某个元素的值,复杂度也为$log$级别。
来观察这个图:
令这棵树的结点编号为$C_1,C_2,C_3......C_n$。令每个结点的值为这棵树的值的总和,那么容易发现:
C1 = A1
C2 = A1 + A2
C3 = A3
C4 = A1 + A2 + A3 + A4
C5 = A5
C6 = A5 + A6
C7 = A7
C8 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8
这里有一个有趣的性质:
设节点编号为x,那么这个节点管辖的区间为$2^k$(其中k为x二进制末尾0的个数)个元素。
这样说是不是就要明确一些了?
根据这个性质,就有一个千古大罪人发明了树状数组
代码实现
大体结构
struct B_I_T{
int C[100001];
int n;
......//函数
};
lowbit
时间复杂度
$O(1)$
实现
还记得上面说的那一个性质吗?借助C++强大的位运算,我们可以在O(1)时间内求出$2^k$
int lowbit(int x) { return x - ( x & (x - 1) ); }
还有一种更简单也更常用的方式,是这样的
int lowbit(int x) { return x & -x; }
struct B_I_T{
int C[100001];
int n;
inline int lowbit(int x) { return x & -x; }
......//函数
};
就可以在结构体中加入这样一个函数了.
不过,更常用的方法不是写在结构体里面,而是写在外面,否则不在结构体当中就只能用B_I_T::lowbit(x)
了,即
inline int lowbit(int x) { return x & -x; }
struct B_I_T{
int C[100001];
int n;
......//函数
};
其实还有一种实现方法,用宏定义:#define lowbit(x) (x & -x)
lowbit的作用
update
时间复杂度
$O(log n)$
意义
$update(x,val)$ => $A_x=A_x+val$
实现
void update(int x,int val) {
for(int i=x;i<=n;i+=lowbit(x)) C[x]+=val;
}
这种使用for循环的做法,和下面使用while循环的原理是一样的.
void update(int x,int val) {
while(x<=n) {
C[x]+=val;
x+=lowbit(x);
}
}
假设n=8
,执行update(3,5)
,则有如下流程
x=3 C[3]+=5 x=3+lowbit(3)=4
x=4 C[4]+=5 x=4+lowbit(4)=8
x=8 C[8]+=5 x=8+lowbit(8)=16
x=16 退出
对照上文的图片 我也不知道有多上文 我们可以知道,每一次C数组中执行加操作的下标,刚好都包括了$A_3$!
作用
作用1——初始化
inline void init() {
scanf("%d",&n);
for(reg int i=1;i<=n;i++) {
scanf("%d",&a);
update(i,a);
}
}
作用2——改变单点的值 话说就是拿来干这件事的就不讲了
query
时间复杂度
$O(log n)$
意义
$query(x)$ => $A_1+A_2+……+A_x$
实现
int query(int x) {
int res=0;
for(reg int i=x;i;i-=lowbit(x)) res+=C[i];
return res;
}
这种使用for循环的做法,和下面使用while循环的原理是一样的.
int query(int x) {
int res=0;
while(x) {
res+=C[x];
x-=lowbit(x);
}
return res;
}
假设n=8
,执行query(7)
,则有如下流程
res=0
x=7 res=C[7]
x=6 res=C[6]+C[7]
x=4 res=C[4]+C[6]+C[7]
x=0 退出
仍然对照上(?)表,可以知道$C_4=A_1+A_2+A_3+A_4$,$C_6=A_5+A_6$,$C_7=A_7$。所以$C_4+C_6+C_7=\sum^7_{i=1}A_i$!
作用
作用1——求单点前缀和 不多赘述,直接$query(x)$即可
作用2——求区间和 $Sum(l,r)=query(r)-query(l-1)$,即r的前缀和减去l-1的前缀和,即为l->r的区间和
总结+代码
将以上的所有总结在一起,可以有如下代码
struct B_I_T{
int n;
int C[MAXN];
inline int lowbit(int x) {
return x & -x;
}
inline void update(int x,int val) {
for(register int i=x;i<=n;i+=lowbit(i))
C[i]+=val;
}
inline int query(int x) {
int s=0;
for(register int i=x;i;i-=lowbit(i))
s+=C[i];
return s;
}
inline void init() {
for(register int i=1;i<=n;i++) {
int a;
scanf("%d",&a);
update(i,a);
}
}
};
如此,我们就可以愉快地完成开头的问题A+B problem了!
逆序对
话说这不一道归并的题吗?拿到B.I.T.这里来干哈的?
先无脑打上mergesort的代码一份
int n,a[500001];
int s[500001];
long long ans;
void mrg(int left,int right,int mid)
{
int i=left,j=mid+1,k=left;
while(i<=mid&&j<=right)
{
if(a[i]>a[j])
{
ans+=mid-i+1;
s[k++]=a[j++];
}
else s[k++]=a[i++];
}
while(i<=mid)
s[k++]=a[i++];
while(j<=right)
s[k++]=a[j++];
for(int i=left;i<=right;i++)
a[i]=s[i];
}
void mergesort(int left,int right)
{
if(left==right)return;
int mid=(left+right)/2;
mergesort(left,mid);
mergesort(mid+1,right);
mrg(left,right,mid);
}
离散化
喂喂喂,这又是什么鬼?又关这道题什么事?不要急,先听我说。
离散化是程序设计中一个常用的技巧,它可以有效的降低时间复杂度。
有些数据本身很大,自身无法作为数组的下标保存对应的属性。如果这时只是需要这堆数据的相对属性,那么可以对其进行离散化处理。当数据只与它们之间的相对大小有关,而与具体是多少无关时,可以进行离散化。比如当你数据个数n很小,数据范围却很大时(超过1e9)就考虑离散化更小的值,能够实现更多的算法。
Set an example:
方式
离散化常见的两种方式: 1、数组离散化 2、用STL+二分离散化
数组法
for(register int i=1;i<=n;i++)
{
cin>>a[i].val;
a[i].id = i;
}
sort(a+1,a+n+1);//定义结构体时按val从小到大重载
for(int i=1;i<=n;i++)
b[a[i].id]=i;//将a[i]数组映射成更小的值,b[i]就是a[i]对应的rank(顺序)值
STL+二分
//n原数组大小 num原数组中的元素 lsh离散化的数组 cnt离散化后的数组大小
int lsh[MAXN],cnt,num[MAXN],n;
for(int i=1;i<=n;i++)
{
scanf("%d",&num[i]);
lsh[i]=num[i];//复制一份原数组
}
sort(lsh+1,lsh+n+1);//排序,unique虽有排序功能,但交叉数据排序不支持,所以先排序防止交叉数据
//cnt就是排序去重之后的长度
cnt=unique(lsh+1,lsh+n+1)-lsh-1;//unique返回去重之后最后一位后一位地址-数组首地址-1
for(int i=1;i<=n;i++)
num[i]=lower_bound(lsh+1,lsh+cnt+1,num[i])-lsh;
//lower_bound返回二分查找在去重排序数组中第一个等于或大于num[i]的值的地址-数组首地址,从而实现离散化
实现
逆序对实际上就是统计当前元素的前面有几个比它大的元素的个数,然后把所有元素比它大的元素总数垒加就是逆序对总数。
代码
#include<map>
#include<set>
#include<list>
#include<queue>
#include<deque>
#include<stack>
#include<ctime>
#include<cmath>
#include<vector>
#include<bitset>
#include<cstdio>
#include<cctype>
#include<string>
#include<cstdlib>
#include<cstring>
#include<climits>
#include<iomanip>
#include<iostream>
#include<algorithm>
using namespace std;
#define reg register
template <typename T>
inline T read() {
T a=0; char c=getchar(),f=1;
while(c<'0'||c>'9') {
if(c=='-') f=-f;
if(c==-1) return c;
c=getchar();
}
while(c>='0'&&c<='9') a=(a<<1)+(a<<3)+(c^48),c=getchar();
return a*f;
}
template <class T>
inline int write(T x) {
if(x<0) x=(~x)+1, putchar('-');
if(x/10) write(x/10);
return putchar(x%10|48);
}
template <class T>
inline int write(T x,char c) {
return write(x)&&putchar(c);
}
template <class T>
inline T Max(T a,T b) { return a>b?a:b; }
template <class T>
inline T Min(T a,T b) { return a<b?a:b; }
template <class T>
inline T Abs(T a) { return a<0?-a:a; }
int n=read<int>();
int C[500001];
long long ans;
int a[500001];
int rnk[500001];
inline int lowbit(int x) {
return x & -x;
}
inline int query(int x) {
int s=0;
while(x>0) {
s+=C[x];
x-=lowbit(x);
}
return s;
}
inline void update(int x,int val) {
while(x<=n) {
C[x]+=val;
x+=lowbit(x);
}
}
int main() {
for(reg int i=1;i<=n;i++) {
a[i]=rnk[i]=read<int>();
}
sort(a+1,a+n+1);
int cnt=unique(a+1,a+n+1)-a-1;
for(reg int i=1;i<=n;i++) {
rnk[i]=lower_bound(a+1,a+cnt+1,rnk[i])-a;
}
for(reg int i=1;i<=n;i++) {
update(rnk[i],1);
ans+=i-query(rnk[i]);
}
write(ans);
}
树状数组进阶
差分
如果你像我一样是个小白,那你也许看不懂这是什么东西。
好吧,先上万能的百度百科
差分,又名差分函数或差分运算,差分的结果反映了离散量之间的一种变化,是研究离散数学的一种工具,常用函数差近似导数。
有没有仍然看不懂?
那就直接来看一下差分的BIT吧
区间修改+单点查询
看起来有点正经的内容了
主要思想
令i~j的区间和为$a_i-a_{j-1}(i>j)$,于是前缀和就为$a_i-a_0$。如果如此,那输入的时候就可以处理为$update(i,a_i-a_{i-1})$。
单点查询
想一想,如果这样,$A_i=a_i-a_{i-1},A_1+A_2+...+A_i=a_i-a_0$,就可以直接这样处理了。至于单点查询,可以很容易地想到,对于一个点i
而言,因为a[0]=0
,所以query(i)=a[i]
区间修改
直接上代码
inline void update(int x,int delta) {
......//同上
}
inline void update(int l,int r,int delta) {
update(l,delta); update(r+1,-delta);
}
想一想,如果$n=6$,$[2-4]$这个区间加上4,那么如下
id 1 2 3 4 5 6
A 0 4 4 4 0 0
C 0 4 0 0 -4 0
震惊! 只有$C_2(+4)$和$C_5(-4)$改变了!
对于$C_3$的分析
C[3]=A[3]-A[2]
C'[3]=A'[3]-A'[2]=(A[3]+4)-(A[2]+4)=A[3]-A[2]=C[3]
所以,按这样推来,只需要改动$C_l$和$C_r$即可。
区间修改+区间查询
差分分析
我们同样引入上面的差分C数组。
$∵C_i=A_i-A_{i-1}$
$∴A_i=C_1+C_2+C_3+......+C_i$
那么可以得到这一坨
$????A_1+A_2+...+A_i$
$=C_1+(C_1+C_2)+...+(C_1+C_2+...+C_i)$
$=iC_1+(i-1)C_2+...+C_i$
$=i(C_i+C_2+...+C_i)-1C_2-...-(i-1)C_i$
所以,我们就可以再用一个毒瘤的差分数组$C1_i$来储存$(i-1)C_i$
接上面的公式
$=i*(C_1+C_2+C_3+......+C_i)-(C1_1+C1_2+C1_3+......+C1_i)$
代码
template <typename T>
inline T read() {
T a=0; char c=getchar(),f=1;
while(c<'0'||c>'9') {
if(c=='-') f=-f;
if(c==-1) return c;
c=getchar();
}
while(c>='0'&&c<='9') a=(a<<1)+(a<<3)+(c^48),c=getchar();
return a*f;
}
typedef long long LL;
#define lowbit(x) (x & -x)
struct BIT{
int n;
LL C1[MAXN],C2[MAXN];
inline BIT(){}
inline void update(LL *C,int x,LL val) {
while(x<=n) {
C[x]+=val;
x+=lowbit(x);
}
}
inline void update(int l,int r,LL val) {
update(C1,l,val); update(C1,r+1,-val);
update(C2,l,val*(l-1)); update(C2,r+1,-val*r);
}
inline LL query(LL *C,int x) {
LL s=0;
while(x) {
s+=C[x];
x-=lowbit(x);
}
return s;
}
inline LL query(int l,int r) {
return (l-1)*query(C1,l-1)-query(C2,l-1)-(r*query(C1,r)-query(C2,r));
}
inline void init(int N=0) {
if(N) n=N;
else n=read<int>();
LL bef=0;
for(reg int i=1;i<=n;i++) {
LL num=read<LL>();
update(C1,i,num-bef);
update(C2,i,(num-bef)*(i-1));
bef=num;
}
}
};
2D树状数组
怎么会有这么毒瘤的题……
int n,m;
struct BIT2D{
int C[1001][1001];
......//函数
};
query
分析——放弃
代码——
inline int query(int x,int y) {
int res=0;
for(register int i=x;i;i-=lowbit(i)) {
for(register int j=y;j;j-=lowbit(j)) {
res+=C[i][j];
}
}
return res;
}
update
分析——同上
代码——
inline void update(int x,int y,int delta) {
for(register int i=x;i<=n;i+=lowbit(i)) {
for(register int j=y;j<=n;j+=lowbit(j)) {
C[i][j]+=delta;
}
}
}
区间修改+单点查询
一句话总结:$C_{i,j}=A_{i,j}-A_{i-1,j}-A_{i,j-1}+A_{i-1,j-1}$
区间修改+区间查询
同样是一句话:维护$C_{i,j}$和$iC_{i,j}$和$jC_{i,j}$和$ijC_{i,j}$
时间复杂度
每一次操作为$O(log^2)$
当然了,更高维的树状数组也可以此类推,每一次操作的时间复杂度为$O(log^k)$(k是树状数组的维度)
罗列例题
一维
- A+B problem(好滑稽啊)
- 统计和(比板题还板题的板题)
- Stars(大板题)
- 【模板】树状数组 1
- 逆序对
- 【模板】树状数组 2(推荐用 区间修改+单点查询 和 区间修改+区间查询 各做一遍)
- 上帝造题的七分钟2(很灵活,注意使用并查集优化常数...附上题解)
多维
- Mobile phones(简直就是二维的板题)
- Matrix
- 上帝造题的七分钟
- Cube(三维虚不虚)
完结撒花(图)!!!!!!!!!
原文地址:https://www.cnblogs.com/PI-UKE/p/10490794.html