😊😊 😊😊
不求点赞,只求耐心看完,指出您的疑惑和写的不好的地方,谢谢您。本人会及时更正感谢。希望看完后能帮助您理解算法的本质
😊😊 😊😊
题目描述:
一、差分 + 树状数组:
分析题意可知:题目要求我们进行两个操作,分别是
- 区间更新;
- 区间查询;
而对于这两个操作,从最常见的来看,区间查询操作,我们可以使用前缀和,树状数组,而选择哪一种,取决于题目是否有单点修改的操作,一般单点更新的前提下,首选 树状数组;实际上我都用的是树状数组,求前缀和也是!毕竟代码也不复杂
另外对于另一个操作,区间更新,虽然不是单点更新,但是直觉告诉我,就是树状数组;而对于区间更新,要使得区间里面的每个元素,都同时加上某个数的话,最快的做法,显然是差分!那么,不禁想到,既然要修改原数组,那么就等于修改它的差分数组,因为对于一个区间性修改而言,差分数组更适合,对于个别元素的修改,显然直接索引原数组进行修改即可!
所以说这里我们采用树状数组维护原数组的差分数组,从而达到区间更新的目的!
但是两个操作分开来看显然是没有问题的,但是这里你就要注意了,区间更新和区间查询分开来看,没有什么问题,但是两个操作是在同一个问题中,同一个对象,同一个序列。而区间查询之前针对的是原数组,区间更新针对的是原数组的差分数组。这就矛盾了,你想查询原数组的和,采用树状数组的求前驱方式:
int sum (int pos)
{
int res=0;
for (int i=pos; i > 0; i-=lowbit(i))
res += c[i];
return res;
}
而区间更新维护的是差分数组 b b b,这就很矛盾了,所以说,更新的是差分数组,而查询差分数组的话,得到的是原数组的某个元素更新之后的值,而不是区间和。所以这里我们需要考虑树状数组的维护的底层数组到底是谁!
void add (int pos, int w)
{
for (int i=pos; i <= n; i += lowbit(i))
c[i] += w;
}
既然可以由差分数组求前缀和得出原数组,而原数组更新的话效率太慢了,并且差分数组能够快速更新原数组,求一次前缀和是原数组的元素值,那假如我求两次差分数组的前缀和呢?那不就是原数组的前缀和了吗?
很显然,这就是我们要求的区间和,将各个
a
i
a_i
ai 相加,等价于各个恒等式右边的
b
i
b_i
bi 相加求和!但是很明显,直接求不好求,这样去推公式的话,也很不好推导,但是有的同学可能会说,这不是挺规律的吗?一共x项,那么总和就是:
竖着看
(
x
−
1
+
1
)
∗
b
1
+
(
x
−
2
+
1
)
∗
b
2
+
.
.
.
.
+
(
x
−
i
+
1
)
∗
b
i
+
.
.
.
(
x
−
x
+
1
)
b
x
(x-1+1)*b_1 + (x-2+1)*b_2 + ....+ (x-i+1)*b_i +...(x-x+1)b_x
(x−1+1)∗b1+(x−2+1)∗b2+....+(x−i+1)∗bi+...(x−x+1)bx
这是
1
−
x
的区间和
1-x的区间和
1−x的区间和,存在很明显的一个公式就是:
每项
b
i
b_i
bi要加多少项 =
(
x
−
i
+
1
)
∗
b
i
(x-i+1)*b_i
(x−i+1)∗bi;
若要求的是 3~6的和呢?你这个公式就不规律了!
所以说,所求对象没有问题,但是不好求!
直接求不行,那我就间接求呗:直接采用容斥原理里面的补集思想!
很明显,我们要求的是黑色部分,现在补了红色部分,可不可以这么思考:
黑色部分 = 整体部分 - 红色部分!从而间接求我们目标对象!
那么红色部分怎么求呢?
红色部分的规律很明显啦:
(
b
1
∗
1
+
b
2
∗
2
+
.
.
.
.
+
b
i
∗
i
)
+
.
.
.
b
x
∗
x
(b_1*1 + b_2*2 + .... + b_i*i) + ... b_x*x
(b1∗1+b2∗2+....+bi∗i)+...bx∗x;
求整体部分和:
我们可以按列来求啊,很明显啊,由于多加了一行,所以每一列都是
x
+
1
x+1
x+1 项,所以将每一列相加,不就是整体的和了吗??
故可得目标区域和为:
现在再来观察这个公式,不难发现有两部分需要我们求和,
一部分是:
b
i
∗
i
b_i * i
bi∗i;
另一部分是:
b
i
b_i
bi;
而一个树状数组只能维护一个序列,显然我们的树状数组最初维护的是一个
b
i
b_i
bi 序列。那么对于:
b
i
∗
i
b_i * i
bi∗i 序列该如何维护呢?所以另外开一个树状数组进行维护呗,即底层不再是:
b
i
b_i
bi,而是
b
i
∗
i
b_i * i
bi∗i;
但是我们不是有区间更新操作吗?我们在
b
i
b_i
bi 序列上进行了更新,那么
i
∗
b
i
i*b_i
i∗bi怎么办呢?那它怎么更新呢?哈哈,其实不受影响的,区间更新的是:
b
i
b_i
bi,把
b
i
b_i
bi换一下不就行了,比如:
b
1
+
3
b1 + 3
b1+3 ⇒
b
1
′
b_1'
b1′,这是在
b
i
b_i
bi序列里面,然后跳到我们的
b
i
∗
i
b_i * i
bi∗i序列里面,
(
b
1
+
3
)
∗
i
(b_1 + 3) * i
(b1+3)∗i ⇒ 项数不变呗,还是
i
i
i 项,但是变的是:
b
1
′
∗
i
b_1' * i
b1′∗i,只是数值变化了,是该乘以变化后的数值呗。所以该做法可行!
代码:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
LL a[N];
LL tr1[N], tr2[N];
int n, m;
int lowbit(int x)
{
return x & (-x);
}
void add (LL tr[], int pos, LL d)
{
for (int i=pos; i<=n; i += lowbit(i))
tr[i] += d;
}
LL sum(LL tr[], int pos)
{
LL res=0;
for (int i=pos; i>0; i -= lowbit(i))
res += tr[i];
return res;
}
LL query(int x)
{
return (x+1) * sum(tr1, x) - (sum(tr2, x));
}
int main()
{
scanf("%d%d", &n, &m);
for (int i=1; i <= n; i ++)
cin >> a[i];
for (int i=1; i <= n; i ++)
{
add (tr1, i, a[i]-a[i-1]);
add (tr2, i, (LL)(a[i]-a[i-1])*i);
}
while (m -- )
{
char op[2];
int l, r;
scanf("%s%d%d", &op, &l, &r);
if (*op == 'C'){ //更新!
int d;
cin >> d;
// += d;
add (tr1, l, d); add (tr2, l, (LL)l*d);
// -= d;
add (tr1, r+1, -d); add (tr2, r+1, (LL)(r+1)*(-d));
}
else {
cout << query(r) - query(l-1) << endl;
}
}
return 0;
}
二、线段树:
思路推敲代码 – 线段树的建立过程!
- 先建立线段树: b u i l d ( ) ; build(); build();
const int N = 1e5 + 10;
struct Node{
当前节点所在的区间的左右端点
int l, r;
当前节点的区间和,更新标记
LL sum, add;
}tr[4*N];
int n, m;
int w[N];
参数:当前节点的编号,当前节点所在区间的左右端点!
void build(int u, int l, int r)
{
当前节点为叶子节点:
if (l == r)
{
tr[u] = {l, r, w[r], 0};
return ;
}
否则:先把当前节点的左右区间端点存进去
tr[u] = {l,r};
划分出当前节点的中间节点,去递归建立左右子树!
int mid = l + r >> 1;
递归建立左子树:
build(u<<1, l, mid)
递归建立右子树:
build(u<<1|1, mid+1, r);
递归到了叶子节点,然后会开始回溯,可是由于叶子节点的区间和赋值为w[u];
则需要我们去更新其父节点的区间和 = 左右节点的区间和相加!
pushup(u);
}
- 实现
p
u
s
h
u
p
pushup
pushup 函数:
p u s h u p pushup pushup 的作用是:当某个子区间发生更新的时候,其包含它的父区间也必然会发生更新,所以说要向上更新呗!
比如:[2, 4]区间里的元素和增加了 15,那么我们本题涉及到了:查询某个区间的元素和,那么包含 [2, 4]的区间 [1, 6],则请问它的区间和,不需要更新吗?
void pushup(int root)
{
tr[root].sum = tr[root<<1].sum + tr[root<<1|1].sum
}
- 区间更新: m o d i f y ( ) modify () modify(),即要修改某个区间内的元素和,这里提前告知下,更新的是区间和,而不是什么单点更新,并且题目都告知了,是某个区间里面的所有元素都加上 d d d,那么加了多少个 d d d 呢?取决于区间的长度: r − l + 1 r-l + 1 r−l+1;等价于 整个区间和增加了 : ( r − l + 1 ) ∗ d (r-l+1)*d (r−l+1)∗d;
参数:当前的节点编号,所要更新的区间的左右端点!增加多少值!
void modify (int sum, int l, int r, int v)
{
如果当前节点所在的区间被目标区间所覆盖:
if (tr[u].l >= l && tr[u].r <= r)
{
tr[u].sum += (LL)(r-l+1)*v;
tr[u].add += v;
}
else
{
如果当前所在的节点是打过修改标记的,说明之前它的子节点并没有进行标记修改,
而现在又要处理其子节点了,子节点是因为之前节省时间没有去递归修改的,所以现在要
pushdown一下!临时修改!
pushdown(u);
int mid = tr[u].r + tr[u].l >> 1;
if (l <= mid) modify (u<<1, l, r, v);
if (r > mid) modify (u<<1|1, l, r, v);
pushup(u);
}
return ;
}
- 向下更新:
线段树的懒标记:本来我们要修改某段区间的,这段区间在某个节点上,然后该节点又藏得比较深,我们需要一个一个节点去找,就很累,所以我们可以在包含该目标节点的父节点上,打一个懒标记,然后下次路过的时候,再去修改它,避免重复的搜索!所以说处理懒标记的时候,才去传承处理!
每次处理完记得将之前放置的懒标记给标记为0!否则下次路过,本来已经解决了的事,又去解决一遍吗?
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;
}
}
- 区间查询:
和步骤4中的路过 ⇒ 处理一下,一样,查询的时候,路过懒标记的时候,记得顺便将它带走为:pushdown(u);其余时候就不断查询即可!
LL query(int u, int l, int r)
{
if (tr[u].l >= l && tr[u].r <= r)
{
return tr[u].sum; //返回当前的区间和!
}
//记得检查是否有过修改标记,有的话赶快去处理掉!
pushdown(u);
//局部变量存储当前状态到目标状态的答案!
LL sum=0;
int mid = (tr[u].l + tr[u].r) >> 1;
if (l <= mid) sum = query(u<<1, l, r);
if (r > mid) sum += query(u<<1|1, l, r);
return sum;
}
代码:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
struct Node{
//当前节点的区间的左右端点,而不是左右孩子编号,左右孩子编号可以由当前节点编号求得!
int l, r;
//当前区间和,以及当前区间需要更新多少!
LL sum, add;
}tr[N*4];
int n, m; //n个点m个查询!
int w[N];
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) //如果根节点的修改值不等于0的话!说明这里有改动!将其传递下去!
{
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[r], 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 += (LL)(tr[u].r - tr[u].l + 1)*v;
tr[u].add += v;
}
else
{
pushdown(u);
int mid = tr[u].r + tr[u].l >> 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) //查询的是区间和!
{
//如果插叙的区间已经覆盖了当前的节点,例如查询的是 [2, 6],当前节点的区间端点: [3, 4];
//所以说是满足条件的!则说明没必要再往下找了!
if (tr[u].l >= l && tr[u].r <= r) return tr[u].sum;
pushdown(u);
//LL sum将sum定义成了局部变量,也可以将其进行传参携带,但是不方便!
//[清华大学考研机试题,整数拆分]详解了递归中的局部变量的作用:
//记录的是从当前节点到达叶子节点的答案,本题答案求的是“区间和”,
//则记录的是从当前节点区间的和值。只不过因为将区间和划分到子节点里面去了,所以需要递归查找!
LL sum=0;
int mid = tr[u].l + tr[u].r >> 1; //求出当前区间的中点!
if (l <= mid) sum = query(u<<1, l, r);
if (r > mid) sum += query(u<<1|1, l, r);
return sum;
}
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, d;
while (m -- )
{
scanf ("%s%d%d", op, &l, &r);
//区间更新操作!
if (*op == 'C')
{
int d;
scanf ("%d", &d);
//根节点编号,更新的区间的做右端点,增加的权值!
modify (1, l, r, d);
}
//区间查询:
else
{
//输出查询的区间和!
cout << query(1, l, r) << endl;
}
}
return 0;
}
草稿:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
struct Node{
//当前节点所在的区间的左右端点
int l, r;
//当前节点的区间和,更新标记
LL sum, add;
}tr[4*N];
int n, m;
int w[N];
void pushup(int root)
{
tr[root].sum = tr[root<<1].sum + tr[root<<1|1].sum;
}
//参数:当前节点的编号,当前节点所在区间的左右端点!
void build(int u, int l, int r)
{
//当前节点为叶子节点:
if (l == r)
{
tr[u] = {l, r, w[r], 0};
return ;
}
//否则:先把当前节点的左右区间端点存进去
tr[u] = {l,r};
//划分出当前节点的中间节点,去递归建立左右子树!
int mid = (l + r) >> 1;
//递归建立左子树:
build(u<<1, l, mid);
//递归建立右子树:
build(u<<1|1, mid+1, r);
//递归到了叶子节点,然后会开始回溯,可是由于叶子节点的区间和赋值为w[u];
//则需要我们去更新其父节点的区间和 = 左右节点的区间和相加!
pushup(u);
}
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 modify (int u, int l, int r, int v)
{
//如果当前节点所在的区间被目标区间所覆盖:
if (tr[u].l >= l && tr[u].r <= r)
{
tr[u].sum += (LL)(tr[u].r - tr[u].l + 1)*v;
tr[u].add += v;
}
else
{
//如果当前所在的节点是打过修改标记的,说明之前它的子节点并没有进行标记修改,
//而现在又要处理其子节点了,子节点是因为之前节省时间没有去递归修改的,所以现在要
//pushdown一下!临时修改!
pushdown(u);
int mid = (tr[u].r + tr[u].l) >> 1;
if (l <= mid) modify (u<<1, l, r, v);
if (r > mid) modify (u<<1|1, l, r, v);
pushup(u);
}
return ;
}
LL query(int u, int l, int r)
{
if (tr[u].l >= l && tr[u].r <= r)
{
return tr[u].sum; //返回当前的区间和!
}
//记得检查是否有过修改标记,有的话赶快去处理掉!
pushdown(u);
//局部变量存储当前状态到目标状态的答案!
LL sum=0;
int mid = (tr[u].l + tr[u].r) >> 1;
if (l <= mid) sum = query(u<<1, l, r);
if (r > mid) sum += query(u<<1|1, l, r);
return sum;
}
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, d;
while (m -- )
{
scanf("%s%d%d", op, &l, &r);
if (*op == 'C')
{
scanf("%d", &d);
modify(1, l, r, d);
}
else printf("%lld\n", query(1, l, r));
}
return 0;
}