目录

板块一:树状数组

引子:lowbit

1、存入数据(单点修改)

2、区间查询

练习1:hdoj1541

3、区间修改和单点查询(差分数组)

练习1:hdoj 1556

练习2:洛谷P3368

4、求逆序对(两种版本)

练习题1:二次离散化+映射,求逆序对

5、二维的树状数组

6、树状数组求区间最大值

练习1:hdoj 1754

7、树状数组求第k大的数(???做到了再说)

板块二:线段树

前言:

1、建树:

2、区间修改+区间查询


板块一:树状数组

树状数组又加二进制搜索树(binary search tree)

据说树状数组写起来比线段树简单,不过对于初学者的博主来说还是很抽象。

这是一个非常神奇的数据结构,虽然是一维的数组但能存储树形的结构。

首先定义N为数组的长度,那么从1到N我们可以这样分类,从下往上,我们可以得到这样几种类型的数。

用我自己好理解的方式思考。

第零层:最多是2^0的倍数(奇数):*****1

第一层:最多是2^1的倍数***10

第二层:最多是2^2的倍数*****100

………………

首先,树状数组是完备的。奇数+2的倍数+ ………… = 奇数 + 偶数 = 所有下标。

这些数是有限的,因为有上界N,有种筛法的味道。

这些数字包含了所有可以引用的下标。

根据最低位数,我们将其分层。

引子:lowbit

为了方便索引树状数组里面的下标,我们构造一种函数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;
}

1、存入数据(单点修改)

数据结构首先它能存储数据,我们要如何存入数据?

其实存入数据也是一种单点修改的过程,就是把原来的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);}
}

2、区间查询

像台阶一样一直去掉最低位,一级级得到几项和,最终得到前几项和。

没时间画图。

板子:

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 <bits/stdc++.h>
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;
} 

3、区间修改和单点查询(差分数组)

终于悟了。

在使用区间修改的时候,我们的树状数组不再是普通的数组了,其实存储的是差分,是区间左端和区间右端加一的差值。

首先,对于某个区间而言,内部都加上或者减去一个量,区间内的差分不变,差值不变。

例如: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 <bits/stdc++.h>
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 <bits/stdc++.h>
#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;
}

4、求逆序对(两种版本)

大量阅读后发现两种板子,个人觉得第一种写起来更加简洁而且我更好理解。

公共部分:单点修改和前缀和查询

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表示下标!!。

练习题1:二次离散化+映射,求逆序对

首先要关注离散化后的数组的含义,数组的含义是第i大的数的编号,我们对两个数组都进行离散化处理,接下来,我们要把一个数组的优先级反映到另一个数组的。构建一个哈希表,讲同样是第i大的数的编号对应起来。从而每个数的标号都得到了对应,接下来枚举被排序的数组的标号,以另一个数组的标号为优先级,求逆序数,这里必须反着求逆序数,sum求出来的是优先级小的和,只有后面的数在前面的数,本应当放在被排序的数组里较前的位置的时候,后面的数居然优先级比它小就要加逆序数。反之,如果如果正着求的话,被排序数组后面的数本来就是在后面,而且优先级大,那么求出来的就是正序数。当然也可以向前面那样i-巴拉巴拉,不过这样写的话就不需要再sum的时候减一了。

#include <bits/stdc++.h>
#define p 99999997
using namespace std;
int n;
const int N = 1e5 + 5;
int a[N], b[N], c[N], mp[N], tree[N];
int lowbit(int x) {return x & -x;
}
void add(int x, int k) {while (x <= n) {tree[x] += k;x += lowbit(x);}
}
long long sum(int x) {long long sum = 0;while (x > 0) {sum += tree[x];x -= lowbit(x);}return sum;
}
bool cmp(int x, int y) {return a[x] > a[y];
}
int main() {ios::sync_with_stdio(false);cin.tie(0);long long ans = 0;cin >> n;for (int i = 1; i <= n; i++) {cin >> a[i];b[i] = i;}sort(b + 1, b + 1 + n, cmp);for (int i = 1; i <= n; i++) {cin >> a[i];c[i] = i;}sort(c + 1, c + 1 + n, cmp);for (int i = 1; i <= n; i++) {mp[c[i]] = b[i];}for (int i = n; i >= 1; i--) {add(mp[i], 1);ans += sum(mp[i] - 1) % p;
//    cout << mp[i] << ' ';}cout << ans % p << '\n';return 0;
}

5、二维的树状数组

一维的树状数组已经是二维的了,再增加一维可能就是三维的数据结构了,树状数组真心累。

可能不靠谱的想象,可以把二维树状数组看成,两个树状数组垂直正交构成的十字形的“树”。

二维树状数组里面存的是矩阵,我想里面应该也可以读取一维数组,也就是某一矩阵的某一行。不过这样就大材小用了,这里主要目的是为了求出前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;
}

大概是这样的,三维空间里面大大小小的方块,下面投影的是原始的二维数据矩阵。

6、树状数组求区间最大值

至此,我们有了三种不同类型的树状数组,前缀和类型,差分类型,最值类型,为什么花样这么多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 <bits/stdc++.h>
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;
}

7、树状数组求第k大的数(???做到了再说)

板块二:线段树

线段树拥有所有树状数组的具备的功能,但是树状数组不一定具备线段树的操作。

前言:

线段树和树状数组的类似之处,都是通过一维的的数组来表示树的数据结构。

胡乱分析:

                  110   11100    101   110     111
1000 1001 1010 1011 1100 1101 1110 1111

