树状数组
树状数组作为一种实现简单、应用较广的高级数据结构,在OI界的地位越来越重要,下面我来简单介绍一下树状数组和它的简单应用。
一、树状数组简介
树状数组:顾名思义,是一种数组,其中包含了树的思想。它是用来处理动态更新、动态统计区间问题的一种良好的数据结构,查询和修改复杂度都为O(
logn)
的数据结构。
【引例】统计和
给定一个长度为n(n<=10^5),初始时都为0的序列A,有m(m<=10^5)次操作,每次可以进行如下两类操作:
(1).Add i d:表示将序列第i个数加上d;
(2).Ask s t:询问序列s到t的所有数的和;
•问如何设计算法,使得修改和询问操作的时间复杂度尽量低?
【题目分析】
■如果直接用数组模拟,那么修改是O(1),查询是O(n),总的时间复杂度为O(nm);
■如果维护另一个数组b[i],表示a[1]到a[i]的和,那么修改是O(n),查询是O(1),总的时间复杂度为O(nm);
■一个程序的时间复杂度取决于其中最大的时间复杂度。
■树状数组的目的就是平摊修改和查询的时间,使复杂度由O(n)降到O(logn)。
树状数组的原理是增加一个辅助序列C数组,令C
[i]=a[i-2k+1]+a[i-2k+2]+…+a[i]
,其中
k
为
i
在二进制形式下末尾
0
的个数。
由C数组的定义可以得出以下这张表格:
i
|
二进制
|
K
|
|
1
|
(1)2
|
0
|
c[1]=a[1]
|
2
|
(10) 2
|
1
|
c[2]=a[1]+a[2]=c[1]+a[2]
|
3
|
(11) 2
|
0
|
c[3]=a[3]
|
4
|
(100) 2
|
2
|
c[4]=a[1]+a[2]+a[3]+a[4]=c[2]+c[3]+a[4]
|
5
|
(101) 2
|
0
|
c[5]=a[5]
|
6
|
(110) 2
|
1
|
c[6]=a[5]+a[6]=c[5]+a[6]
|
7
|
(111) 2
|
0
|
c[7]=a[7]
|
8
|
(1000) 2
|
3
|
c[8]=a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]+a[8]
=c[4]+c[6]+c[7]+a[8]
|
……
|
假如a数组有8个元素,可以得到C数组的形状,如下图:
由
C数组的结构对应一棵树,因此将它称之为树状数组。仔细观察上图,由树根至a[3]的路径可以看出,更新a[3]仅更新C[3]、C[4]、C[8]有关;由a[4]所在的子树变可以看出,a[4]=C[4]-C[2]-C[3];即使计算a[1]+a[2]+
…
+a[7],也只需计算 C[4]+C[6]+C[7]。可见
修改与统计和时,只需对C数组进行相应的操作即可。
二、树状数组的基本操作
【操作1】求
LOWBIT
(i)=2^k
由C数组的定义可知,每个元素C
[i]=a[i-2k+1]+…+a[i]
,其中k为
i
在二进制形式下末尾
0
的个数,这里的关键是2^k如何计算?计算2^k有个快捷办法,采用异或运算,
有以下两种不同的方法:
①
.Lowbit(
i
)=
i&
(
i^
(
i
-1))
②
.Lowbit(
i
)=
i&(-i)
如:10101000 这是i
10100111 这是i-1
00001111 这是i^(i-1)
00001000 这是i&(i^(i-1))
int Lowbit(int t)
{ return t&(-t);//return t&(t ^ (t-1));
}
|
【操作2】修改操作:将A[i]的值加d
■
修改了某个a[i],就需改动所有包含a[i]的c[j];
■
从上图看就是要更改从该叶子节点a[i]到根节点路径上的所有c[j];
■
但是怎么求一个节点的父节点呢?
■
增加虚构点,变成满二叉树!
■
每个节点的父亲就跟其右兄弟的父亲一样了;
■
而左右兄弟的管辖区域是一样的;
■
所以i的父节点就是i+lowbit(i);
void Modify(int x,int d)
{ for(
int
i=
x
;i<=n;i+=lowbit(i))
c[i]+=
d
;
}
|
其中n为数组的长度,时间复杂度同样是O(logN);
【操作3】求和操作:
Sum(
N
)
■
根据c[i]的定义可知每个变量表示那段区间的和之后就可以很快的求出前缀和了;
■
要求sum[i]=a[1]+a[2]+.....+a[i];
■
C[i]=a[i-lowbit(i)+1]+...+a[i];
■
还需要求a[1]+a[2]+......+a[i-lowbit(i)];
■
于是,可以直接用一个循环求得sum,时间复杂度为O(logn);
int Sum(int x)
{ int
A
ns=0,i;
for(i=
x
;i>0;i-= lowbit(i))
A
ns+=c[i];
return
A
ns;
}
|
【思考】求区间
a[
i
]
+
…
+
a[
j
]
的和
可以先统计出a[1]+a[2]+…+a[i-1]的和,在统计a[1]+a[2]+…+a[j]的和,两者相减即可得到
a[
i
]
+
…
+
a[
j
]
的和。即=sum(j)-sum(i-1);
三、树状数组的扩展
1、二维树状数组m*n
–
动态求子阵和;
–
logm*logn的查询复杂度;
■
求解方法:设
C[x,y]
为
c[x][y]=∑a[i][j]
(其中
x-lowbit(x)+1<=i<=x,y-lowbit(y)+1<=j<=y
),其具体的修改和求和过程实际上是一维过程的嵌套。
在二维情况下,对应的更新和查询函数为:
int Lowbit(int
i
){return
i
&(-
i
);} //return
i
&(
i
^(
i
-1));
void
M
odify(int x,int y,int d)
{ for(int i=x;i<=n;i+=
L
owbit(i))
for(int j=y;j<=n;j+=
L
owbit(j))c[i][j]+=d;
}
int
S
um(int x,int y)
//
求以
(1,1)
,
(x,y)
分别为左上顶点,右下顶点的矩形区域内的和
{ int
A
ns=0;
for(int i=x;i>0;i-=
L
owerbit(i))
for(int j=y;j>0;j-=
L
owerbit(j))
A
ns+=c[i][j];
return
A
ns;
}
int
Ask
(int x1,int y1,int x2,int y2)
//
计算以
(x1,y1),(x2,y2)
分别为左上顶点,右下顶点的矩形区域内的和
{ return
S
um(x2,y2)-
S
um(x1-1,y2)-
S
um(x2,y1-1)+
S
um(x1-1,y1-1);}
|
2、三维树状数组m*n*L
–
动态求三维长方体体积;
–
logm*logn*logL的查询复杂度;
四、运用树状数组可以解决的几类例题
1、单点修改,区间查询
S
um(x)
返回原数组[1,x]的区间和,Modify
(x,w)
将原数组下标为x的数加上w。
这两个函数使用O(logn)的时间和O(n)的空间完成
单点加减,区间求和的功能
。
【例题1】序列和
2521
【问题描述】
给定一个初始值都为
0
的序列,动态地修改一些位置上的数字,加上一个数,减去一个数,然后动态地提出问题,问题的形式是求出一段数字的和。
规定:
Add i d
:表示将序列第
i
个数加上
d
;
Sub i d
:表示将序列中第
i
数减去
d
;
Ask i j
:询问序列
i
到
j
的所有数的和;
例如:
操作
|
回答
|
操作后的序列
|
Add 2 3
|
|
0 3 0 0 0 0 0 0 0 0
|
Sub 3 1
|
|
0 3 -1 0 0 0 0 0 0 0
|
Ask 3 7
|
-1
|
0 3 -1 0 0 0 0 0 0 0
|
Add 4 2
|
|
0 3 -1 2 0 0 0 0 0 0
|
Ask 3 6
|
1
1
|
0 3 -1 2 0 0 0 0 0 0
|
Sub 1 1
|
|
-1 3 -1 2 0 0 0 0 0 0
|
【输入格式】
第一行两个整数:
n m
,分别表示序列的长度和有
m
条指令;
第
2
行到第
m+1
行,都是上面所示的指令格式,没有多余的空格;
【输出格式】
每行一个整数,分别对指令序列中的
ask
做出的回答。
【输入输出样例】
seq.in
|
seq.out
|
10 6
Add 2 3
Sub 3 1
Ask 3 7
Add 4 2
Ask 3 6
Sub 1 1
|
-1
1
|
【数据范围】
动态加减的数
d
与序列中所有数据之和不会超过
Longint
范围,
1<=n<=100000
;
1<=m<=50000
;
#include
#include<
cstdio
>
using namespace std;
int a[10000
5
]={0},N,M;
int Lowbit(int x){return x&(-x);}
void
Modify
(int x,int
d
)
{ int i;
for(i=x;i<=N;i+=Lowbit(i)) a[i]+=
d
;
}
int
Sum
(int x)
{ int i,Ans=0;
for(i=x;i>0;i-=Lowbit(i)) Ans+=a[i];
return Ans;
}
int main()
{ int i,x,y;
string order;
scanf("%d%d",&N,&M);
for(i=1;i<=M;i++)
{ cin>>order;
scanf("%d%d",&x,&y);
if(order=="Add")
Modify
(x,y);
if(order=="Sub")
Modify
(x,-y);
if(order=="Ask")printf("%d\n",
Sum
(y)-
Sum
(x-1));
}
return 0;
}
【例题2】夜空星辰(
PKU 2352
)1329
【题目描述】
夜空中有N颗恒星(N
≤
100
000
),每颗恒星具有其坐标(x, y)(0
≤
x, y
≤
100
000
)。现在,天文学家要对这些恒星进行分类,分类的标准如下:对于任意一颗恒星S(x,y),如果存在k颗恒星,其x, y坐标均不大于S,则恒星S属于k类星。
如下图所示:第5颗恒星为3类星,这是由1、2、4三颗恒星均在其左下方而得出的,类似地第2、4两颗恒星为1类星,第3颗恒星为2类星。因此在这幅图中只有一颗0类星,共有二颗1类星,2类星和3类星各有一颗。
现给出N颗恒星的坐标,要求统计出0~N-1类星的个数。
【输入格式】
输入文件第一行包含一个整数N,表示恒星总数。
接下来的N行每行两个整数表示一颗恒星的坐标。不存在两颗星拥有相同的坐标。
【输出格式】
输出文件包含N行,每行包含一个整数,第i行表示第i-1类星的数量。
【样例输入】
5
3 3
5 1
5 5
1 1
7 1
|
【样例输出】
1
2
1
1
0
|
|
【数据范围】
对于20%的数据,n<=10
00
;
对于100%的数据, n<=100
000
;
【题意简述】
平面中有N个点,对于每个点(x,y),要求输出在其左下方(包括正左正下)点的个数。N<=100000;x,y<=maxlongint。
【题目考点】快排+树状数组
【题目分析】本题完成
单点加减,区间求和的功能
■
枚举?
■
代码简单,时间复杂度达到O(n^2)
■
有没有代码简单、时间复杂度低的方法?
■
此题有效的算法很多,树状数组可以简洁快速的解决此问题。
□
如何构建树状数组?
□
如何处理
“
左下方
”
?
■
首先按x坐标从小到大排序,x相同则y坐标由小到大,然后从左到右扫描每个点,这样可以保证已经插入树状数组的点都在左侧或正下侧。
■
我们只需寻找有多少点位于当前点下方,很容易想到树状数组。处理完当前点后,将其按y坐标插入树状数组,即让a[y]加1;
■
注意,就是横或纵坐标为0的情况,如果在更新的时候循环用的条件是x<=N,对于x=0的情况,会无限循环,因为x+lowbit(x)依然是0,因此我们对于所有的横坐标都加1,这样就解决这个问题
#include
#include
using namespace std;
struct Point{int x,y;}a[100005];
int n,c[100005]={0},Ans[100005]={0},MaxY;//Ans[i]第i+1类星星的数量
bool cmp(const Point &a,const Point &b)
{ return (a.x
void Read()
{ int i;
cin>>n;MaxY=0;
for(i=1;i<=n;i++)
{ scanf("%d%d",&a[i].x,&a[i].y);
a[i].x++; a[i].y++;
MaxY=max(MaxY,a[i].y);
}
}
int Lowbit(int i){return i&(-i);}
void Add(int t){for(int i=t;i<=MaxY;i+=Lowbit(i))c[i]++;}
int Sum(int t)
{ int i,s=0;
for(i=t;i>0;i-=Lowbit(i))s+=c[i];
return s;
}
void Solve()
{ int i;
sort(a+1,a+n+1,cmp);//按照x从小到大排序,x相同y从小到大
for(i=1;i<=n;i++){Ans[Sum(a[i].y)]++;Add(a[i].y);}
}
int main()
{ Read();
Solve();
}
【例题3】
cows
(
PKU 2481
)1459
【问题描述】
农民约翰的奶牛们已经发现,越来越多的草沿山脊(看成是一个数轴)长的特别好。约翰有
N
头牛(编号从
1
到
N
)。每头奶牛都特别喜欢吃一定范围内的草(可能重叠)。这个范围可以看成是一个闭区间
[S,E]
。
例如两头牛
cowi
和
cowj
,它们喜欢吃草的范围分别为
[Si,Ei]
和
[Sj,Ej]
。如果
Si<=Sj
,
Ej<=Ei
且
Ei-Si>Ej-Sj
,我们就是
cowi
比
cowj
强壮。对于每头牛来说,有多少牛是比她强呢?农民约翰需要你的帮助!
【输入格式】
输入文件包含多组测试数据。
每组测试数据的第一行为一个整数
N (1<=N<=105),
表示奶牛的头数;
接下来
N
行,第
i+1
行两个整数
S
和
E(0<=S5)
,表示第
i
头奶牛的范围,最后用一个
0
作为文件结束。
【输出格式】
对于每组测试数据输出仅一行为
n
个用空格分开的整数,第
i
个数字表示比第
i
头牛强壮的个数。
【样例输入】
3
1 2
0 3
3 4
0
【样例输出】1 0 0
【题目大意】
FJ
有n头牛(编号为1~n),每一头牛都有一个测验值[S, E],如果对于牛i和牛j来说,它们的测验值满足下面的条件则证明牛i比牛j强壮:Si<=Sj and Ej<=Ei and Ei-Si>Ej -Sj。现在已知每一头牛的测验值,要求输出每头牛有几头牛比其强壮。
【题目分析】
■
此题条件简单,但并不直观;
■
将区间按照横坐标从小到大排序,横坐标相同纵坐标从大到小排序,我们发现,对于每头牛,要求的就是其左上方牛的个数。
■
同stars,注意树状数组是从1开始的和判断点重合的情况;
#include
#include
#include
using namespace std;
const int Maxn=100005;
struct Point{int x,y,id;}a[Maxn];
int n,ans[Maxn],f[Maxn];
bool cmp(const Point &a,const Point &b)
{ return (a.xb.y);}
int Getsum(int x)//树状数组求前X项和
{ int i,sum=0;
for(i=x;i>0;i-=i&(-i))sum+=f[i];
return sum;
}
void Add(int x,int d)//给X插入个1
{ for(int i=x;i
void Read()//读入
{ int i;
for(i=1;i<=n;i++)
{ cin>>a[i].x>>a[i].y;
a[i].x++;a[i].y++;//注意树状数组的下标必须从1开始
a[i].id=i;
}
sort(a+1,a+n+1,cmp);//按照横坐标从小到大,横坐标相同纵坐标从大到小
}
void Solve()
{ int i,Max=0;
memset(f,0,sizeof(f));
ans[a[1].id]=0;Add(a[1].y,1);Max=a[1].y;//初始化排序后的第一头牛
for(i=2;i<=n;i++)//从第2头牛依次处理
{ if((a[i].x==a[i-1].x)&&(a[i].y==a[i-1].y))ans[a[i].id]=ans[a[i-1].id];//区间相同
else ans[a[i].id]=Getsum(Max)-Getsum(a[i].y-1);//统计
Add(a[i].y,1);//插入
Max=max(Max,a[i].y);//更新Max
}
cout<
}
int main()
{ cin>>n;
while(n!=0)
{ Read();
Solve();
cin>>n;
}
}
【例题4】逆序对
Ultra-QuickSort
(
PKU 2299
)
2662
【问题描述】
给定N个数,可以任意交换相邻的两个数,最后使其变成升序,问需要交换多少次。
【输入格式】
输入文件包含多组测试数据。
每组测试数据的第一行为一个整数n(n<=100000),表示输入序列的长度;
第二行为n个用空格分开的整数a[i](
0≤a[i]≤999,999,999
),最后用一个0作为文件结束,不必处理。
【输出格式】
对于每组测试数据输出一行为最少交换的次数。
【样例输入】
|
【样例输出】
|
5
9
1
0
5
4
3
1
2
3
0
|
6
0
|
【方法1】树状数组
◊
这个题目的模型就是求逆序对的数目。
◊
此题直接枚举同样需要n2的时间
◊
此题同样解法较多,可以使用分治法类似归并排序,也可以使用树状数组。
◊
此题和stars同样有两个限制:
“
ia[j]
”
◊
应用同一思路,顺序扫描,将其转化为一个限制
——
a[i]>a[j]。
算法步骤:
◊
首先对a数组离散化;
◊
按顺序扫描,只需找到有多少比a[i]大的数已经出现过。这可以用树状数组维护;
◊
初始时,数组全为0。每次扫描到a[i],用树状数组求出a[i]+1~max中出现过多少个数,然后将a[i]插入树状数组;
例如n=5,输入100 6 7 2 3;首先离散化,输入变为5 3 4 1 2;顺序扫描
#include
using namespace std;
const int maxn=500001;
int n;long long ans,a[maxn],b[maxn],link[maxn],f[maxn];
void qsort(int L,int r)
{ int i,j;long long x,t;
i=L;j=r;x=a[(L+r)/2];
while(i<=j)
{ while(a[i]
while(a[j]>x)j--;
if(i<=j){swap(a[i],a[j]);swap(link[i],link[j]);i++;j--;}
}
if(i
if(L
}
long long getsum(long long x)//前N项和
{ long long i,g=0;
for(i=x;i>0;i-=i&(-i))g+=f[i];
return g;
}
void insert(long long x)//插入
{ for(long long i=x;i<=n;i+=i&(-i))f[i]++;}
void init()
{ int i;
for(i=1;i<=n;i++){cin>>a[i];link[i]=i;}
qsort(1,n);
for(i=1;i<=n;i++)b[link[i]]=i;//离散化
}
void solve()
{ int i,ma;
memset(f,0,sizeof(f));ans=0;ma=0;
for(i=1;i<=n;i++)
//依次统计和b[i]构成的逆序对个数
{ ans+=getsum(ma)-getsum(b[i]);
insert(b[i]);//将b[i]插入树状数组中
if(b[i]>ma)ma=b[i];
}
}
int main()
{ cin>>n;
while(n!=0){init();solve();cin>>n;}
}
【方法2】:归并排序
#include
using namespace std;
const int maxn=500001;
int n;long long ans,a[maxn],b[maxn];
void mergesort(int L,int mid,int r)
{ int i,j,k;
i=L;j=mid+1;k=L;
while(k<=r)
else{b[k]=a[j];j++;ans+=mid-i+1;}
k++;
}
for(i=L;i<=r;i++)a[i]=b[i];
}
void merge(int L,int r)
{ int mid;
mid=(L+r)/2;
if(L!=r)
{
merge(L,mid);
merge(mid+1,r);
mergesort(L,mid,r);
}
}
void solve()
{ int i;ans=0;
for(i=1;i<=n;i++)cin>>a[i];
merge(1,n);
}
int main()
{ cin>>n;
while(n!=0){solve();cin>>n;}
}
【例题5】
Japan(POJ3067) 3736
【问题描述】
日本岛的东海岸和西海岸分别有N和M个城市
(M,N<=1000)
,在这些城市中有K条高速公路,每条公路连接着东海岸一个城市和西海岸的一个城市,最多有两条高速公路在同一个城市车出发或者到达,问总共这些公路有多少交叉点。
【输入格式】
输入第一行一个整数T,表示有T组测试数据。对于每组测试数据的第一行有三个整数N,M,K,接下来K行,每行两个整数,表示有一条高速公路连接着东海岸和西海岸的城市编号。
【输出格式】
对于每组测试数据输出仅一行为交叉点的个数。
【样例输入】
1
3 4 4
1 4
2 3
3 2
3 1
【样例输出】
Test case 1: 5
【题目分析】求逆序对问题。对左边的点从小到大排序,相等则对右边的从小到大排,最后只要求右边的逆序对即可,求逆序对除了树状数组还有归并排序。
注意:边可以达到1000*1000,结果会超int。
#include
#include
using namespace std;
int maxn,n,m,k;
struct Point{int x,y;}a[1000010];
long long c[1005];
int Lowbit(int i){return i&(-i);}
long long Sum(int x)
{ long long res=0;
for(int i=x;i<=maxn;i+=Lowbit(i))res+=c[i];
return res;
}
void Modify(int x)
{
for(int i=x;i>0;i-=Lowbit(i))c[i]++;
}
bool cmp(const Point &a,const Point &b)
{ return (a.x
int main()
{ int t,cas=1,i;
long long ans;
scanf("%d",&t);
while(t--)
{ ans = 0;
scanf("%d %d %d",&n,&m,&k);
for(i=1;i<=m;i++)c[i]=0;
maxn=m;
for(i=1;i<=k;i++)scanf("%d %d",&a[i].x,&a[i].y);
sort(a+1,a+1+k,cmp);
for(i=1;i<=k;i++)
{ ans+=Sum(a[i].y+1);
Modify(a[i].y);
}
printf("Test case %d: %
ll
d\n",cas++,ans);
}
return 0;
}
【例题6】移动电话(
IOI2001
/
PKU 1195
)
2653
【问题描述】
假设第四代移动电话的收发站是这样运行。整个区域被分割成很小的方格。所有的方格组成了一个
S*S
的矩阵,行和列从
0~S-1
编号。每个小方格都包含一个收发站。每个方格内的开机的移动电话数量可以不断改变,因为手机用户在各个方格之间移动,也有用户开机或者关机。一旦某个方格里面开机的移动电话数量发生了变化,该方格里的收发站就会向总部发送一条信息说明这个改变量。
总部要你写一个程序,用来管理从各个收发站收到的信息。老板可能随时会问:某一个给定矩形区域内有多少部开机的移动电话啊?你的程序必须要能随时回答老板的问题。
【输入格式】
输入包括一个指示数和一些参数,见下表:
指示数
|
参数
|
意义
|
0
|
S |
初始指令。整个区域由
S*S
个小格子组成。这个指令只会在一开始出现一次。
|
1
|
X Y A
|
方格
(X,Y)
内的开机移动电话量增加了
A
。
A
可能是正数也可能是负数。
|
2
|
L B R T
|
询问在矩形区域
(L,B)—(R,T)
内有多少部开机的移动电话。矩形区域
(L,B)—(R,T)
包括所有的格子
(X,Y)
满足
L
£
X
£
R, B
£
Y
£
T
。
|
3
|
|
终止程序。这个指示只会在最后出现一次。
|
所有的数据总是在给定的范围内,你不需要查错。特别的,如果
A
是负数,你可以认为该操作不会让该格子的开机移动电话数变成负数。格子是从
0
开始编号的,比如一个
4*4
的区域,所有的格子
(X,Y)
应该表示为
0
<=
X
<=
3,0
<=
Y
<=
3
。
【输出格式】
如果指示是
2
,输出一个整数,表示该区域内开机的电话数目。
【样例输入】
0 4
//
初始化
4
´
4
的区域
.
1 1 2 3
//
格子
(1,2)
加
3
。
2 0 0 2 2
//
询问矩形
0
£
X
£
2,0
£
Y
£
2
里面的开机移动电话总量
1 1 1 2 //
格子
(1,1)
加
2
。
1 1 2 -1 //
格子
(1,2)
减
1
。
2 1 1 2 3 //
询问矩形
1
£
X
£
2,1
£
Y
£
3
里面的开机移动电话总量。
3
【样例输出】
3 //
回答询问
4 //
回答询问
【数据范围】
区域大小
|
S
´
S
|
1
´
1
£
S
´
S
£
1024
´
1024
|
每个格子的值
|
V |
0
£
V
£
215 –1 (= 32767)
|
增加
/
减少量
|
A |
-215
£
A
£
215–1 (= 32767)
|
指令总数
|
U |
3
£
U
£
60002
|
所有格子的总和
|
M |
M= 230
|
在
20
个输入数据中,有
16
个数据的区域大小不超过
512*512
。
【题目大意】
给定一个N*N的矩阵A,每个元素的初始值为0,可以对矩阵进行一下两种操作:
1、修改A[i][j]的值为d,(1<=i,j<=N);
2、查询左下角坐标为(x1,y1),右上角坐标为(x2,y2)的子矩阵的元素和;
【题目分析】
■
此题可以用二维线段树或二维树状数组解决。
■
二维树状数组的代码与一维及其相似。
■
对于询问(x1,y1)-(x2,y2),Ans=Sum(x2,y2)-Sum(x2,y1-1)-Sum(x1-1,y2)+Sum(x1-1,x2-1);
■
树状数组下标必须从1开始
#include
#include
using namespace std;
int n,c[1100][1100];
int Lowbit(int x){ return x&(-x);}
void Add(int x,int y,int k)//a[x][y]+k
{ int i,j;
for(i=x;i<=n;i=i+Lowbit(i))
for(j=y;j<=n;j=j+Lowbit(j))c[i][j]+=k;
}
int Sum(int x,int y)
//求以(1,1),(x,y)分别为左上顶点,右下顶点矩形区域内的和
{ int i,j,s=0;
for(i=x;i>0;i=i-Lowbit(i))
for(j=y;j>0;j=j-Lowbit(j))s+=c[i][j];
return s;
}
int search(int x1,int y1,int x2,int y2)
//计算以(x1,y1),(x2,y2)分别为左上顶点,右下顶点的矩形区域内的和
{ return Sum(x2,y2)-Sum(x1-1,y2)-Sum(x2,y1-1)+Sum(x1-1,y1-1);}
int main()
{ int t,i,j,x1,y1,x2,y2,k;
while(1)
{ scanf("%d",&t);
if(t==0)
{ scanf("%d",&n);
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)c[i][j]=0;
}
if(t==1)
{ scanf("%d%d%d",&x1,&y1,&k);
x1++;y1++;
Add(x1,y1,k);
}
if(t==2){ scanf("%d%d%d%d",&x1,&y1,&x2,&y2);
x1++;y1++;x2++;y2++;
printf("%d\n",search(x1,y1,x2,y2));
}
if(t==3)break;
}
return 0;
}
【例题7】
Apple Tree
(
PKU 3321
)3161
【问题描述】
有一棵N个结点的树,一开始每个结点上都有一个苹果,每次有两种操作:
(1)C x:如果x结点上有一个苹果,那么摘下它,否则x节点上会再生出一个苹果;
(2)Q x:询问以x结点为根的子树中苹果的个数;
你要对于每个Q操作,输出对应的答案。
【输入格式】
第一行为一个数N,表示树的结点个数,默认以1为根;
接下来N-1行,每行两个数Ui,Vi,表示一条树边;
然后一行是一个数M,表示操作数目,接下来M行为M个操作,格式如题所述。
【输出格式】
对于每个Q操作,输出对应的答案,一个操作一行。
【样例输入】
|
【样例输出】
|
3
1 2
1 3
3
Q 1
C 2
Q 1
|
3
2
|
【数据范围】
对于30%的数据,N,M<=1000;
对于100%的数据,N,M<=100000;
【题目分析】
■由于题目给定的是一棵树,无法直接建立树状数组,需要进行转化。
■将树转化成链,通常的办法是将其转化为dfs序列。对于任意的结点其后继结点的前后序遍历号一定在该结点的前后序号之间。
■深度优先遍历此树,初始k=0。每次新访问一个结点时,k+1,将该结点存入,然后递归遍历他的儿子结点,然后k+1,第二次将该结点存入,这样就形成了一个dfs序列。
■对于每个结点,记录他两次出现在dfs序列的位置pre和post,以便于查找。
■对于此图,得到的一个可能的dfs序列是:1,2,2,3,4,4,5,5,3,1
■对于取反操作,在pre[i]位置值加减1。
■对于查询操作答案是Sum(post[a])-Sum(pre[a]-1)
#include
#include
#include
using namespace std;
const int MAXN=100005;
struct Edge{int y,next;}w[2*MAXN];
int head[MAXN],c[MAXN],low[MAXN],high[MAXN];
int n,m,k,step=0,vst[MAXN],f[MAXN];
int Lowbit(int i){return i&(-i);}
int sum(int x)
{ int ret=0;
for(int i=x;i>0;i-=Lowbit(i))ret+=c[i];
return ret;
}
void Add(int x,int d)
{ for(int i=x;i<=n;i+=Lowbit(i))c[i]+=d;}
void AddEdge(int u,int v){w[k].y=v;w[k].next=head[u];head[u]=k++;}
void DFS(int u)
{ int i;
low[u]=++step;
vst[u]=1;
for(i=head[u];i!=-1;i=w[i].next)
if(!vst[w[i].y])DFS(w[i].y);
high[u]=++step;
}
void Read()
{ int i,u,v;
memset(head,-1,sizeof(head));
memset(vst,0,sizeof(vst));
scanf("%d",&n);
k=1;
for(i=1;i
}
void Solve()
{ int i,x;char ch;
for(i=1;i<=n;i++){Add(low[i],1);f[i]=1;}//初始化每个结点一个苹果
scanf("%d",&m);
while(m--)
{ while(ch=getchar())if(ch=='C'||ch=='Q')break;
scanf("%d",&x);
if(ch=='C'){Add(low[x],f[x]);f[x]=-f[x];}
else printf("%d\n",sum(high[x])-sum(low[x]-1));
}
}
int main()
{ Read();
DFS(1);
Solve();
}
2、单点查询、区间修改
考虑将原数组差分,令c
[i]=a[i]-a[i-1]
,特别地,c
[1]=a[1]
。
那么区间[L
,r]
整体加上d的操作就可以简单地使用c
[
L
]+=
d
;
c
[r+1]-=
d来完成了。
此时a[i]=c
[1]+..+
c
[i]
,所以单点查询a[i]实际上就是在求c数组的[1..i]区间和,很容易完成了。
括号:
■括号表示法是运用树状数组解题的重要方法之一。
■应用括号表示,可以将一部分修改区间、查询点值的题目转化为修改点值、查询区间,从而可以使用树状数组。
|
【例题1】
Color the ball
(HDU1556)1377
【问题描述】
N
个气球排成一排,从左到右依次编号为1,2,3....N。每次给定2个整数a和
b(a<=b)
,
lele
便为骑上他的
“
小飞鸽
”
牌电动车从气球a开始到气球b依次给每个气球涂一次颜色。但是N次以后lele已经忘记了第i个气球已经涂过几次颜色了,你能帮他算出每个气球被涂过几次颜色吗?
【输入格式】
每个测试实例第一行为一个整数N(N<=100000)。接下来的N行,每行包括2个整数a b(1<=a<=b<=N)。当N=0,输入结束。
【输出格式】
每个测试实例输出一行,包括N个整数,第
i
个数代表第
i
个气球总共被涂色的次数。
【样例输入】
3
1 1
2 2
3 3
3
1 1
1 2
1 3
0
【样例输出】
1 1 1
3 2 1
【题目分析】
■
此题与前面几题不同,要求完成
区间加减,单点查询的功能
。
■
使用线段树可以轻松处理。
■
有没有更简单、快速的方法?
■
利用括号,转化为树状数组;
■
要将修改区间操作转化为修改点的操作,只有在区间端点做文章。
■
每次修改区间,便在区间两端加括号。这样,每次询问时只需要输出从1~这个点中左括号数-右括号数。
■
具体的,对于每个修改操作[L,r],将树状数组中c[L]+1,c[r+1]-1。对于询问操作t,输出Getsum(t)。
■
时间复杂度O(mlogn)
#include
#include
#include
using namespace std;
const int MAXN=100005;
int c[MAXN],n;
int Lowbit(int x){return x&(-x);}
void Add(int x,int d){for(int i=x;i<=n;i+=Lowbit(i))c[i]+=d;}
int Sum(int x)
{ int i,ret=0;
for(i=x;i>0;i-=Lowbit(i))ret+=c[i];
return ret;
}
int main()
{ int i,j,L,r;
while(1)
{ scanf("%d",&n);
if(n==0)break;
memset(c,0,sizeof(c));
for(i=1;i<=n;i++)
{ scanf("%d%d",&L,&r);
Add(L,1);
Add(r+1,-1);
}
for(i=1;i
printf("%d\n",Sum(n));
}
return 0;
}
【例题2】简单题(CQOI2006)
1152
【问题描述】
有一个
n
个元素的数组,每个元素初始均为
0
。有
m
条指令,要么让其中一段连续序列数字反转
——0
变
1
,
1
变
0
(操作
1
),要么询问某个元素的值(操作
2
)。例如当
n=20
时,
10
条指令如下:
【输入格式】
输入文件第一行包含两个整数
n
,
m
,表示数组的长度和指令的条数;
以下
m
行,每行的第一个数
t
表示操作的种类。若
t=1
,则接下来有两个数
L, R (L<=R)
,表示区间
[L, R]
的每个数均反转;若
t=2
,则接下来只有一个数
I
,表示询问的下标。
【输出格式】
每个操作
2
输出一行(非
0
即
1
),表示每次操作
2
的回答。
【样例输入】
|
【样例输出】
|
20 10
1 1 10
2 6
2 12
1 5 12
2 6
2 15
1 6 16
1 11 17
2 12
2 6
|
1
0
0
0
1
1
|
【数据范围】
50%
的数据满足:
1<=n<=1,000
,
1<=m<=10,000
100%
的数据满足:
1<=n<=100,000
,
1<=m<=500,000
【题目分析】本题完成
区间加减,单点查询的功能
#include
#include
using namespace std;
int c[100005]={0},n;
int Lowbit(int x){return x&(-x);}
void Add(int x,int d)
{ for(int i=x;i<=n;i+=Lowbit(i))c[i]+=d;}
void Ask(int x)
{ int i,Ans=0;
for(i=x;i>=1;i-=Lowbit(i))Ans+=c[i];
printf("%d\n",Ans%2);
}
int main()
{ int p,x,y,i,m;
cin>>n>>m;
for(i=1;i<=m;i++)
{ scanf("%d",&p);
if(p==1){scanf("%d%d",&x,&y);Add(x,1);Add(y+1,1);}
else{scanf("%d",&x);Ask(x);}
}
return 0;
}
【思考题】
Topcoder SRM 310 500 FloatingMedian
【问题描述】
有
N
个硬币,标号为
1,2,…,N
,一开始都是正面朝上。定义两种操作:
①
T I J
表示将
I
到
J
的硬币翻转;
②
Q I
表示询问第
I
个硬币的正反,正面回答
1
,反面回答
0
;
现给一系列上述操作,动态回答。
【思路点拨】我们先考虑最朴素的做法。我们用数组
c
,记录每个硬币被翻转了几次,那么对于操作①,我们只要对
c[i..j]
都加上
1
。而操作②,我们只要输出
c[i]%2
就可以了。
但是这样的做法最坏情况下复杂度为
O(n2)
,不能令人满意。
我们不妨换个思路,把
c
看成求和数组,并根据它定义原始数组
v
,如果把
c[k]
看作是
v[1]+v[2]+…v[k]
,那么对于操作①,我们只不过是将
v[i]
增加
1
,而
v[j+1]
减少
1
而已!这两个操作对
c[i..j]
都加上
1
,对其他元素保持不变,于是我们利用树状数组来维护
c
数组,这样时间复杂度就变成了
O(nlogn)
,问题至此解决。
【例题3】矩阵
Matrix
(
PKU2155
)
2658
【问题描述】
给一个
N*N
的矩阵
A
,其中元素是
0
或
1
。
A[i,j]
表示在第
i
行第
j
列的数。最初时,
A[i,j]=0(1<=i,j<=N)
。我们以以下方式来改变矩阵,给定一个矩形的左上角为
(x1,y1)
和右下角为
(x2,y2)
,我们对这个矩形范围内的所有元素进行“非”操作(如果它是一个
'0'
,那么变化为
'1'
,否则它变为
'0'
)。请你编写一个程序完成以下两种操作:
1.C x1 y1 x2 y2 (1<=x1<=x2<=n,1<=y1<=y2<=n)
改变左上角为
(x1,y1)
和右下角为
(x2,y2)
矩形范围内的值。
2.Q x y (1<=x,y<=n)
询问
A[x
,
y]
的值。
【输入格式】
输入文件的第一行是一个整数
x(x<=10)
代表测试数据的组数。
对于每组测试数据的第一行包含两个数字
N
和
T
(
2<=N<=1000
,
1<=T<=50000
)分别代表矩阵的大小和操作的次数。
接下来
T
行,每行代表一个指令操作
“Q x y”
或者
“C x1 y1 x2 y2”
。
【输出格式】
输出文件若干行,每行对应一个
Q
操作表示
A[x,y]
的值。
【输入输出样例】
m
atrix
.in
|
m
atrix
.out
|
1
2 10
C 2 1 2 2
Q 2 2
C 2 1 2 1
Q 1 1
C 1 1 2 1
C 1 2 1 2
C 1 1 2 2
Q 1 1
C 1 1 2 1
Q 2 1
|
1
0
0
1
|
【题目大意】给定一个n*n的01矩阵,初始全部为0。要求维护两个操作:
1、C x1 y1 x2 y2,子矩阵(x1,y1)~(x2,y2)中数值全部取反
2、Q x y,询问点(x,y)的值
【题目分析】
■
区间加减,单点查询的功能
■
此题是区间求反问题,而树状数组所维护的是区间和问题。
■
如何转化?
■
注意到所求为一个点的值,而点值只与求反操作次数有关;
■
首先考虑一维情况
■
原问题变为:对于一个数列,每次对一段区间取反,询问一个点的值。
■
由于一个点的值只与取反次数有关,所以我们记录每个点的取反次数。
■
树状数组支持的操作是修改一个点的值,查询一段区间的和。而此题恰恰相反。
■
如何改变树状数组的意义?
——
括号
■
每次对一段区间(a,b)取反,可以看成加了一对括号。
■
每次询问只需求出从1到k中左括号-右括号,即为点k修改的次数;
■
数组c[i]记录1~i中左括号-右括号的值。
■
每次修改(a,b),c[a]加1,c[b+1]减1。
■
这样,每次询问求出c[k],判断奇偶即可。
■
如何推广到二维?
#include
#include
using namespace std;
int n,c[1005][1005]={0},Ans;
int Lowbit(int x){return x&(-x);}
void Add(int x,int y,int d)
{ int i,j;
for(i=x;i<=n;i+=Lowbit(i))
for(j=y;j<=n;j+=Lowbit(j))c[i][j]+=d;
}
void Ask(int x,int y)
{ int i,j;
for(i=x;i>=1;i-=Lowbit(i))
for(j=y;j>=1;j-=Lowbit(j))Ans+=c[i][j];
}
void Solve()
{ char cc; int t,i,j,k,x1,x2,y1,y2;
cin>>n>>t;
for(i=1;i<=n;i++)for(j=1;j<=n;j++)c[i][j]=0;
while(--t>=0)
{ cin>>cc;
if (cc=='C')
{ scanf("%d%d%d%d",&x1,&y1,&x2,&y2);
Add(x1,y1,1); //从原点到x1,y1加上1
Add(x2+1,y2+1,1); //从原点到x2+1,y2+1加上1
Add(x1,y2+1,-1); //从原点到x1-1,y2加上-1
Add(x2+1,y1,-1); //从原点到x2,y1-1加上-1
}//这样就相当于给x1,y1,x2,y2加上了1
else{ scanf("%d%d",&x1,&y1);Ans=0;
Ask(x1,y1);
printf("%d\n",Ans%2);
}
}
}
int main()
{ int i,k;
cin>>k;
for(i=1;i<=k;i++){Solve();cout<
return 0;
}
【例题4】
C
ube
(HDU
3584
)
1354
【问题描述】
给定一个体积为N*N*N立方体,每个单位小立方体A[x
,y,z]
里有一个值,初始值全部为0,我们可以对立方体进行一下两种操作:
操作
“Not”
:改变
A[i,j,k]=!A[i,j,k]
。意思是改变
A[i,j,k]
的值,从
0->1
或者
1->0
。
(x1<=i<=x2,y1<=j<=y2,z1<=k<=z2)
。
操作
“
Query”
:询问
A[i,j,k]
的值。
【输入格式】
多组测试数据。对于每组测试数据:
第一行包含两个整数N和M,接下来M行,每行首先一个整数X,若X=1表示
“
Not
”
操作;X=0表示
“
Query
”
操作;如果X=1,接下来
x1, y1, z1, x2, y2, z2
;如果
X=0
,接下来x,y,z。
【输出格式】
对于每个
“Query”
操作输出一行。
(1<=n<=100,m<=10000)
【样例输入】
2 5
1 1 1 1 1 1 1
0 1 1 1
1 1 1 1 2 2 2
0 1 1 1
0 2 2 2
【样例输出】
1
0
1
【题目分析】三维树状数组
#include
#include
#include
using namespace std;
const int MAXN=105;
int c[MAXN][MAXN][MAXN],n;
int Lowbit(int x){return x&(-x);}
void Add(int x,int y,int z)
{ int i,j,k;
for(i=x;i
for(j=y;j
for(k=z;k
c[i][j][k]++;
}
int Sum(int x,int y,int z)
{ int i,j,k,ret=0;
for(i=x;i>0;i-=Lowbit(i))
for(j=y;j>0;j-=Lowbit(j))
for(k=z;k>0;k-=Lowbit(k))
ret+=c[i][j][k];
return ret;
}
int main()
{ int x1,x2,y1,y2,z1,z2,i,m,t;
while(scanf("%d%d",&n,&m)!=EOF)
{ memset(c,0,sizeof(c));
for(i=1;i<=m;i++)
{ scanf("%d",&t);
if(t)
{ scanf("%d%d%d%d%d%d",&x1,&y1,&z1,&x2,&y2,&z2);
Add(x1,y1,z1);
Add(x2+1,y1,z1);
Add(x1,y2+1,z1);
Add(x1,y1,z2+1);
Add(x2+1,y2+1,z1);
Add(x2+1,y1,z2+1);
Add(x1,y2+1,z2+1);
Add(x2+1,y2+1,z2+1);
}
else
{ scanf("%d%d%d",&x1,&y1,&z1);
printf("%d\n",Sum(x1,y1,z1)&1);
}
}
}
return 0;
}
3、区间修改,区间查询
■仍然沿用c数组,考虑a数组[1,x]区间和的计算。c
[1]
被累加了x次,c
[2]
被累加了x-1次,...,c
[x]
被累加了1次。
■因此得到
∑
a[i]=
∑
{c
[i]*(x-i+1)}=∑{
c
[i]*(x+1) -
c
[i]*i}=(x+1)*∑
c
[i]-∑(
c
[i]*i)
■所以我们再用树状数组维护一个数组c
2[i]=
c
[i]*i
,即可完成任务。
【例题1】区间操作(POJ 3468)3735
【问题描述】
给你N个整数
A
[
1
]
, A
[
2
]
, ... , A
[
N
]。你需要处理两类问题:
“C a b c”
表示给A[
a
]
, A
[
a+1
]
, ... , A
[
b
]之间的每个数都加上
c(-10000≤c≤10000)
。
“Q a b”
求A[
a
]
, A
[
a+1
]
, ... , A
[
b
]之间数字的总和;
【输入格式】
输入的第一行包含两个整数
N
和
Q
(
1≤
N
,
Q
≤100000
);
第二行包含N个整数Ai(
-10
^9
≤
Ai
≤10
^9)
接下来Q行,表示Q个问题,形式如题;
【输出格式】
输出要求计算出的区间总和,每行一个。
【输入输出样例】
opt.in
|
opt.out
|
10 5
1 2 3 4 5 6 7 8 9 10
Q 4 4
Q 1 10
Q 2 4
C 3 6 3
Q 2 4
|
4
55
9
15
|
【注意】总和可能会超过32位。
【题目分析】本题完成
区间加减,区间求和的功能。
【方法1】:线段树
#include
#include
#include
using namespace std;
#define LL long long
struct Node{LL L,R,lazy,sum;}tree[100005*4];
int n,q;
void up(LL v)
{ LL ls=v<<1,rs=v<<1|1;
tree[v].sum=(tree[ls].sum+tree[rs].sum);
}
void down(LL v,LL c)
{ LL ls=v<<1,rs=v<<1|1;
if(tree[v].lazy)
{ tree[ls].lazy+=tree[v].lazy;
tree[rs].lazy+=tree[v].lazy;
tree[ls].sum+=(c-c/2)*tree[v].lazy;
tree[rs].sum+=(c/2)*tree[v].lazy;
tree[v].lazy=0;
}
}
void Built(LL v,LL L,LL R)
{ tree[v].L=L;tree[v].R=R;tree[v].lazy=0;tree[v].sum=0;
if(L==R){scanf("%lld",&tree[v].sum);return;}
LL mid=(L+R)/2;
Built(v<<1,L,mid);
Built(v<<1|1,mid+1,R);
up(v);
}
void Modify(LL v,LL L,LL r,LL c)
{ if(tree[v].L>=L&&tree[v].R<=r)
{ tree[v].lazy+=c;
tree[v].sum+=(LL)c*(tree[v].R-tree[v].L+1);
return ;
}
down(v,tree[v].R-tree[v].L+1);
LL mid=(tree[v].L+tree[v].R)/2;
if(L<=mid)Modify(v<<1,L,r,c);
if(r>mid)Modify(v<<1|1,L,r,c);
up(v);
}
LL Ask(LL v,LL L,LL r)
{ if(tree[v].L>=L&&tree[v].R<=r)return tree[v].sum;
LL mid=(tree[v].L+tree[v].R)/2;
down(v,tree[v].R-tree[v].L+1);
LL Ans=0;
if(L<=mid)Ans+=Ask(v<<1,L,r);
if(r>mid)Ans+=Ask(v<<1|1,L,r);
return Ans;
}
int main()
{ LL L,R,c;
char ch;
scanf("%lld%lld",&n,&q);
Built(1,1,n);
for(int i=1;i<=q;i++)
{ while(ch=getchar())if(ch=='C'||ch=='Q')break;
if(ch=='Q'){scanf("%lld%lld",&L,&R);cout<
else{scanf("%lld%lld%lld",&L,&R,&c);Modify(1,L,R,c);}
}
}
【方法2】:树状数组
树状数组天生用来动态维护数组前缀和,其特点是每次更新一个元素的值,查询只能查数组的前缀和,但这个题目求的是某一区间的数组和,而且要支持批量更新某一区间内元素的值,怎么办呢?实际上,还是可以把问题转化为求数组的前缀和。
首先,看更新操作update(s,t,d)把区间A[s]...A[t]都增加d,我们引入一个数组c
[i]
,表示A[i]...A[n]的共同增量,n是数组的大小。那么update操作可以转化为:
1
)令c
[s]=
c
[s]+d
,表示将A[s]...A[n]同时增加d,但这样A[t+1]...A[n]就多加了d,所以
2
)再令c
[t+1]=
c
[t+1]-d
,表示将A[t+1]...A[n]同时减d;
然后来看查询操作Ask
(s,t)
,求A[s]...A[t]的区间和,转化为求前缀和,设sum[i]=
A[1]+...+A[i]
,则A[s]+...+A[t]=sum[t]-sum[s-1],那么前缀和sum[x]又如何求呢?它由两部分组成,一是数组的原始和,二是该区间内的累计增量和, 把数组A的原始值保存在数组org中,并且
c
[i]
对sum[x]的贡献值为c
[i]*(x+1-i)
,那么
sum[x]=org[1]+...+org[x]+
c
[1]*x+
c
[2]*(x-1)+
c
[3]*(x-2)+...+
c
[x]*1
=org[1]+...+org[x]+segma(
c
[i]*(x+1-i))
=segma(org[i])+(x+1)*segma(
c
[i])-segma(
c
[i]*i)
,1<=i<=x
这其实就是三个数组org[i], c
[i]
和c
[i]*i
的前缀和,org[i]的前缀和保持不变,事先就可以求出来,c
[i]
和c
[i]*i
的前缀和是不断变化的,可以用两个树状数组来维护。
#include
#include
using namespace std;
const int MaxN=100005;
//设delta[i]表示[i,n]的公共增量
long long c1[MaxN];//维护c[i]的前缀和
long long c2[MaxN];//维护c[i]*i的前缀和
long long sum[MaxN];
int A[MaxN],n;
int Lowbit(int i){return i&(-i);}
long long Getsum(long long *a, int x)
{ long long Ans=0;
for(int i=x;i>0;i-=Lowbit(i))Ans+=a[i];
return Ans;
}
void Modify(long long *a,int x,long long d)
{
for(int i=x;i<=n;i+=Lowbit(i))a[i]+=d;
}
int main()
{ int q,i,s,t,d;
long long Ans;
char ch;
scanf("%d%d",&n,&q);
for(i=1;i<=n;i++)scanf("%d",&A[i]);
for(i=1;i<=n;i++)sum[i]=sum[i-1]+A[i];
while(q--)
{
while(ch=getchar())if(ch=='C'||ch=='Q')break;
if(ch=='Q')
{ scanf("%d %d",&s,&t);
Ans=sum[t]-sum[s-1];
Ans+=(t+1)*Getsum(c1,t)-Getsum(c2,t);
Ans-=s*Getsum(c1,s-1)-Getsum(c2,s-1);
printf("%lld\n",Ans);
}
else{ scanf("%d %d %d",&s,&t,&d);
//把c[i](s<=i<=t)加d,策略是
//先把[s,n]内的增量加d,再把[t+1,n]的增量减d
Modify(c1,s,d);
Modify(c1,t+1,-d);
Modify(c2,s,d*s);
Modify(c2,t+1,-d*(t+1));
}
}
return 0;
}
【例题
2
】
上帝造题的七分钟
【题目描述】
XLk
觉得《上帝造题的七分钟》不太过瘾,于是有了第二部。
"
第一分钟,X说,要有数列,于是便给定了一个正整数数列。
第二分钟,L说,要能修改,于是便有了对一段数中每个数都开平方(下取整)的操作。
第三分钟,k说,要能查询,于是便有了求一段数的和的操作。
第四分钟,彩虹喵说,要是noip难度,于是便有了数据范围。
第五分钟,诗人说,要有韵律,于是便有了时间限制和内存限制。
第六分钟,和雪说,要省点事,于是便有了保证运算过程中及最终结果均不超过64位有符号整数类型的表示范围的限制。
第七分钟,这道题终于造完了,然而,造题的神牛们再也不想写这道题的程序了。"
——
《上帝造题的七分钟
·
第二部》
所以这个神圣的任务就交给你了。
【输入格式】
第一行一个整数n,代表数列中数的个数。
第二行n个正整数,表示初始状态下数列中的数。
第三行一个整数m,表示有m次操作。
接下来m行每行三个整数k,l,r,k=0表示给[l,r]中的每个数开平方(下取整),k=1表示询问[l,r]中各个数的和。
【输出格式】
对于询问操作,每行输出一个回答。
【样例输入】
10
1 2 3 4 5 6 7 8 9 10
5
0 1 10
1 1 10
1 1 5
0 5 8
1 4 8
【样例输出】
19 7 6
【数据范围】
对于30%的数据,1<=n<=1000,数列中的数不超过32767。
对于100%的数据,1<=n<=100000,1<=l<=r<=n,数列中的数大于0,且不超过1e12。
-
求序列中第k大数
【例4】魔兽争霸
描述
小
x
正在销魂地玩魔兽他正控制着死亡骑士和
N
个食尸鬼
(
编号
1
~
N)
去打猎。死亡骑士有个魔法,叫做
“
死亡缠绕
”
,可以给食尸鬼补充
HP
。
战斗过程中敌人会对食尸鬼实施攻击,食尸鬼的
HP
会减少。小
x
希望随时知道自己部队的情况,即
HP
值第
k
多的食尸鬼有多少
HP
,以便决定如何施放魔法。请同学们帮助他
:)
小
x
向你发出
3
种信号:(下划线在输入数据中表现为空格)
- A i a 表示敌军向第i 个食尸鬼发出了攻击,并使第i 个食尸鬼损失了a 点HP,如果它的HP<=0,那么这个食尸鬼就死了(Undead也是要死的……)。敌军不会攻击一个已死的食尸鬼。
- C i a 表示死亡骑士向第i个食尸鬼放出了死亡缠绕,并使其增加了a点HP。HP值没有上限。死亡骑士不会向一个已死的食尸鬼发出死亡缠绕
- Q k 表示小x向你发出询问
输入
第一行,一个正整数N,以后
N
个整数 表示
N
个食尸鬼的初始
HP
值
接着一个正整数
M
,以下
M
行 每行一个小
x
发出的信号
输出
对于小
x
的每个询问,输出
HP
第
k
多的食尸鬼有多少
HP
,如果食尸鬼总数不足
k
个,输出
-1
。每个一行数。
最后一行输出一个数:战斗结束后剩余的食尸鬼
分析
这道题目描述十分清楚,关键就是选取好的数据结构来实现。
我们设
C[i]
表示HP等于
i
出现的次数,假设所有数不超过
S
。
- 对于操作A_i_a:我们只需要将C[hp[i]]减1,C[hp[i]-a]加1即可,同时将hp[i]减a。要特殊考虑hp[i]-a小于等于0的情况;
- 对于操作C_i_a:我们只需要将C[hp[i]]减1,C[hp[i]+a]加1即可,同时将hp[i]加a;
- 对于操作Q_k:我们需要找到一个数x使得{ΣC[i]=k |i<=x,C[x]<>0}即可。
我们可以发现前两个操作的时间复杂度均为
O(1)
,而第三个操作的时间复杂度接近于
O(S)
。显然我们只要将操作
3
的时间复杂度降下来就可以了。注意到这道题目只需要求和,这时候树状数组就派上了巨大的用场。构建一个树状数组
C[]
:
- 对于操作A_i_a:我们只需要Add(hp[i],-1),Add(hp[i]-a,1)即可,同时将hp[i]减a。要特殊考虑hp[i]-a小于等于0的情况;
- 对于操作C_i_a:我们只需要Add(hp[i],-1),Add(hp[i]+a,1)即可,同时将hp[i]加a;
- 对于操作Q_k:我们可以使用二分查找来找到那个数x(实现方式和找最小值一样)。
至此,我们只需要将所有的数先离散化就可以了。
时间复杂度:
O(MlbN)
3.
逆序对问题的统计
【例5】逆序对
描述
对于一个包含N个非负整数的数组A[1..n],如果有i < j,且A[ i ]>A[ j ],则称(A[ i] ,A[ j] )为数组A中的一个逆序对。例如,数组(3,1,4,5,2)的逆序对有(3,1),(3,2),(4,2),(5,2),共4个。给定一个数组,求该数组中包含多少个逆序对。
输入
输入数据第一行包含一个整数N,表示数组的长度;
第二行包含N个整数。
输出
输出数据只包含一个整数,表示逆序对的个数。
分析
如题,这道题目只要求输出逆序对的个数就可以了,传统的求逆序对的方法有好多,这里我只介绍用树状数组求逆序对。
设C[i](i如果会很大,只需要离散化即可)表示i出现的次数。我们顺序枚举每一个数A[i],显然以A[i]为结尾的逆序对的个数为
,这一步我们可以利用树状数组优化到O(lbN),接下来inc(C[A[i]])。将所有的
加起来就可以计算出答案了。