本文章是作者调了3个多小时后,感触颇深,历经1个多小时写出来的,不喜勿喷
相关内容可参考 模板总结 中的树状数组及线段树;
进入正题
树状数组
所谓树状数组,即像树的数组,一般应用于求多次前缀和以及区间前缀和的问题中;
其根据节点编号的二进制数的末尾0的个数划分层次,每个节点的管辖范围为2^k,其中k为此节点的二进制数末尾0的个数,并用lowbit实现跳父亲的操作;
所谓lowbit,就是一个数的二进制的从右往左数第一个1出现的位置和这个1右面所有0构成的数,如6 == 110, 则lowbit(6) == 10 == 2; 6 + lowbit(6) == 8 == 1000,则节点8是节点6的父亲(父亲由其儿子数值加和转移而来,具体见下图);
树状数组的主要题目有三类:
1.单点修改,区间查询(树状数组操作的基础,后面的2个操作都是由它转变而来);
对于单点修改,我们只需先修改它自己,然后一步步修改其父亲即可;
对于区间查询,根据上面的思路,我们可以知道,树状数组可以查询1i的区间和,如果要查询xy的区间和,只需用(1 ~ y) - (1 ~ x - 1)即可;
点击查看代码
#include <iostream>
#include <string>
using namespace std;
int n, m;
int a[1000005];
int t[1000005];
string s;
int lowbit(int x) {
return x & (-x);
}
void add_dian(int x, int k) { //对第x个数加k(单点修改);
while(x <= n) {
t[x] += k;
x += lowbit(x);
}
}
int ask_he(int l, int r) { //查询区间和(区间查询);
int ans = 0;
int i = l - 1;
while(i > 0) {
ans -= t[i];
i -= lowbit(i);
}
i = r;
while(i > 0) {
ans += t[i];
i -= lowbit(i);
}
return ans;
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
for (int i = 1; i <= n; i++) {
add_dian(i, a[i]);
}
cin >> m;
int c, d;
for (int i = 1; i <= m; i++) {
cin >> s;
cin >> c >> d;
if (s == "ADD") {
add_dian(c, d);
}
if (s == "SUM") {
cout << ask_he(c, d) << endl;
}
}
return 0;
}
2.区间修改,单点查询;
对于区间修改,我们如果遍历修改的话会TLE,所以我们要维护原数组的差分数组b,每次修改区间(x, y)时只需修改x 和 y + 1的值即可(将b[x] + k, b[y + 1] - k);
对于单点查询i,我们只需求b的前缀和(1 ~ i)即可;
记得开long long!
点击查看代码
#include <iostream>
#include <string>
using namespace std;
int n, m;
string s;
int a[1000005];
int b[1000005]; //a的差分数组;
int t[1000005]; //维护b的树状数组;
int lowbit(int x) {
return x & (-x);
}
void add_dian(int x, int k) {
while(x <= n) {
t[x] += k;
x += lowbit(x);
}
}
//如要进行区间修改,只需将起点加k,终点 + 1减k(差分数组);
long long ask_dian(int x) { //输出点(a数组中,x为位置);
long long ans = 0;
while(x > 0) {
ans += t[x];
x -= lowbit(x);
}
return ans;
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
for (int i = 1; i <= n; i++) {
b[i] = a[i] - a[i - 1];
}
for (int i = 1; i <= n; i++) {
add_dian(i, b[i]);
}
cin >> m;
int c, d, e;
for (int i = 1; i <= m; i++) {
cin >> s;
if (s == "ADD") {
cin >> c >> d >> e;
add_dian(c, e);
add_dian(d + 1, -e);
}
if (s == "QUERY") {
cin >> c;
cout << ask_dian(c) << endl;
}
}
return 0;
}
3.区间修改,区间查询;
这个比较麻烦,需要推公式,个人感觉最好用线段树做;
公式的推导:
设T(n) 为原数组的前缀和,S(n)为差分数组的前缀和;
则:
\[T(n) == S(1) + S(2) + ... + S(n) \]\[== b1 + (b1 + b2) + (b1 + b2 + b3) + ... + (b1 + b2 + ... + bn) \]\[== nb1 + (n - 1)b2 + ... + bn \]\[== (n + 1)(b1 + b2 + ... + bn) - (b1 + 2b2 + 3b3 + ... + nbn) \]\[== (n + 1)\sum^{i}_{i <= n} bi - \sum^i_{i <= n} i * bi \]所以,我们只需维护两个树状数组(差分数组的和i*差分数组的)即可;
点击查看代码
#include <iostream>
#include <string>
#include <cstring>
using namespace std;
int n, m;
string s;
int a[100005];
int b[100005]; //a的差分数组;
long long t[100005]; //维护b的树状数组;
long long t1[100005]; //
int lowbit(int x) {
return x & (-x);
}
void add_dian(int x, long long k) {
long long val = 1ll * k * x; //(x + a[i]) * y 和 a[i] * y 差 x * y;
while(x <= n) {
t[x] += k;
t1[x] += val;
x += lowbit(x);
}
}
void add_dian1(int x, long long k) {
while(x <= n) {
t1[x] += k;
x += lowbit(x);
}
}
//如要进行区间修改,只需将起点加k,终点减k(差分数组);
long long ask_sum(int x) {
long long ans = 0;
while(x > 0) {
ans += t[x];
x -= lowbit(x);
}
return ans;
}
long long ask_sum1(int x) {
long long ans = 0;
while(x > 0) {
ans += t1[x];
x -= lowbit(x);
}
return ans;
}
long long ask(int l, int r) {
long long ans1 = ask_sum(r) * (r + 1) - ask_sum(l - 1) * l; //根据公式(x + 1) * ask_sum(x) - ask_sum1(x)得来(只不过前面的x + 1分开了)(以前是r 和 l - 1,因为公式(x + 1),所以是(r + 1) 和 l;
long long ans2 = ask_sum1(r) - ask_sum1(l - 1);
return ans1 - ans2;
}
int main() {
memset(t, 0, sizeof(t));
memset(t1, 0, sizeof(t1));
memset(a, 0, sizeof(a));
memset(b, 0, sizeof(b));
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
for (int i = 1; i <= n; i++) {
b[i] = a[i] - a[i - 1];
}
for (int i = 1; i <= n; i++) {
add_dian(i, b[i]);
}
cin >> m;
int c, d;
long long e;
for (int i = 1; i <= m; i++) {
cin >> s;
if (s == "ADD") {
cin >> c >> d >> e;
add_dian(c, e);
add_dian(d + 1, -e);
}
else {
cin >> c >> d;
cout << ask(c, d) << endl;
}
}
return 0;
}
最后,附上例题及题解,辅助理解;
线段树
线段树是一棵二叉搜索树,它支持上述树状数组的全部操作,并支持树状数组外的其它例如求区间最值等的操作,既支持在线操作,也支持离线操作;
线段树与树状数组对比:
优点:线段树适用范围广,当你正确写出板子时,其它操作就变得很简单;
缺点:板子代码量大,容易打错且不易察觉;
常数比树状数组大;
查错麻烦(由于使用递归形式建树及更新值);
线段树的每个点存储的是一段区间的信息,每个节点的编号是普通二叉树的编号;
所以,对于一个节点x,其左儿子的编号为x << 1;其右儿子的编号为(x << 1) + 1(或者写成更快的位运算x << 1 | 1,因为x << 1为偶数,x << 1 | 1就相当于(x << 1) + 1;
设节点x管辖区间为(l, r),则定义左儿子管辖区间为(l, mid),右儿子管辖区间为(mid + 1, r);
对于一棵二叉树,我们通常使用递归形式建树,先从根节点出发分别递归左右儿子,最后回溯时更新值;
线段树同理,我们使用递归形式建树,先从根节点出发分别递归左右儿子,最后回溯时更新值(一般为区间最值和区间和);
首先开一个结构体储存该点的区间左右端点,以及区间最值和区间和;
点击查看代码
struct sss{
int l, r, ma, sum; //左端点,右端点,区间最大值,区间和;
}tr[50000005]; //要开至少4倍空间,防炸;
建树部分代码:
点击查看代码
void bt(int id, int l, int r) {
tr[id].l = l;
tr[id].r = r;
if (l == r) {
tr[id].ma = a[l];
tr[id].sum = a[l];
return;
}
int mid = (l + r) >> 1;
bt(ls(id), l, mid);
bt(rs(id), mid + 1, r);
tr[id].sum = tr[ls(id)].sum + tr[rs(id)].sum;
tr[id].ma = max(tr[ls(id)].ma, tr[rs(id)].ma);
}
建完了树,开始操作;
1.单点修改,区间查询;
这个挺简单;
点击查看代码
#include <iostream>
#include <string>
using namespace std;
int n, m;
string s;
int a[100005];
struct sss{
int l, r, ma, sum; //左端点,右端点,区间最大值,区间和;
}tr[50000005]; //要开4倍空间;
int ls(int x) { //x的左孩子编号;
return x << 1;
}
int rs(int x) { //x的右孩子编号;
return x << 1 | 1;
}
void bt(int id, int l, int r) {
tr[id].l = l;
tr[id].r = r;
if (l == r) {
tr[id].ma = a[l];
tr[id].sum = a[l];
return;
}
int mid = (l + r) >> 1;
bt(ls(id), l, mid);
bt(rs(id), mid + 1, r);
tr[id].sum = tr[ls(id)].sum + tr[rs(id)].sum;
tr[id].ma = max(tr[ls(id)].ma, tr[rs(id)].ma);
}
void add_dian(int id, int x, int k) { //单点修改,将位置为x的数加上k(id为现在找到的点编号);
if (tr[id].l == tr[id].r) {
tr[id].ma = k;
tr[id].sum += k; //加k操作;
return;
}
int mid = (tr[id].l + tr[id].r) >> 1;
add_dian(x <= mid ? ls(id) : rs(id), x, k); //看看x在左子树还是右子树(注意中点在左子树);
tr[id].sum = tr[ls(id)].sum + tr[rs(id)].sum; //更新每个sum值;
tr[id].ma = max(tr[ls(id)].ma, tr[rs(id)].ma);
}
int ask_sum(int id, int l, int r) { //查询区间(l, r)的和(id为现在找的点的编号);
if (tr[id].l >= l && tr[id].r <= r) {
return tr[id].sum; //如果区间正好被包含就不用找了;
}
int mid = (tr[id].l + tr[id].r) >> 1;
if (r <= mid) return ask_sum(ls(id), l, r);
else if (l > mid) return ask_sum(rs(id), l, r);
else return ask_sum(ls(id), l, mid) + ask_sum(rs(id), mid + 1, r);
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
if (n != 0) bt(1, 1, n);
cin >> m;
int k, d;
for (int i = 1; i <= m; i++) {
cin >> s;
cin >> k >> d;
if (s == "ADD") {
add_dian(1, k, d);
}
else if (s == "SUM") {
cout << ask_sum(1, k, d) << endl;
}
}
return 0;
}
2.区间修改,区间查询及单点查询;
对于区间修改,如果要全部修改的话,从该区间开始要一直修改到根,会TLE;
怎么办呢?
我们引入一个新的东西:懒惰标记;
所谓懒惰标记,即在更新时只更新到管辖此更新区间的子树的根,并将此根的懒惰标记 += 要更新的值,等查询时在下放标记更新它的子节点,这样就可以减少许多不必要的操作,进而避免TLE;
具体更新操作见下面的题;
对于后面的查询,单点查询可以理解成区间长度为1的查询,每次查询从mid递归寻找原区间,最后将答案合并即为最终答案;
点击查看代码
#include <iostream> //要开long long, long long, long long!!!!!!!!!!!!!!!!!!!;
#include <string>
using namespace std;
int n, m;
string s;
long long a[10000005];
struct sss{
int l, r;
long long ma, sum, lazy; //左端点,右端点,区间最大值,区间和,懒惰标记;long long, long long, long long!!!!!!!!!!!!!!!!!!!!!!!!!;
}tr[50000005]; //要开4倍空间;
int ls(int x) { //x的左孩子编号;
return x << 1;
}
int rs(int x) { //x的右孩子编号;
return x << 1 | 1;
}
void bt(int id, int l, int r) {
tr[id].l = l;
tr[id].r = r;
if (l == r) {
tr[id].ma = a[l];
tr[id].sum = a[l];
return;
}
int mid = (l + r) >> 1;
bt(ls(id), l, mid);
bt(rs(id), mid + 1, r);
tr[id].sum = tr[ls(id)].sum + tr[rs(id)].sum;
tr[id].ma = max(tr[ls(id)].ma, tr[rs(id)].ma);
}
void push_down(int id) { //下放标记;
tr[ls(id)].lazy += tr[id].lazy;
tr[rs(id)].lazy += tr[id].lazy;
tr[ls(id)].sum += tr[id].lazy * (tr[ls(id)].r - tr[ls(id)].l + 1);
tr[rs(id)].sum += tr[id].lazy * (tr[rs(id)].r - tr[rs(id)].l + 1); //sum存储区间总和;
tr[id].lazy = 0;
}
void add(int id, int a, int b, int l, int r, int k) { //区间修改,将区间(l, r)加k;
if (l > b || r < a) return;
if (a >= l && b <= r) {
tr[id].sum += k * (b - a + 1);
tr[id].lazy += k;
return;
}
int mid = (a + b) >> 1; //以修改区间为基准,找被修改区间(原区间);
push_down(id);
add(ls(id), a, mid, l, r, k);
add(rs(id), mid + 1, b, l, r, k);
tr[id].sum = tr[ls(id)].sum + tr[rs(id)].sum;
}
long long ask_he(int id, int a, int b, int l, int r) { //区间(l, r)查询和;
if (a > r || b < l) return 0;
if (a >= l && b <= r) return tr[id].sum;
int mid = (a + b) >> 1; //同上;
push_down(id);
return ask_he(ls(id), a, mid, l, r) + ask_he(rs(id), mid + 1, b, l, r);
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
if (n != 0) bt(1, 1, n);
cin >> m;
int k, d, c;
for (int i = 1; i <= m; i++) {
cin >> s;
if (s == "ADD") {
cin >> k >> d >> c;
add(1, 1, n, k, d, c);
} else if (s == "QUERY") {
cin >> k;
cout << ask_he(1, 1, n, k, k) << endl;
}
}
return 0;
}
点击查看代码
#include <iostream> //要开long long, long long, long long!!!!!!!!!!!!!!!!!!!;
#include <string>
using namespace std;
int n, m;
string s;
long long a[10000005];
struct sss{
int l, r;
long long ma, sum, lazy; //左端点,右端点,区间最大值,区间和,懒惰标记;long long, long long, long long!!!!!!!!!!!!!!!!!!!!!!!!!;
}tr[50000005]; //要开4倍空间;
int ls(int x) { //x的左孩子编号;
return x << 1;
}
int rs(int x) { //x的右孩子编号;
return x << 1 | 1;
}
void bt(int id, int l, int r) {
tr[id].l = l;
tr[id].r = r;
if (l == r) {
tr[id].ma = a[l];
tr[id].sum = a[l];
return;
}
int mid = (l + r) >> 1;
bt(ls(id), l, mid);
bt(rs(id), mid + 1, r);
tr[id].sum = tr[ls(id)].sum + tr[rs(id)].sum;
tr[id].ma = max(tr[ls(id)].ma, tr[rs(id)].ma);
}
void push_down(int id) { //下放标记;
tr[ls(id)].lazy += tr[id].lazy;
tr[rs(id)].lazy += tr[id].lazy;
tr[ls(id)].sum += tr[id].lazy * (tr[ls(id)].r - tr[ls(id)].l + 1);
tr[rs(id)].sum += tr[id].lazy * (tr[rs(id)].r - tr[rs(id)].l + 1); //sum存储区间总和;
tr[id].lazy = 0;
}
void add(int id, int a, int b, int l, int r, int k) { //区间修改,将区间(l, r)加k;
if (l > b || r < a) return;
if (a >= l && b <= r) {
tr[id].sum += k * (b - a + 1);
tr[id].lazy += k;
return;
}
int mid = (a + b) >> 1; //以修改区间为基准,找被修改区间(原区间);
push_down(id);
add(ls(id), a, mid, l, r, k);
add(rs(id), mid + 1, b, l, r, k);
tr[id].sum = tr[ls(id)].sum + tr[rs(id)].sum;
}
long long ask_he(int id, int a, int b, int l, int r) { //区间(l, r)查询和;
if (a > r || b < l) return 0;
if (a >= l && b <= r) return tr[id].sum;
int mid = (a + b) >> 1; //同上;
push_down(id);
return ask_he(ls(id), a, mid, l, r) + ask_he(rs(id), mid + 1, b, l, r);
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
if (n != 0) bt(1, 1, n);
cin >> m;
int k, d, c;
for (int i = 1; i <= m; i++) {
cin >> s;
cin >> k >> d;
if (s == "ADD") {
cin >> c;
add(1, 1, n, k, d, c);
} else if (s == "SUM") {
cout << ask_he(1, 1, n, k, d) << endl;
}
}
return 0;
}
总模板
点击查看代码
#include <iostream> //要开long long, long long, long long!!!!!!!!!!!!!!!!!!!;
#include <string>
using namespace std;
int n, m;
string s;
long long a[10000005];
struct sss{
int l, r;
long long ma, sum, lazy; //左端点,右端点,区间最大值,区间和,懒惰标记;long long, long long, long long!!!!!!!!!!!!!!!!!!!!!!!!!;
}tr[50000005]; //要开4倍空间;
int ls(int x) { //x的左孩子编号;
return x << 1;
}
int rs(int x) { //x的右孩子编号;
return x << 1 | 1;
}
void bt(int id, int l, int r) {
tr[id].l = l;
tr[id].r = r;
if (l == r) {
tr[id].ma = a[l];
tr[id].sum = a[l];
return;
}
int mid = (l + r) >> 1;
bt(ls(id), l, mid);
bt(rs(id), mid + 1, r);
tr[id].sum = tr[ls(id)].sum + tr[rs(id)].sum;
tr[id].ma = max(tr[ls(id)].ma, tr[rs(id)].ma);
}
void add_dian(int id, int x, int k) { //单点修改,将位置为x的数加上k(id为现在找到的点编号);
if (tr[id].l == tr[id].r) {
tr[id].ma = k;
tr[id].sum += k; //加k操作;
return;
}
int mid = (tr[id].l + tr[id].r) >> 1;
add_dian(x <= mid ? ls(id) : rs(id), x, k); //看看x在左子树还是右子树(注意中点在左子树);
tr[id].sum = tr[ls(id)].sum + tr[rs(id)].sum; //更新每个sum值;
tr[id].ma = max(tr[ls(id)].ma, tr[rs(id)].ma);
}
long long ask_sum(int id, int l, int r) { //查询区间(l, r)的和(id为现在找的点的编号);
if (tr[id].l >= l && tr[id].r <= r) {
return tr[id].sum; //如果区间被包含就不用找了;
}
int mid = (tr[id].l + tr[id].r) >> 1;
if (r <= mid) return ask_sum(ls(id), l, r);
else if (l > mid) return ask_sum(rs(id), l, r);
else return ask_sum(ls(id), l, mid) + ask_sum(rs(id), mid + 1, r);
}
void push_down(int id) { //下放标记;
tr[ls(id)].lazy += tr[id].lazy;
tr[rs(id)].lazy += tr[id].lazy;
tr[ls(id)].sum += tr[id].lazy * (tr[ls(id)].r - tr[ls(id)].l + 1);
tr[rs(id)].sum += tr[id].lazy * (tr[rs(id)].r - tr[rs(id)].l + 1); //sum存储区间总和;
tr[id].lazy = 0;
}
void add(int id, int a, int b, int l, int r, int k) { //区间修改,将区间(l, r)加k;
if (l > b || r < a) return;
if (a >= l && b <= r) {
tr[id].sum += k * (b - a + 1);
tr[id].lazy += k;
return;
}
int mid = (a + b) >> 1; //以修改区间为基准,找被修改区间(原区间);
push_down(id);
add(ls(id), a, mid, l, r, k);
add(rs(id), mid + 1, b, l, r, k);
tr[id].sum = tr[ls(id)].sum + tr[rs(id)].sum;
}
long long ask_he(int id, int a, int b, int l, int r) { //区间(l, r)查询和;
if (a > r || b < l) return 0;
if (a >= l && b <= r) return tr[id].sum;
int mid = (a + b) >> 1; //同上;
push_down(id);
return ask_he(ls(id), a, mid, l, r) + ask_he(rs(id), mid + 1, b, l, r);
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
if (n != 0) bt(1, 1, n);
cin >> m;
int k, d, c;
for (int i = 1; i <= m; i++) {
cin >> s;
if (s == "ADD") {
cin >> k >> d >> c;
add(1, 1, n, k, d, c);
} else if (s == "QUERY") {
cin >> k;
cout << ask_he(1, 1, n, k, k) << endl;
}
}
return 0;
}
还有一篇写的不错的博客,可以参考参考(里面的图画的非常详细);、
线段树这东西很难调,一定一定要自己静下心来调;
标签:树状,int,线段,tr,long,详解,ls,sum,id From: https://www.cnblogs.com/PeppaEvenPig/p/18020355