首先用二进制可以发现,各个区间的标号之间的关系,当标号乘二时会得到左儿子,当标号乘二加一的时候会得到右儿子。

而且,每一层的可以存储的区间都是二的倍数增加,每一层的二进制数的位数是一样。因此,所有的标号都是存在对应区间的(如果有区间的话),这些标号是稠密的,而不是稀疏的。

因为,每层的二进制数的最大可能性是有限的,而且和当前层数的标号是一一对应的。

1、建树:

板子:

void build(int s, int t, int p) {//s是开始点,t是结束点,p是初始标号1if (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);//右儿子,右儿子的下标是母节点的两倍加1d[p] = d[p * 2] + d[(p * 2) + 1];//递归,从叶子节点开始,逐层更新
}

2、区间修改+区间查询

树状数组的是单点修改加区间查询,或者是区间修改单点查询,实际上,树状数组还是只能单点修改,就算是区间修改,也不过是通过差分数组实现两端两点修改来模拟区间修改。

【学习笔记+习题集】(树状数组)(9473字)相关推荐

  1. 树状数组(Binary Index Tree)

    树状数组(Binary Index Tree, BIT)是用用数组来模拟树形结构.最简单的树状数组支持两种操作,时间复杂度均为 O ( log ⁡ ⁡ n ) O(\log⁡ n) O(log⁡n): ...

  2. 用树状数组解决求区间最值的问题:hdu1754

    以前都学过树状数组,但是已经差不多忘记了!不过看一看后,马上就都回忆起来了!而且感觉经过这么久的学习,对树状数组有了更深一层的领悟!个人觉得树状数组在本质上与线段树是没有区别的!都是管理区间,只不过树 ...

  3. b+树时间复杂度_前端大神用的学习笔记:线段树和树状数组

    全文篇幅较长,细心理解一定会有收获的♪(^∇^*). 1|0线段树 1|1一些概念     线段树是一种二叉搜索树,每一个结点都是一个区间(也可以叫作线段,可以有单点的叶子结点),有一张比较形象的图如 ...

  4. 1010 Lehmer Code (35 分)(思路+详解+树状数组的学习+逆序对+map+vector) 超级详细 Come baby!!!

    一:题目 According to Wikipedia: "In mathematics and in particular in combinatorics, the Lehmer cod ...

  5. 如此好的树状数组学习资料

    树状数组学习系列1 之 初步分析--czyuan原创 其实学树状数组说白了就是看那张图,那张树状数组和一般数组的关系的,看懂了基本就没问题了,推荐下面这个教程:http://www.topcoder. ...

  6. [算法学习] 线段树,树状数组,数堆,笛卡尔树

    都是树的变种,用途不同 [线段树 Interval Tree] 区间管理,是一种平衡树 可看做是对一维数组的索引进行管理.一维数组不需要是排序好的 深度不超过logL 任一个区间(线段)都分成不超过2 ...

  7. ACM学习历程—51NOD 1685 第K大区间2(二分 树状数组 中位数)

    http://www.51nod.com/contest/problem.html#!problemId=1685 这是这次BSG白山极客挑战赛的E题. 这题可以二分答案t. 关键在于,对于一个t,如 ...

  8. 【数据结构】树状数组笔记

    树状数组(Binary Indexed Tree, BIT) 本质上是按照二分对数组进行分组,维护和查询都是O(lgn)的复杂度 树状数组与线段树:树状数组和线段树很像,但能用树状数组解决的问题,基本 ...

  9. 0x42.数据结构进阶 - 树状数组

    目录 一.树状数组与逆序对 A.luogu P1908 逆序对(模板题) B.AcWing 241. 楼兰图腾 树状数组的拓展应用 1.区间加,求单点值 A.AcWing 242. 一个简单的整数问题 ...

最新文章

  1. 【Java 并发编程】线程锁机制 ( 悲观锁 | 乐观锁 | CAS 三大问题 | ABA 问题 | 循环时间长问题 | 多个共享变量原子性问题 )
  2. 用户输入和while循环
  3. android 音视频 教程,Android移动端音视频的快速开发教程(九)
  4. 4位16色灰度图像处理
  5. 黑马程序程序员基础测试(二)
  6. linux卸载mysql和myodbc_linux下卸载mysql rpm安装方式和源码安装方式的两种方法
  7. Linux du 命令
  8. python爬虫实例100例-python 爬虫实例
  9. ubuntu下cpu以最大频率运行、查看CPU主频几种方法
  10. BP神经网络算法基本原理,bp神经网络的算法步骤
  11. Labview_QMH模板解析
  12. 选股服务器 主站没有响应,通达信软件运行缓慢的解决办法
  13. Office - - Excel宏录制批量处理格式相同文件
  14. 【面经】国信证券数据清算工程师面经
  15. 【转】UAP studio基础使用技巧
  16. 推荐一款国产ECG心电芯片
  17. 【海康视频SDK】linux服务器端截图与下载视频
  18. java 同步数据,同步数据到另一个库中。
  19. 配置数据源(DataSource)
  20. 使用服务网格提升应用和网络安全

热门文章

  1. 几周的紧张的考试后的感想
  2. 配置docker镜像仓库
  3. Postgres-XL概述
  4. windows下安装运行redis(压缩包方式)
  5. c++数组定义与使用
  6. java程序员用代码表达喜欢你,程序员写三行情诗表达爱意 你造么?
  7. 怎么复习信息系统项目管理师?
  8. MATLAB静力学分析,[转载]Comsol 有限元静力分析
  9. kafka eagle安装与使用
  10. 如何恢复无法修复的Visual Studio 的破损文件