树状数组/分块/线段树
用树状数组完成 $O(\log n)$ 时间复杂度的
区间查询
+区间修改
直接上图
因此只需维护两个树状数组即可
一个是差分数组d[i]
的树状数组tr[i]
,还有一个是i*d[i]
的树状数组tri[i]
Code(树状数组)
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
int n, m;
int a[N];
LL tr[N], tri[N];
//tr[]数组是原始数组的差分数组d[i]的树状数组
//tri[]数组是原始数组的差分数组乘以i即i*d[i]的树状数组
int lowbit(int x)
{
return x & -x;
}
void add(LL c[], int x, int v)
{
for (int i = x; i <= n; i += lowbit(i))
c[i] += v;
}
LL query(LL c[], int x)
{
LL res = 0;
for (int i = x; i; i -= lowbit(i))
res += c[i];
return res;
}
//对应最后一步推导的公式
LL get_sum(int x)
{
return query(tr, x) * (x + 1) - query(tri, x);
}
int main()
{
scanf("%d%d", &n, &m);
//输入数组a[i]
for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
//先构造两个数组 d[i] 和 i*d[i]
for (int i = 1; i <= n; ++i)
tr[i] = a[i] - a[i - 1], tri[i] = tr[i] * i;
//原地 O(n) 建树状数组
for (int x = 1; x <= n; ++x)
for (int i = x - 1; i >= x - lowbit(x) + 1; i -= lowbit(i))
tr[x] += tr[i], tri[x] += tri[i];
//读入查询
while (m--)
{
char op[2];
int l, r, c;
scanf("%s", op);
if (op[0] == 'Q')
{
scanf("%d%d", &l, &r);
printf("%lld\n", get_sum(r) - get_sum(l - 1));
}
else
{
scanf("%d%d%d", &l, &r, &c);
add(tr, l, c), add(tr, r + 1, -c);
add(tri, l, l * c), add(tri, r + 1, (r + 1) * -c);
}
}
return 0;
}
Code(分块)$O(m\sqrt{n})$
#include <iostream>
#include <cstring>
#include <algorithm>
#include <cmath>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10, M = 350;
int n, m, len;
LL sum[M], flag[M]; //sum:块的总和;flag:块的懒标记
int w[N];
int get(int i)
{
return i / len;
}
void modify(int l, int r, int d)
{
if (get(l) == get(r)) //段内直接暴力
{
for (int i = l; i <= r; i ++ )
w[i] += d, sum[get(i)] += d;
}
else
{
int i = l, j = r;
while (get(i) == get(l)) w[i] += d, sum[get(i)] += d, i ++ ;
while (get(j) == get(r)) w[j] += d, sum[get(j)] += d, j -- ;
for (int k = get(i); k <= get(j); k ++ ) sum[k] += len * d, flag[k] += d;
}
}
LL query(int l, int r)
{
LL res = 0;
if (get(l) == get(r)) //段内直接暴力
{
for (int i = l; i <= r; i ++ )
res += w[i] + flag[get(i)];
}
else
{
int i = l, j = r;
while (get(i) == get(l)) res += w[i] + flag[get(i)], i ++ ;
while (get(j) == get(r)) res += w[j] + flag[get(j)], j -- ;
for (int k = get(i); k <= get(j); k ++ ) res += sum[k];
}
return res;
}
int main()
{
scanf("%d%d", &n, &m);
len = sqrt(n);
for (int i = 1; i <= n; i ++ )
{
scanf("%d", &w[i]);
sum[get(i)] += w[i];
}
char op[2];
int l, r, d;
while (m -- )
{
scanf("%s%d%d", op, &l, &r);
if (*op == 'C')
{
scanf("%d", &d);
modify(l, r, d);
}
else printf("%lld\n", query(l, r));
}
return 0;
}
线段树 $O(m\log n)$
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
typedef long long LL;
const int N = 1e5 + 10;
int n, m;
int w[N];
struct Node
{
int l, r;
LL sum, add;
}tr[N * 4];
void pushup(int u)
{
tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;
}
void pushdown(int u)
{
auto &root = tr[u], &left = tr[u << 1], &right = tr[u << 1 | 1];
if (root.add)
{
//传递懒标记,更新子树
left.add += root.add, left.sum += (LL) (left.r - left.l + 1) * root.add;
right.add += root.add, right.sum += (LL) (right.r - right.l + 1) * root.add;
//删除父结点懒标记
root.add = 0;
}
}
void build(int u, int l, int r)
{
if (l == r) tr[u] = {l, r, w[l], 0};
else
{
tr[u] = {l, r};
int mid = l + r >> 1;
build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
pushup(u);
}
}
void modify(int u, int l, int r, int v)
{
if (l <= tr[u].l && tr[u].r <= r)
{
tr[u].sum += (tr[u].r - tr[u].l + 1) * v;
tr[u].add += v;
}
else
{
pushdown(u);
int mid = tr[u].l + tr[u].r >> 1;
if (l <= mid) modify(u << 1, l, r, v);
if (r > mid) modify(u << 1 | 1, l, r, v);
pushup(u);
}
}
LL query(int u, int l, int r)
{
if (l <= tr[u].l && tr[u].r <= r) return tr[u].sum;
pushdown(u);
int mid = tr[u].l + tr[u].r >> 1;
LL v = 0;
if (l <= mid) v = query(u << 1, l, r);
if (r > mid) v += query(u << 1 | 1, l, r);
return v;
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i) scanf("%d", &w[i]);
build(1, 1, n);
char op[2];
int l, r, t;
while (m -- )
{
scanf("%s%d%d", op, &l, &r);
if (*op == 'Q') printf("%lld\n", query(1, l, r));
else
{
scanf("%d", &t);
modify(1, l, r, t);
}
}
return 0;
}
线段树为啥query函数里不需要pushup呢
询问不会做修改操作,所以不用pushup
下传懒标记是更新之前未更新的子区间,没有对区间进行修改
懂了,谢谢大佬
活捉大佬
活捉大佬
活捉俩大佬
看一下 OI-Wiki 上的讲解
活捉仨大佬
巨佬,好厉害,
一直有用理解懒标记有啥用
延迟更新:在某些情况下,为了提高更新的效率,可以使用延迟更新技巧。通过在节点上维护一个延迟标记,在需要查询或修改节点时再执行 pushdown 操作,将延迟的更新信息传递给子节点。
分块的解析可以看我这篇
orz
树状数组为什么不维护一个,非要维护两个,你那个公式不是可以合并吗?
我也想问这个,哥们你找到答案了吗
没找到
合并之后答案错误的
大佬太强了😭
树状数组的图,额外写成额了(
为什么要在modify那里加pushdown操作呢,在query查询时直接将需要的标记pushdown不可以吗
为什么要在modify()函数中分裂时加入pushdown()操作?
在进行下一次区间修改时且该区间是上次修改区间的子区间
如果在修改子区间时没有将上次区间的标记下传
那么子区间在修改后的值在做pushup()操作时会将上一次的修改值覆盖
然后导致区间修改的值错误
6666666666666
已经保存在我的笔记里了,大佬讲的真的好
一直不用理解懒标记有啥用
不需要用到大区间中的小区间就不管他,除非要修改这个小区间或者进入时才把懒标记传递给他
大佬,可以拿走做笔记吗,会标记出处
巨巨 为什么不需要分三种情况啊
可以分成这三种情况
与上面分的那两种是等价的
累加了。
同时这也是那个 v 要初始化成零的原因。
线段树query这句 if (l <= mid) v = query(u << 1, l, r);是不是应该是 if (l <= mid) v+ = query(u << 1, l, r);?两种写法都能过
初始是0,应该都能过
一语点醒梦中人,多谢
add(tri, l, l * c), add(tri, r + 1, (r + 1) * -c)
这里的 lc 和r+1c 不太理解啊 尽管知道上面的i*bi
idi[i]看成一个新数组,用来维护 i * d[i] 的值就好了
可以不用纠结于树状数组的写法,这题线段树才是经典做法
能说的详细点吗?我纠结了一天还是不明白,没搞明白总觉得缺点什么不踏实,谢谢
emmmm 我举一个简单的例子
现维护一个序列
a[i]
和ia[i]
,其中ia[i] = i * a[i]
现我们把
a[i]
中的某一个元素a[j] += k
,那么维护的ia[j] += j * k
即可这部分需要脱离出差分去看,比较好理解
我的理解:
比如t1、t2分别维护两个差分数组b[ i ]和ib[ i ]
首先要明确我们的目的是让t1[l ~ r]的值+c, 所以b[ l ] + c,即add(t1, l, c), 再让b[r + 1] - c, 即add(t1, r+1, -c); 单点修改之后t1[l ~ r]的值都会+c, 其他位置不变;
而由于b[ l ]和b[r + 1]发生改变,对应到ib[i]中:
对于点 l * b[ l ], 由于b[ l ] + c, l * b[ l ] => l * (b[ l ] + c) = l * b[ l ] + l * c,即比原来多了l * c
同理点(r+1) * b[r+1] 会变成 (r+1) * (b[r+1] -c) = (r+1)b[r+1] + (r+1) -c ,即比原来多了(r+1) * -c
这种变化仅仅只是因为b[i]变了导致的,为了维护t2做的处理, 变化后t2中除了l~r,r之后的元素也可能变,但这并没有关系,我们的目的不是让t2[l + r] + c, 而是t1[l ~ r] + c
di求和的实际意义是每一项的值,所以求和之后区间外的值不变,是必要的。
di’求和之后没有实际意义,就是i * di’的和,r后面的数变和不变,都没有实际意义,不用非要抵消。而每一项定义又是i * di’,所以如果变了,说明求和之后r之后的数就应该变。
豁然开朗!!
//原地 O(n) 建树状数组
for (int x = 1; x <= n; ++x)
for (int i = x - 1; i >= x - lowbit(x) + 1; i -= lowbit(i))
tr[x] += tr[i], tri[x] += tri[i];
树状数组初始化,这个代码怎么理解??
首先当前位置x之前的x-1个位置已经全部建立完成
考虑如何建立x位置的树状数组
根据树状数组的定义,下标x位置存储的是x值的二进制表达式中最低位的1所包含的前缀和
那么我们只需把该段前缀和全部加到x位置的值即可
谢谢%%%
该段的前缀和为什么要这么求呀
for (int i = x - 1; i >= x - lowbit(x) + 1; i -= lowbit(i)) tr[x] += tr[i], tri[x] += tri[i];
emmmmm这个就是回归树状数组的定义呀
更新的边界是
x - lowbit(x) + 1
也是定义吗对的,这是树状数组定义中划分的区间
你这样写,时间复杂度怎么可能是O(n)。 你要再开一个数组,来存前缀和, 把里面那个求区间和的复杂度降成O(1), 这样才是O(n)
参考向下建堆时间复杂度证明
统计一下每个数加的次数都知道这样的复杂度肯定是nlog(n)的
你这个跟向下建堆有本质区别
呃呃呃
这个才是O(n)的吧,您的写法还是以“全0数组为基础,依次插入的待初始化的数据”的思想进行初始化的,复杂度为O(log(n))。