树状数组与线段树初步
树状数组与线段树初步
BIT(BinaryIndexedTree)
BIT的要求
普通的树状数组可维护的操作必须满足交换律和可差分性。
BIT能做什么
- 单点修改,区间和
- 区间修改,单点查询
- 区间修改,区间和
- 单点取max,前缀最大值
BIT的实现
首先,树状数组是一种基于二进制拆分的,支持单点修改,区间查询的数据结构。
对于二进制拆分,我们有:
\(15 = 2^3 + 2^2 + 2^1 + 2^0\)
我们发现这个拆分和其最低位的1以及其后面的0形成的数有关,所先定义:
\(\operatorname{lowbit}(x) = x \ \& \ -x\)
那么我们就可以设计:
\(c[x]\) 管辖区间 \([x - lowbit(x) + 1, x]\) 的前缀和。
然后我们不难注意到:若修改 \(a[p]\) 的值,\(c[p + lowbit(p)]\)、 \(c[(p + lowbit(p)) + lowbit(p + lowbit(p))]...\) 的值将会被影响。然后好像可以写代码了。
很幽默的一点是之前我好像不会BIT,甚至不会背BIT板子((
所以我选择把板子贴在这:
struct BIT {
int c[N];
void add(int x, int val) {
while (x <= n) {
c[x] += val;
x += x & -x;
}
return ;
}
int query(int x) {
int sum = 0;
while (x) {
sum += c[x];
x -= x & -x;
}
return sum;
}
};
以上代码实现了单点修改与区间求和的功能
例题:AT_dp_q Flowers
Solution
注意到这其实是带权的最长上升子序列。设 \(dp[i]\) 表示以 \(i\) 结尾的序列的美力值的最大值,于是不难写出转移方程:
\[dp[i] = a[i] + \max\{ dp[j] \mid j < i \text{ 且 } h[j] < h[i] \} \]
暴力转移的复杂度是 \(O(N^2)\),考虑优化。
按顺序扫描, \(j < i\) 自动满足。用树状数组维护按高度查询前缀最大值, \(O(N \log N)\) 解决。
Code
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 2e5 + 10;
int n;
struct BIT {
ll c[N];
void update(int x, ll val) {
while (x <= n) {
c[x] = max(val, c[x]);
x += x & -x;
}
return ;
}
ll query(int x) {
ll sum = 0;
while (x) {
sum = max(sum, c[x]);
x -= x & -x;
}
return sum;
}
};
BIT bit;
ll h[N], a[N];
int main() {
cin >> n;
for (int i = 1; i <= n; ++i)
cin >> h[i];
for (int i = 1; i <= n; ++i)
cin >> a[i];
ll ans = 0;
for (int i = 1; i <= n; ++i) {
ll tmp = bit.query(h[i] - 1) + a[i];
bit.update(h[i], tmp);
ans = max(ans, tmp);
}
cout << ans << '\n';
return 0;
}
复杂度
时间:\(O(N \log N)\)
空间:\(O(N)\)
SGT(SegmentTree)
SGT的要求以及SGT能做什么
线段树是个强力的维护区间信息的数据结构,BIT能做的它都能做,BIT不能做的它有的也能做((
SGT的实现
感觉挺好理解以及背诵,这边就不写了,放一个洛谷上线段树1的代码:
Code
struct SGT {
ll tr[N << 2], lzy[N << 2];
void build(int l, int r, int p) {
if (l == r) {
tr[p] = a[l];
return ;
}
int mid = (l + r) >> 1, ls = p << 1, rs = p << 1 | 1;
build(l, mid, ls);
build(mid + 1, r, rs);
tr[p] = tr[ls] + tr[rs];
}
void spread(int l, int r, int p) {
int mid = (l + r) >> 1, ls = p << 1, rs = p << 1 | 1;
tr[ls] += (mid - l + 1) * lzy[p], lzy[ls] += lzy[p];
tr[rs] += (r - mid) * lzy[p], lzy[rs] += lzy[p];
lzy[p] = 0;
}
ll query(int l, int r, int x, int y, int p) {
if (x <= l && r <= y) return tr[p];
int mid = (l + r) >> 1, ls = p << 1, rs = p << 1 | 1;
spread(l, r, p);
ll ans = 0;
if (x <= mid) ans += query(l, mid, x, y, ls);
if (y >= mid + 1) ans += query(mid + 1, r, x, y, rs);
return ans;
}
void update(int l, int r, int x, int y, int p, ll val) {
if (x <= l && r <= y) {
tr[p] += (r - l + 1) * val, lzy[p] += val;
return ;
}
spread(l, r, p);
int mid = (l + r) >> 1, ls = p << 1, rs = p << 1 | 1;
if (x <= mid) update(l, mid, x, y, ls, val);
if (y >= mid + 1) update(mid + 1, r, x, y, rs, val);
tr[p] = tr[ls] + tr[rs];
}
};
例题1:小白逛公园
Solution
不难发现这题实际上维护的是单点修改与区间最大子段和的查询。区间最大值很显然不能直接做的,我们考虑维护各个区间的总和,前缀最大值,后缀最大值,从而得到区间最大子段和。
Code
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 10;
int a[N];
// 考虑求区间最大子段和没有可合并性,故需要维护区间的前缀最大值,后缀最大值,以及区间和和最大子段和并并
struct SGT {
struct Node {
int lmax, rmax, sum, maxx;
} tr[N << 2];
Node merge(Node l, Node r) {
Node res;
res.sum = l.sum + r.sum;
res.lmax = max(l.lmax, l.sum + r.lmax);
res.rmax = max(r.rmax, r.sum + l.rmax);
res.maxx = max({l.maxx, r.maxx, l.rmax + r.lmax});
return res;
}
void build(int l, int r, int p) {
if (l == r) {
tr[p].sum = tr[p].lmax = tr[p].rmax = tr[p].maxx = a[l];
return ;
}
int mid = (l + r) >> 1, ls = p << 1, rs = p << 1 | 1;
build(l, mid, ls);
build(mid + 1, r, rs);
tr[p] = merge(tr[ls], tr[rs]);
}
// 在单点修改 x 的时候,所有包含 x 的区间都要对应修改,这些区间恰好是在定位 x 时遇到的区间
void update(int l, int r, int x, int val, int p) {
if (l == r) return tr[p].sum = tr[p].lmax = tr[p].rmax = tr[p].maxx = val, void();
int mid = (l + r) >> 1, ls = p << 1, rs = p << 1 | 1;
// spread(l, r, p);
if (x <= mid) update(l, mid, x, val, ls);
else update(mid + 1, r, x, val, rs);
tr[p] = merge(tr[ls], tr[rs]);
}
Node query(int l, int r, int x, int y, int p) {
if (x <= l && r <= y) return tr[p];
int mid = (l + r) >> 1, ls = p << 1, rs = p << 1 | 1;
if (y <= mid) return query(l, mid, x, y, ls);
if (x >= mid + 1) return query(mid + 1, r, x, y, rs);
Node le = query(l, mid, x, y, ls), ri = query(mid + 1, r, x, y, rs);
return merge(le, ri);
}
};
SGT sgt;
int n, m;
int main() {
cin >> n >> m;
for (int i = 1; i <= n; ++i)
cin >> a[i];
sgt.build(1, n, 1);
while (m--) {
int op, x, y; cin >> op >> x >> y;
if (op == 1) {
if (x > y) swap(x, y);
cout << sgt.query(1, n, x, y, 1).maxx << '\n';
} else if (op == 2)
sgt.update(1, n, x, y, 1);
}
return 0;
}
例题2: P4145 上帝造题的七分钟 2 / 花神游历各国
Solution
注意到 \(\sqrt1 = 1\),这等价于不进行操作。故用一个不需要下传的剪枝标记(待指正)标记出数字为 \(1\) 的段即可。
Code
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1e5 + 10;
ll a[N];
struct SGT {
ll tr[N << 2];
bool tag[N << 2];
void build(int l, int r, int p) {
if (l == r) return tr[p] = a[l], tag[p] = (a[l] == 1), void();
int mid = (l + r) >> 1, ls = p << 1, rs = p << 1 | 1;
build(l, mid, ls);
build(mid + 1, r, rs);
tr[p] = tr[ls] + tr[rs];
tag[p] = tag[ls] && tag[rs];
}
void update(int l, int r, int x, int y, int p) {
if (tag[p]) return ;
if (l == r) return tr[p] = sqrt(tr[p]), tag[p] = (tr[p] == 1), void();
int mid = (l + r) >> 1, ls = p << 1, rs = p << 1 | 1;
if (x <= mid) update(l, mid, x, y, ls);
if (y >= mid + 1) update(mid + 1, r, x, y, rs);
tr[p] = tr[ls] + tr[rs];
tag[p] = tag[ls] && tag[rs];
}
ll query(int l, int r, int x, int y, int p) {
if (x <= l && r <= y) return tr[p];
int mid = (l + r) >> 1, ls = p << 1, rs = p << 1 | 1;
ll ans = 0;
if (x <= mid) ans += query(l, mid, x, y, ls);
if (y >= mid + 1) ans += query(mid + 1, r, x, y, rs);
return ans;
}
};
SGT sgt;
int n, m;
int main() {
cin >> n;
for (int i = 1; i <= n; ++i)
cin >> a[i];
sgt.build(1, n, 1);
cin >> m;
while (m--) {
int op, l, r;
cin >> op >> l >> r;
if (l > r) swap(l, r);
if (op == 0) sgt.update(1, n, l, r, 1);
else cout << sgt.query(1, n, l, r, 1) << '\n';
}
return 0;
}

浙公网安备 33010602011771号