树状数组
基本概念
1、lowbit(x):取x二进制最低位的1
lowbit(x) = x & (-x)
eg : lowbit(1010) = 10
lowbit(1000) = 1000
2、和原始数组不同,树状数组存储的不是原始数据,而是原始数组中下标在一个区间的数之和。它像是前缀和和差分方法的优化。前缀和虽然可以 O(1) 进行区间求和,即查询操作,但是它的单次更新复杂度达到了 O(n) ;差分的更新为 O(1),而查询却为 O(n)。
3、对于树状数组 a[pos] ,它存储的是下标在区间 (pos-lowbit(pos), pos] 的数之和。(见下图)
也正是因为这点,树状数组在查询和更新时使用和 lowbit 相关的操作,实现了查询和更新 O(log n) 的复杂度。
单点更新、区间查询
单点更新
从图片中我们可以直观地感受到:对于x节点,最小的“管辖”它的节点(就是父亲节点,之前的想象力hh)应该是x+lowbit(x), 因此x+y,x+lowbit(x)节点也要+y,x+lowbit(x)+lowbit(x+lowbit(x))节点也要+y……
区间查询
要查询(0, x]内的和,由于x节点只“管辖”(x-lowbit(x), x],下一段应该是(x-lowbit(x)-lowbit(x-lowbit(x)), x-lowbit(x)],再下一段……,直到(0, c].
// 单点更新
void update(int x, ll y)
{
while (x <= n) // 表示维护区间的上限,不一定等于n
{
a[x] += y;
x += lowbit(x);
}
}
// 区间查询
ll getsum(int x)
{
ll res = 0;
while (x > 0)
{
res += a[x];
x -= lowbit(x);
}
return res;
}
求逆序对
先将原序列离散化,用 1 - n 代替原序列但不改变相对位置的大小关系。为什么要离散化?很多情况下我们要研究的是一个元素在序列中的性质,并不关心它的实际值,这时可以将其映射至 1,2,3,4,…这个自然数序列。求逆序对,我们只关心序列中两个数的偏序关系(大小关系),因此做离散化处理。
// 离散化: 用1-n替换原序列
cin >> n;
for(int i = 1; i <= n; i++){
cin >> data[i];
mp[data[i]] = i; // 记录该数在原序列中的位置
}
sort(data + 1, data + n + 1);
// 构造离散后的序列
for(int i = 1; i <= n; i++){
b[mp[data[i]]] = i; // 第i小的数在原序列中位置,将其映射为i
}
之后可以单点更新,统计答案。遍历时,t出现时,[1, t-1]还未出现的数能和t构成逆序对,这样数的个数为 t - 1 - getsum(t-1).此时树状数组a类似一个桶,维护的是一堆01,表示对应下标的数是否出现。
//单点更新
void update(int x){
while(x <= n){
a[x]++;
x += x & (-x);
}
}
for(int i = 1; i <= n; i++){
update(b[i]);
ans += b[i] - 1 - getsum(b[i] - 1);
}
维护区间最值(最大值为例)
不同于线段树,树状数组不保存外部节点(原数组信息)。虽然在维护区间和时可以通过查询获得,但是在维护区间最值时就无法做到了。区间 A 的 max 为 a,将其分为A1,A2两个区间后无法求出各个区间的 max 。(不满足区间减法)因此树状数组在维护区间最值时要保存原始数组。
单点更新
更新后,遍历直接相连的孩子节点,重新确定该节点的值。这里归纳一个性质:节点x的直接孩子为 x-i ,其中 i=2^k<lowbit(x) 。 可以和之前父亲节点的性质一起理解。
// A[]为原始数组
// 赋值操作: A[x] = y
void update(int x, int y) {
h[x] = y;
while (x <= N)
// 重新求解区间(x - lowbit(x), x]最大值
for (int i = 1; i < lowbit(x); i <<= 1) {
h[x] = max(h[x], h[x - i]);
}
x += 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(h[y], ans);
}
return ans;
}
区间修改、单点查询
通过“差分”,转化为单点更新区间查询。查询和修改函数部分的代码和后者相同。
a[0] = 0;
for(int i = 1; i <= n; i++){
cin >> a[i];
update(i, a[i] - a[i - 1]); // 差分
}
// 更新操作
cin >> c1 >> c2 >> d;
update(c1, d);
update(c2 + 1, -d);
// 查询操作
cin >> c1;
cout << get(c1) << '\n';
区间修改、区间查询
void update(int x, ll y)
{
int cur = x;
while (x <= n)
{
s1[x] += y;
s2[x] += y * (cur - 1);
x += x & (-x);
}
}
ll getsum(int x)
{
ll res = 0, cur = x;
while (x > 0)
{
res += cur * s1[x] - s2[x];
x -= x & (-x);
}
return res;
}
线段树
线段树简介
01 线段树功能非常强大,且拓展性也很强,通过各种操作的组合可以实现在O(log n)的时间内维护区间信息。
02 支持单点修改以及一些特殊的区间操作,包括区间修改(区间加、减、乘)、区间查询(区间求和、求区间最大值、求区间最小值)等。
03 线段树维护的信息,需要满足可加性,即能以可以接受的速度合并信息和修改信息。
线段树的建立
// d[now]维护原数组下标[l,r]的区间
void build(int now, int l, int r)
{
if (l == r)
{
tree[now] = a[l];
return;
}
int mid = (l + r) >> 1;
build(now * 2, l, mid);
build(now * 2 + 1, mid + 1, r);
tree[now] = tree[now * 2] + tree[now * 2 + 1];
// 维护最大值
// tree[now] = max(tree[now * 2], tree[now * 2 + 1]);
}
区间查询
ll query(int now, int l, int r, int ql, int qr)
{
ll sum = 0;
// 完整地包含在询问区间中的极大区间,若不则还要拆分
if (ql <= l && qr >= r)
{
sum += tree[now];
// 维护最大值
// res = max(res, tree[now]);
return sum;
}
pd(now, l, r);
int mid = (l + r) >> 1;
if (ql <= mid)
sum += query(now * 2, l, mid, ql, qr);
// 维护最大值,qr>mid情形同理
// res = max(res, query(now * 2, l, mid, ql, qr))
if (qr > mid)
sum += query(now * 2 + 1, mid + 1, r, ql, qr);
return sum;
}
单点修改
// 递归找到要修改的x位置
void update(int now, int l, int r, int x, int uy)
{
if (l == r)
{
tree[now] += uy;
// 维护最大值
// tree[now] = uy;
return;
}
int mid = (l + r) >> 1;
if (x <= mid)
update(now * 2, l, mid, x, uy);
else
update(now * 2 + 1, mid + 1, r, x, uy);
tree[now] = tree[now * 2] + tree[now * 2 + 1];
// tree[now] = max(tree[now * 2], tree[now * 2 + 1]);
}
区间修改(加)
void update(int now, int l, int r, int xl, int xr, int c)
{
if (xl <= l && xr >= r)
{
tree[now] += (r - l + 1) * c;
tag[now] += c;
return;
}
pd(now, l, r);
int mid = (l + r) >> 1;
if (xl <= mid)
update(now * 2, l, mid, xl, xr, c);
if (xr >= mid + 1)
update(now * 2 + 1, mid + 1, r, xl, xr, c);
tree[now] = tree[now * 2] + tree[now * 2 + 1];
}
懒标记优化
void pd(int now, int l, int r)
{
int ls = now * 2;
int lr = now * 2 + 1;
int mid = (l + r) >> 1;
tag[ls] += tag[now];
tag[lr] += tag[now];
tree[ls] += tag[now] * (mid - l + 1);
tree[lr] += tag[now] * (r - mid);
tag[now] = 0;
区间修改(乘)
void update1(int now, int l, int r, int xl, int xr, int c)
{
if (xl <= l && xr >= r)
{
tree[now] = tree[now] * c % p;
tag1[now] = tag1[now] * c % p;
tag2[now] = tag2[now] * c % p;
return;
}
pd(now, l, r);
int mid = (l + r) >> 1;
if (xl <= mid)
update1(now * 2, l, mid, xl, xr, c);
if (xr >= mid + 1)
update1(now * 2 + 1, mid + 1, r, xl, xr, c);
tree[now] = (tree[now * 2] + tree[now * 2 + 1]) % p;
}