差分数组 and 树上差分
顾z
2018-09-20 08:18:47
## 差分数组
### 定义
百度百科中的[差分定义](https://baike.baidu.com/item/%E5%B7%AE%E5%88%86/10349967?fr=aladdin)
//其实这完全和要讲的没关系 qwq
进去看了之后是不是觉得看不懂?
那我简单概括一下qwq
**差分数组de定义:**记录当前位置的数与上一位置的数的差值.
**栗子**
![](https://i.loli.net/2018/09/20/5ba2e0084887e.png)
容易发现的是,**$\sum_{j=1}^{i} b_j$即代表$a_i$ 的值.** $(\sum$ 即代表累加.)
### 思想
看到前面的$\sum$ 你一定会发现这是前缀和!
那你认为这是前缀和? 的确是qwq.
实际上这并不是真正意义上的前缀和.
**前缀和的思想**是 根据元素与元素之间的并集关系(和的关系),**求出某些元素的和的值**.对应的为$\sum_{j=1}^{i} a_j $
而差分的思想与此不同.
**差分的思想**是 根据元素与元素之间的逻辑关系(大小关系),**求出某一位置元素的值**.对应的为$\sum_{j=1}^{i} b_j$
What?不懂?看下面
继续捡起刚刚的栗子.
![](https://i.loli.net/2018/09/20/5ba33b02b939d.png)
有没有发现不同之处 ( • ̀ω•́ )✧
### 差分数组有什么用?
先看一道题,
>有n个数。
>m次操作,每一次操作,给定l,r,del.将l~r区间的所有数增加del;
>最后有q个询问,给你 l,r ,每一次询问求出l~r的区间和。
>PS: 先进行m个修改操作,后进行查询操作.
如果你是一个巨佬,你会"woc,线段树裸题!" "woc,树状数组裸题!"
然而,今天我要BB的不是这些东西.
是**差分数组的运用**!
有没有想法? 没有的话那就我来~~有也是我来~~
考虑我们差分数组记录的是什么,它记录的是当前位置的数与上一个数的差值.
如果我们**在差分数组的 $b_x$减去$del$ 在$b_{y+1}$位置处加上$del$**,就能达到整个区间修改的操作.
什么?不相信? 那我们来个栗子~~(要糖炒的~~
还是刚开始的栗子.
![](https://i.loli.net/2018/09/20/5ba2e06277403.png)
这样是不是达到了区间修改操作,是不是! ~~"是!,tql!!"~~
这样我们就能**做到$O(1)$修改**啦!
我们再**定义两个数组**(先不要忘记我们的差分数组为$b_i$
> $s_i$代表$\sum_{j=1}^{i} b_j$ (其实就是代表$a[i]$ qwq
> $sum_i$代表$\sum_{j=1}^{i} s_j$ 即代表前缀和. qwq
容易发现的是 $sum_r -sum_{l-1}=\sum_{i=l}^{r} s_i$
什么不理解为什么是$l-1$?
那我们把式子展开看 qwq
$sum_r=s_1+s_2+\dots+s_{l-1}+s_{l}+s_{l+1}+\dots+s_r$
$sum_{l-1}=s_1+s_2+\dots+s_{l-2}+s_{l-1}$
两个式子相减的话,我们得到的就是 $s_l+\dots+s_r$ 即$\sum_{i=l}^r s_i$啦!
而我们如果减去$sum_l$的话,就会多减去一个s_l,得到的值就不是$\sum_{i=l}^r s_i$ 了!
~~emmmm 差点跑去讲前缀和~~
所以说我们可以在修改操作完成之后,$O(n)$的计算我们的$sum$数组,然后在询问的时候$O(1)$的输出了.
具体这个题的代码是这样的↓
#include<bits/stdc++.h>
using namespace std;
int n,m,q,last,sum[10086],b[10086],s[10086];
int main()
{
cin>>n;//n个数
for(int i=1,x;i<=n;i++)
{
cin>>x;//这里实际上不需要a数组,视题而异
b[i]=x-last;//得到差分数组
last=x;//别忘了变化last变量
}
cin>>m;//m次操作
for(int i=1,l,r,del;i<=m;i++)
{
cin>>l>>r>>del;//在[l,r] 加上del
b[l]+=del,b[r+1]-=del;
}
for(int i=1;i<=n;i++)
{
s[i]=s[i-1]+b[i];//这里是处理我们的s数组
sum[i]=sum[i-1]+s[i];//处理我们的sum数组.
}
cin>>q;//q个询问
for(int i=1,l,r;i<=q;i++)
{
cin>>l>>r; //询问[l,r]的区间和.
cout<<sum[r]-sum[l-1]<<endl; //输出即可.
}
}
刚刚**涉及到的用途**有
1. 快速处理区间加减操作:$O(1)$
1. 询问区间和:$O(n)$处理$O(1)$查询.
你以为差分只有这么多用处吗?
利用**差分数组还能算出前缀和**,看式子变形!
![](https://i.loli.net/2018/09/20/5ba33da94971b.png)
所以说,上面的代码完全可以改一下,相信大家写出来应该会很容易. ~~(其实是我懒~~ qwq
差分还有其他用途这里并没有涉及到,所以还需要大家自己去发现去学习 ┓( ´∀` )┏
### 例题
来一个 **差分+二分** 结合运用的题目 -->[p1083 借教室](https://www.luogu.org/problemnew/show/P1083)
看完题目了没? 有没有思路?
想一下,
在 **一个订单的起始天+要借的教室数量, 并在该订单结束的那一天的下一天减去要借的教室数量**
这样我们再维护一下前缀和,这就样就能知道这些天中,我们借了多少教室! 是不是很巧妙qwq
然后我们再二分订单数量,尝试满足mid个订单,(这个不会证明 QAQ)
因此我们ok函数这样写 ↓
IL bool ok(int now)//尝试满足now个订单
{
clear(delt);
for(RI i=1;i<=now;i++)
{
delt[s[i]]+=d[i];//s[i]为第i个订单起始天
delt[t[i]+1]-=d[i];//t[i]为第i个订单结束天.
}
for(RI i=1;i<=n;i++)
{
sum[i]=sum[i-1]+delt[i];//前缀和.
if(sum[i]>could[i])return false;
}
return true;
}
相信你~~随随便便~~也能切掉这个题了.
### 小结
差分数组的话,一般并没有裸的考查,但是差分数组的思想啊,辅助啊,还是比较常用的qwq.
例如树状数组维护差分(到底是谁维护谁我也不是很清楚)qwq
(因为树状数组是维护的前缀和啊,所以可以一起用)
推荐一篇很好的文章(讲解树状数组与差分的结合使用)--->[这里](http://www.cnblogs.com/RabbitHu/p/BIT.html)
[Chanis](https://www.luogu.org/blog/Chanis/super-BIT)写的也很好啊qwq
如果非要说**差分数组的适用范围**的话,
它适用于**离线的区间修改问题**,在线的话就去码 线段Tree 或 Tree状数组就好了 qwq
### 课下作业
差分的板子题[loj](https://loj.ac/problem/2332) [洛谷](https://www.luogu.org/problemnew/show/AT2442)
题解戳[这里](https://rpdreamer.blog.luogu.org/at2442)
[p3943 星空](https://www.luogu.org/problemnew/show/P3943) xor差分+最短路? (我也没有做啊 qwq
[p3948 数据结构](https://www.luogu.org/problemnew/show/P3948) 一道差分的简单题 qwq.
这两个题我只码了[第二题的代码](https://www.luogu.org/paste/avct2k5l)
第一题有时间再做 qwq
## 树上差分
其实主要是为了讲这个的qwq
2015,2016两年Noip对于树上差分都有考察 [noip2015 运输计划](https://www.luogu.org/problemnew/show/P2680) [noip2016 天天爱跑步]( https://www.luogu.org/problemnew/show/P1600)
这两个题涉及的知识点有着 **树上差分+二分+LCA$\dots$**,这是一些进阶的考查 ~~其实是我不太会qwq~~
所以在这里打算~~单纯地~~介绍一下树上差分并讲解一些例题.qwq
### 前置知识
需要知道的**树的性质**:
1. 树上任意两个点的路径唯一.
2. 任何子节点的父亲节点唯一.(可以认为根节点是没有父亲的)
如果你认为你知道了这些你就能秒切这些树上差分的题,那你就太低估这个东西了!
树上差分的**两种基本操作用到了LCA**,不了解LCA的话可以去[这里面](https://www.luogu.org/problemnew/show/P3379)学一下
### 思想
类比于差分数组,树上差分利用的思想也是前缀和思想.(在这里应该是子树和思想.
当我们**记录树上节点被经过的次数,记录某条边被经过的次数的时候.**
如果每次强制dfs去标记的话,时间复杂度将高到爆炸!
因此我们引入了树上差分!
与**树上差分在一起的使用的是$DFS$,因为在回溯的时候,我们可以计算出子树的大小.**
(这个应该不用过多解释
### 定义数组
$cnt_i$为节点i被经过的次数.
### 基本操作
#### 1.点的差分
这个比较简单,所以先讲这个qwq
例如,我们从 $s-->t$ ,求**这条路径上的点被经过的次数.**
很明显的,我们需要**找到他们的LCA**,(因为这个点是中转点啊qwq.
我们需要让$cnt_s++$,让$cnt_t++$,而让他们的$cnt_{lca}--$,$cnt_{faher(lca)}--$;
可能读着会有些难理解,所以我准备了一个图qwq
>绿色的数字代表经过次数.
![](https://i.loli.net/2018/09/20/5ba2e361b8b36.png)
直接去标记的话,可能会T到不行,但是我们现在在讲啥?**树上差分啊!**
根据刚刚所讲,我们的标记应该是这样的↓
![](https://i.loli.net/2018/09/20/5ba2e37999da2.png)
**考虑**:我们搜索到s,向上回溯.
下面以$u$表示当前节点,$son_i$代表i的儿子节点.(如果一些$son$不给出下标,即代表当前节点$u$的儿子
每个$u$统计它的子树大小,顺着路径标起来.(即$cnt_u+=cnt_{son}$)
我们会发现第一次从s回溯到它们的LCA时候,$cnt_{LCA}+=cnt[son_{LCA}]$
$cnt_{LCA}=0$! "不是LCA会被经过一次嘛,为什么是0!"
别急,我们继续搜另一边.
**继续**:我们搜索到t,向上回溯.
依旧统计每个u的子树大小$cnt_u+=cnt_{son}$
再度回到$LCA$ 依旧 是$cnt_{LCA}+=cnt[son_{LCA}]$
这个时候 $cnt_{LCA}=1$ 这就达到了我们要的效果 (是不是特别优秀 ( • ̀ω•́ )✧
**担忧**: 万一我们再从$LCA$向上回溯的时候使得其父亲节点的子树和为1怎么办?
这样我们不就使得其父亲节点被经过了一次? 因此我们需要在$cnt_{faher(lca)}--$
这样就达到了标记我们路径上的点的要求! 厉不厉害 (o゚▽゚)o ~~tql!!~~
这样点的差分应该没什么问题了吧 ,有问题可以问我的哦 qwq (如果我会的话.)
#### 2.边的差分
既然我们已经get到了点的差分,那么我们边的差分也是很简单啦!
机房某dalao:"这不和点差分标记方式一样吗?不就是把边塞给点吗? 看我切了它!"
为这位大佬默哀一下 qwq.
的确,我们**对边进行差分需要把边塞给点**,但是,这里的**标记并不是同点差分一样**.
**PS: 把边塞给点的话,是塞给这条边所连的深度较深的节点. (即塞给儿子节点**
先请大家思考$5s$
$\vdots$
$\vdots$
$\vdots$
好,时间到,有没有想到如何标记?(只要画图模拟一下就可以啦! 上图!
> 红色边为需要经过的边,绿色的数字代表经过次数
正常的话,我们的图是这样的.↓
![](https://i.loli.net/2018/09/20/5ba2e4506217d.png)
但是由于我们把边塞给了点,因此我们的图应该是这样的↓
![](https://i.loli.net/2018/09/20/5ba2e460e51a2.png)
但是根据我们点差分的标记方式来看的话显然是行不通的,
这样的话我们会经过$father_{LCA}--> LCA$这一路径.
因此考虑如何标记我们的点,来达到经过红色边的情况
聪明的你一定想到了,这样来标记
$cnt_s++$ , $cnt_t ++$ ,$cnt_{LCA}-=2$
这样回溯的话,我们即可只经过图中红色边啦!(这里就不详细解释啦,原理其实相同 qwq
把边塞入点中的代码这样写.qwq(顺便在搜索的时候处理即可
void dfs(int u,int fa,int dis)
{
//u为当前节点,fa为当前节点的父亲节点,dis为从fa通向u的边的边权.
depth[u]=depth[fa]+1;
f[u][0]=fa;//相信写过倍增LCA的人都能看懂.
init[u]=dis;//这里是将边权赋给点.
for(int i=1;(1<<i)<=depth[u];i++)f[u][i]=f[f[u][i-1]][i-1];//预处理倍增数组.
for(int i=head[u];i;i=edge[i].u)
{
if(edge[i].v==fa)continue;
dfs(edge[i].v,u,edge[i].w);
}
//这个每个人的写法不一样吧.
//所以根据每个人的代码风格不一样,码出来的也不一样
}
### 例题选讲
代码会在下面统一发 qwq
1. 先来一个简单题练练手 --->[p3128 最大流](https://www.luogu.org/problemnew/show/P3128)
这个题应该算是树上差分的入门题
~~就是入门题qwq~~
**题意简单概括一下**
求被经过次数最多的点,(其实概括出来的话,这题就裸了.)
显然,裸的点差分.
所以请在课下切掉它qwq.
2. 再来一个简单题练手 --->[p3258 松鼠的新家](https://www.luogu.org/problemnew/show/P3258)
也是一个简单的树上差分的题.不过有一些小坑点.
读完题大家先思考$5s$
$\vdots$
时间到。
**简单分析**
很明显,这是一道点差分.但是不同的是,我们需要在每个位置”中转“一下.
即从 $a_1-->a_2$ ,$a_2-->a_3$ ,$a_3-->a_4$ $\dots$我们会重复经过$a_2$,$a_3$,$\dots$这一些点.
因此我们经过这些节点次数会被重复计算,因此,我们需要将其$--$
还要注意的是,当我们到达$a_n$这一位置的时候,小熊会吃饭 qwq ,即在这里不会有糖果吃. 所以这个位置的经过次数也需要$--$.
代码中需要注意的位置也只有这里 这样↓
for(RI i=2;i<=n;i++)cnt[a[i]]--;
3.上点难度了 ----->[p2680 运输计划](https://www.luogu.org/problemnew/show/P2680)
(可能是因为我太弱了 qwq
先感谢dalao的讲解 [@GMPotlc](https://www.luogu.org/space/show?uid=87956)
读完题,我们发现,这是一道边差分的题.
**简单分析**
于是建完边我们先dfs一遍预处理出根节点到每个节点的距离.并把边权塞给点。
预处理距离的话只需要再在dfs中加入一句即可
Dis[edge[i].v]=Dis[u]+edge[i].w;
然后我们可以计算出每条航道间的距离,类似这样
//**$query[i].dis$代表第i个询问两航道之间的距离**
//**则$query[i].dis=Dis[x]+Dis[y]-2*Dis[lca_{x,y}]$**
不能理解这个计算的话来看图 qwq.
>图中给出的边均为从根节点到达某节点的距离.
>颜色对应
![](https://i.loli.net/2018/09/20/5ba2e66688b31.png)
我们发现,实际只要记录的距离仅为LCA下面的红色和绿色路径.
而我们重复经过了LCA上面的边两次."这没用啊"
因此只要减去2*$Dis_{LCA}$即可.
**考虑:**
我们需要将被经过次数最多,且边权最大的边删去.
这样能使我们所用总时间最大值尽可能小 (很明显 qwq
要求最大值最小? 很明显,我们想到了二分答案.
**解决**
既然想到了二分答案,那我们就**二分这些路径的长度**.(即工作时间.
如果一些路径长度大于当前二分的mid,我们就需要记录这些路径上的边其被经过次数.
(比**mid小的路径一定已经合法**,我们可以在mid时间内完成任务.)
假设**路径长度大于mid的有num个**
(我们找到被这些路径共同经过的最大的边权,删去它,使得这些路径长度都小于mid,那么这个mid就是合法的.
**小细节**:我们可以通过排序得到最大的路径长度,如果这条最长的路径减去被经过次数<=mid,那这个mid就是合法的,我们就可以去寻找更优解.
**这里引用题解里的一句话**
>因为要求求最小时间, 然而可以根据单调性可以通过**二分一个时间**来判定这个时间能不能成立.
>也就是通过二分答案将一个**求答案的问题转化为$log_{2}t_{\max}$个判定性问题**.
因此这个题就很简单了
PS:记得每次将标记数组清零.(因为大于mid的路径长度会变化.
(~~可能~~做法常数有些大,但是是可以过的.
(也可能是评测机看脸.第一次交T了一个点,第二次交就A掉了 qwq.
上面三个题的代码都在[这里](https://www.luogu.org/paste/1uqw6p54)
### 小结
树上差分的裸题还是比较少的(其实是我遇到的比较少吧 qwq.
因此树上差分与其他算法的结合考察还是比较多的
例如刚刚讲的运输计划就是一个 LCA+树上差分+二分.
(其实树上差分问题一定有LCA的 qwq)
## BB in last
总的来说,差分数组重点在于思想的运用.
而树上差分一般不会直接考裸题.
所以,我们需要掌握更多的算法. qwq
不会的可以问我 ,直到今年Noip我一直会在.
如果拿到省一的话,我会待的更久,拿不到就滚回去学文化课了 qwq
---
参考了一些dalao的博客,这里不一一列出了.qwq
~~(其实我忘了参考过哪些~~
再度感谢**合伙人[@GMPotlc](https://www.luogu.org/space/show?uid=87956)**