目录
板块一:树状数组
引子:lowbit
1、存入数据(单点修改)
2、区间查询
3、区间修改和单点查询(差分数组)
4、求逆序对(两种版本)
5、二维的树状数组
6、树状数组求区间最大值
7、树状数组求第k大的数(???做到了再说)
板块二:线段树
前言:
1、建树:
2、区间修改+区间查询
据说树状数组写起来比线段树简单,不过对于初学者的博主来说还是很抽象。
这是一个非常神奇的数据结构,虽然是一维的数组但能存储树形的结构。
首先定义N为数组的长度,那么从1到N我们可以这样分类,从下往上,我们可以得到这样几种类型的数。
用我自己好理解的方式思考。
第零层:最多是2^0的倍数(奇数):*****1
第一层:最多是2^1的倍数***10
第二层:最多是2^2的倍数*****100
………………
首先,树状数组是完备的。奇数+2的倍数+ ………… = 奇数 + 偶数 = 所有下标。
这些数是有限的,因为有上界N,有种筛法的味道。
这些数字包含了所有可以引用的下标。
根据最低位数,我们将其分层。
为了方便索引树状数组里面的下标,我们构造一种函数lowbit,来获取最低位的数,也就是判断究竟是2的几次幂的倍数,是第几层。
再计算机中数字用补码储存。
1:补码000001, -1:11111110(这里位数不做考虑了)
1和-1的原码是一毛一样的,就是符号位不同。正数的补码是本身,负数的补码是原码取反(除符号位)加1,虽然符号位和1取反码加一不同,但是后面&后就没有区别了。按位取与。
x & -x 和 x & (~x + 1)是一样的,只不过前者写起来简洁。
如果原来的数是0*********1*1000000000000000000
那么按相反数是1*********0*1000000000000000000
那么这结果就是0000000001000000000000000000
就先不管符号位,因为后面会去掉。这样相反数的原码是一样的,求负数补码的过程中,先各位取反,那么最低位1的位置是零,其后的都是1,加上1之后,进位,一直进到最低位,此刻其他位都是相反的,这时候按位取与就可以得到是几的倍数。
板子:
int lowbit(int x) {
return x & -x;
}
数据结构首先它能存储数据,我们要如何存入数据?
其实存入数据也是一种单点修改的过程,就是把原来的0改成了某个特定的数罢了。
有序的数据结构,意味着我们要有序的存入数据,从而实现数据结构的维护。
树状数组父节点和子节点的关系:
也就是第0层和第1层之间的关系是如何的,我们要想使得最多是2的倍数,变成最多是4的倍数我们就需要将最低位的1变成零。也就是+1, +2, +4,刚好是加最低位所代表的数。
为什么不能加别的呢?如果是加别的,比如奇数,那么就无法有序的查询下一个父节点了。
通过这样,我们可以有序得查询下一个父节点。
树状数组的性质:
每一层是上一层通过进位得到,因而每层的长度都是上一层的两倍,这也是为什么可以得到logn的查询效率的原因,数学上证明博主不懂为什么这么构造,还需要慢慢体会。
板子:
void add(int x, int k) {
while (x <= n) {//不能超出上界
tree[x] += k;//父节点
x += lowbit(x);
}
}
像台阶一样一直去掉最低位,一级级得到几项和,最终得到前几项和。
没时间画图。
板子:
int ask(x){
int sum = 0;
for(int i=x;i;i-=lowbit(i)){
sum+=t[i];
}
return sum;
}
得到前几项和,就可以通过两次调用,做差得到某个区间的和。
练习1:hdoj1541
我只想说这是一道毒题,毒点是这里没有说多组数据,但是hdoj上默认输入多组数据,也是醉了。
wa了n次,哭死。
题目的意思是让我们求星星下方,左方所有的其他的星星。
批注:这道题由于x和y都是按照升序排的,所以后面的星星一定在前面的星星上面,或者同一个位置,所以我们只需要考虑先前放的x即可。
还有一件事,就是注意add/updata函数的上界的范围,数组开的大小!!。
这里没有给出坐标的上界,而这里树状数组存储又是横坐标,所以只能一直到坐标最大值,不要把星星的个数当作是上界!!!。
注意,树状数组坐标为0的话,sum会缺失,所有要加一。
还要就是,先求sum再加,这样就不用减一了。
代码:
#include
using namespace std;
const int N = 32005;
int tree[N], n, ans[N];
int lowbit(int x) {
return x & -x;
}
void add(int x, int k) {
while (x <= N) {
tree[x] += k;
x += lowbit(x);
}
}
int sum(int x) {
int res = 0;
while (x > 0) {
res += tree[x];
x -= lowbit(x);
}
return res;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
while (cin >> n) {
memset(tree, 0, sizeof(tree));
memset(ans, 0, sizeof(ans));
for (int i = 0; i < n; i++) {
int x, y;
cin >> x >> y;
x++;//防止坐标为0,如果坐标为零的话sum的时候可能有问题。
ans[sum(x)]++;
add(x, 1);
}
for (int i = 0; i < n; i++) {
cout << ans[i] << '\n';
}
}
return 0;
}
终于悟了。
在使用区间修改的时候,我们的树状数组不再是普通的数组了,其实存储的是差分,是区间左端和区间右端加一的差值。
首先,对于某个区间而言,内部都加上或者减去一个量,区间内的差分不变,差值不变。
例如:0 0 0 1 1 1 1 1 3 3 3》》》0 0 0 2 2 2 2 2 3 3 3。
而且,两侧区间内部的的差值不变。
然后,我们从树的最底层来看,我们考察相邻两个数的差值,我们发现,改变的只有,第一个1和第一个3,由于更新的传递性,所有包含这个点的父节点的两端差值都受到了影响,我们只需进行两次单点更新即可。
最后,为了获取该点的真实值,我们只需要进行前缀求和即可。着实妙哉。
练习1:hdoj 1556
下面是代码:
#include
using namespace std;
const int N = 1e5 + 5;
int tree[N], n;
int lowbit(int x) {
return x & -x;
}
int sum(int x) {
int res = 0;
while (x > 0) {
res += tree[x];
x -= lowbit(x);
}
return res;
}
void add(int x, int k) {
while (x <= n) {
tree[x] += k;
x += lowbit(x);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
while (cin >> n, n) {
for (int i = 0; i <= n; i++) {
tree[i] = 0;
}
int a, b;
for (int i = 1; i <= n; i++) {
cin >> a >> b;
add(a, 1);
add(b + 1, -1);
}
for (int i = 1; i <= n; i++) {
if (i == 1) {
cout << sum(i);
} else {
cout << ' ' << sum(i);
}
}
cout << '\n';
}
return 0;
}
练习2:洛谷P3368
批注:这里和上面那道题都是从零开始不一样,这里有初始值,我们不能简单的add这个点,因为我们需要的是差分数组,所以,我们要把一个点看成区间长度为1的区间修改。
代码:
#include
#define ll long long
using namespace std;
const int N = 5e5 + 5;
int tree[N] = {0}, n;
int lowbit(int x) {
return x & -x;
}
void add(int x, int k) {
while (x <= n) {
tree[x] += k;
x += lowbit(x);
}
}
int sum (int x) {
int sum = 0;
while (x > 0) {
sum += tree[x];
x -= lowbit(x);
}
return sum;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int m;
cin >> n >> m;
for (int i = 1; i <= n; i++) {
int c;
cin >> c;
add(i, c);
add(i + 1, -c);
}
while (m--) {
int flag;
cin >> flag;
if (flag == 1) {
int x, y, k;
cin >> x >> y >> k;
add(x, k);
add(y + 1, -k);
} else {
int c;
cin >> c;
cout << sum(c) << '\n';
}
}
return 0;
}
大量阅读后发现两种板子,个人觉得第一种写起来更加简洁而且我更好理解。
公共部分:单点修改和前缀和查询
int lowbit(int x) {
return x % -x;
}//最低位
void add(int x, int k) {
while (x <= n) {
tree[x] += k;
x += lowbit(x);
}
} //单点修改
int sum(int x) {
int sum = 0;
while (x > 0) {
sum += tree[x];
x -= lowbit(x);
}//前缀查询
return sum;
}
两种版本的思想都是一样的,本质都是离散化。
版本A:间接排序+从大到小
bool cmp(int x,int y) {
if(a[x] == a[y]) return x > y;
return a[x] > a[y];
}
int main() {
long long ans=0;
cin >> n;
for(int i = 1;i <= n; i++)
cin >> a[i], d[i] = i;
sort(d + 1,d + n + 1,cmp);
for(int i = 1;i <= n; i++) {
add(d[i]);
ans += sum(d[i] - 1);
}
cout << ans;
return 0;
}
解读:
所谓的逆序对或者是逆序数,呃呃,这个就是线性代数里面内容,不过逆序数也就是,冒泡排序所需要交换的次数。对于一个数的逆序我们应该怎么求?我们只需要看看前面有几个数比这个数大就可以了,这样我们就可以通过相邻两个数交换把这个数放到正确的位置,这也是冒泡排序的原理。
一个序列的逆序数是每一个数的逆序数之和。
我发现很多人都不喜欢讲清楚数组的含义,直接上代码,真心累。
这里a数组是原始数据,自始至终没有变动,d数组存的a数组的编号。
首先,通过排序。这里是间接排序,根据a数组内的大小来进行排序,貌似这里面有种指针的联系,尽管是两个数组,但是在排序的过程中始终都是有联系的。注意,这里相同元素的处理,因为我们这里是从大到小排序,所以对于相同的元素,我们取大的,后面讲。
然后,排序完之后,我们得到一个d的数组,里面的对于d[i] = x,i是代表这是第几大的数,x是a中对应数的下标。
我们首先拿第一大的数,更新该元素原来位置的下标x,x象征这原来数组的各个元素的位置,而i则是优先级体现。
然后,我们记录前缀和,注意,后面更新的数一定比前面更新的数要小!!!,所以说,只要前面存在更新过的点,只要前面有数,说明,比这个数大的数的位置在这个数前面!!!,那么这个数的逆序数就是区间0到x里面所有的数,也就是sum(x),但是这是个闭区间,我们也把x的存在放进去了,所以要减一。
依次求和,我们就得到了整个序列的逆序,long long!!!。
回答问题,如果说元素是一样大的话,我们默认,后面更新的比前面更新的要小,但是这是一样的,实际上两者之间没有逆序关系,如果说我们把标号小的放前面,会导致其被计入后面标号大的区间内,导致多计算了逆序数。
可以简单的概括为,求d数组内部的正序数,因为只要是正序的就意味着被计入下一个数的区间内。
大功告成。
B:结构体+从小到大
struct point
{
int num,val;
} a[500010];
bool cmp(point q,point w)
{
if(q.val == w.val)
return q.num < w.num;
return q.val < w.val;
}
int main()
{
scanf("%d",&n);
for(int i = 1;i <= n; i++)
scanf("%d", &a[i].val),a[i].num = i;
sort(a+1, a+1+n, cmp);
for(int i = 1; i <= n; i++)
ranks[a[i].num] = i;
for(int i = 1; i <= n; i++)
{
insert(ranks[i],1);
ans += i-query(ranks[i]);
}
printf("%lld",ans);
return 0;
}
这个感觉有点绕。
这里是从小到大排序,利用了结构体。
这里ranks数组里面存储的是对应下标的优先级ranks[i] = x这里的i才是下标,注意!。
首先放入原来数组下标为1的数的优先级。这里因为是第一个数,所以,它前面的数(含本身)为一。最小,优先级最高rank1
那么对于第i个数,它前面的数(含本身)共有i个数,我们要找出其中比它大的数,我们是减去其中比它小的数,query(ranks[i])。
第一个数更新的时候,这里的树状数组存的是优先级和上面的相反,比它优先级低(rank100,是值小的)的都会受到影响,因为会被包含在较低优先级的区间内部。
所以query查询的是在这个树前面优先级比它低的数(含本身),也就是比它小的数,相减得到。
同样,这里如果有相同的元素的话,小的放前面,rank会小,这样可以多计算一次,以便于和i同时增加相互抵消。否则i增加会带来麻烦。i表示下标!!。
一维的树状数组已经是二维的了,再增加一维可能就是三维的数据结构了,树状数组真心累。
可能不靠谱的想象,可以把二维树状数组看成,两个树状数组垂直正交构成的十字形的“树”。
二维树状数组里面存的是矩阵,我想里面应该也可以读取一维数组,也就是某一矩阵的某一行。不过这样就大材小用了,这里主要目的是为了求出前i行,前j列,的子矩阵之和。
板子:
单点更新。
void update(int i, int j, int num){
for(int x = i; x< first; x += lowbit(x))
for(int y = j; y < last; y += lowbit(y))
c[x][y] += num;
}
前缀求和:
int sum(int i, int j){
int s = 0;
for(int x = i; x > 0; x -= lowbit(x)) {
for(int y = j; y > 0; y -= lowbit(y)) {
s += c[x][y];
}
}
return s;
}
大概是这样的,三维空间里面大大小小的方块,下面投影的是原始的二维数据矩阵。
至此,我们有了三种不同类型的树状数组,前缀和类型,差分类型,最值类型,为什么花样这么多QAQ。
首先,如何维护最大值,肯定是不可能想前缀和和差分类型一样维护的,具有不同性质的树状数组我们就要保证区间性质的方法来维护。
板子:
单点更新(区间维护):
void add(int x) {
while (x <= n) {
tree[x] = a[x];
int lx = lowbit(x);
for (int i = 1; i < lx; i <<= 1) {
tree[x] = max(tree[x], tree[x-i]);
}
x += lowbit(x);
}
}
为什么这里需要一个for循环呢?如果说,我们直接单点tree[x] = max(tree[x], k),加lowbit逐级更新所有的父节点的的话。
会导致一个问题,就是,如果说我们修改的那个数的刚好的原来的最大值的话,而且,新的数比原来的数要小的话,这样就会导致,树状数组里面存了一个虚假的最大值,只是历史曾经存在过的最大值,但是并不是当前真实的最大值。
为了避免这个问题的出现,就不得不查看所有的不包含这个数的区间,但是庆幸的是,由于树状数组特殊的性质,能够更新到这个区间的子区间一定是某个数加上一个lowbit,所以我们只需要查看所有能够通过加上一个lowbit得到这个数的区间即可。
也就是这个数减去一些lowbit,而且也不需要减去所有的lowbit,因为树状数组里面的每一层的lowbit都是相同的,所以子节点的下标的lowbit一定比x要小。
区间查询:
int query(int x, int y)
{
int ans = 0;
while (y >= x)
{
ans = max(a[y], ans);
y--;//!!!
for (; y - lowbit(y) >= x; y -= lowbit(y))
ans = max(tree[y], ans);
}
return ans;
}
模板解读:我们要查询区间[x, y]上的最大值,tree[y]表示的是某个以a[y]为末端的区间,这个区间可能很长可能很短,但是,我们要求,这个区间不能超过x否则最大值就可能取到外面了。
如果减去lowbit还在区间内说明原来的区间长度小于[x, y],直接max综合求最大值,一直求到无法这么求为止,然后再减1取掉当前的元素求最大值,继续操作,依次类推。
由于lowbit的二进制特性,这样操作可以大大加快区间的检索。
练习1:hdoj 1754
批注:注意一下,memset函数其实和for循环的速度是一样的,这里不建议用,每次清空全部可能会超时。
注意了,这里数状数组里存的最值,函数输入的是下标。
下面是代码:
#include
using namespace std;
const int N = 2e5 + 5;
int a[N], tree[N], n;
int lowbit(int x) {
return x & -x;
}
void update(int x) {
while (x <= n) {
tree[x] = a[x];
for (int i = 1; i < lowbit(x); i <<= 1) {
tree[x] = max(tree[x], tree[x - i]);
}
x += lowbit(x);
}
}
int query(int x, int y) {
int ans = 0;
while (y >= x) {
ans = max(a[y], ans);
y--;
while (y - lowbit(y) >= x) {
ans = max(tree[y], ans);
y-= lowbit(y);
}
}
return ans;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int m;
while (cin >> n >> m) {
for (int i = 0; i <= n; i++) {
a[i] = 0;
tree[i] = 0;
}
for (int i = 1; i <= n; i++) {
cin >> a[i];
update(i);
}
while (m--) {
char ch;
int x, y;
cin >> ch >> x >> y;
if (ch == 'Q') {
cout << query(x, y) << '\n';
} else {
a[x] = y;//注意看,这里要先更改a数组
update(x);
}
}
}
return 0;
}
线段树拥有所有树状数组的具备的功能,但是树状数组不一定具备线段树的操作。
线段树和树状数组的类似之处,都是通过一维的的数组来表示树的数据结构。
胡乱分析:
1
10 11
100 101 110 111
1000 1001 1010 1011 1100 1101 1110 1111
首先用二进制可以发现,各个区间的标号之间的关系,当标号乘二时会得到左儿子,当标号乘二加一的时候会得到右儿子。
而且,每一层的可以存储的区间都是二的倍数增加,每一层的二进制数的位数是一样。因此,所有的标号都是存在对应区间的(如果有区间的话),这些标号是稠密的,而不是稀疏的。
因为,每层的二进制数的最大可能性是有限的,而且和当前层数的标号是一一对应的。
板子:
void build(int s, int t, int p) {//s是开始点,t是结束点,p是初始标号1
if (s == t) {//当区间长度为1的时候
d[p] = a[s];//直接将数组里的数存入
return;
}
int m = s + t >> 1;//取中间
build(s, m, p * 2);//左儿子,左儿子的下标是母节点的下标的两倍
build(m + 1, t, p * 2 + 1);//右儿子,右儿子的下标是母节点的两倍加1
d[p] = d[p * 2] + d[(p * 2) + 1];//递归,从叶子节点开始,逐层更新
}
树状数组的是单点修改加区间查询,或者是区间修改单点查询,实际上,树状数组还是只能单点修改,就算是区间修改,也不过是通过差分数组实现两端两点修改来模拟区间修改。