差分数组 and 树上差分
差分数组
定义
百度百科中的差分定义
//其实这完全和要讲的没关系 qwq
进去看了之后是不是觉得看不懂?
那我简单概括一下qwq
差分数组de定义:记录当前位置的数与上一位置的数的差值.
栗子
容易发现的是,\(\sum_{j=1}^{i} b_j\)即代表\(a_i\) 的值. \((\sum\) 即代表累加.)
思想
看到前面的\(\sum\) 你一定会发现这是前缀和!
那你认为这是前缀和? 的确是qwq.
实际上这并不是真正意义上的前缀和.
前缀和的思想是 根据元素与元素之间的并集关系(和的关系),求出某些元素的和的值.对应的为$\sum_{j=1}^{i} a_j $
而差分的思想与此不同.
差分的思想是 根据元素与元素之间的逻辑关系(大小关系),求出某一位置元素的值.对应的为\(\sum_{j=1}^{i} b_j\)
What?不懂?看下面
继续捡起刚刚的栗子.
有没有发现不同之处 ( • ̀ω•́ )✧
差分数组有什么用?
先看一道题,
有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\),就能达到整个区间修改的操作.
什么?不相信? 那我们来个栗子(要糖炒的
还是刚开始的栗子.
这样是不是达到了区间修改操作,是不是! "是!,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; //输出即可.
}
}
刚刚涉及到的用途有
- 快速处理区间加减操作:\(O(1)\)
- 询问区间和:\(O(n)\)处理\(O(1)\)查询.
你以为差分只有这么多用处吗?
利用差分数组还能算出前缀和,看式子变形!
所以说,上面的代码完全可以改一下,相信大家写出来应该会很容易. (其实是我懒 qwq
差分还有其他用途这里并没有涉及到,所以还需要大家自己去发现去学习 ┓( ´∀` )┏
称不上是拓展的拓展
xor差分
其实刚开始听到这个名字还是很迷的emmm
感谢咕咕日报的管理大大告诉我有这个东西才能学来分享给大家
这里给出如何求这种情况下的差分数组.
n++;//原长度要+1,因为我们差分数组用到了右端点右边一位.
for(int i=1;i<=n;i++)b[i]=a[i]^a[i-1];//与上一位异或。
思想
类比于普通差分的思想,我们在被修改区间的左端点\(b_l\)^= 1,右端点的右侧\(b_{r+1}\)^=1.
但是这样可行吗?
再来一个栗子 (干炒
我们将差分数组一路滚过去 发现\(\sum_{j=1}^{i}b_j\)依旧等于\(a_i\)
这证明xor是可以差分的!
然后我们尝试翻转a_2 到a_4这段区间.
根据差分数组的思想,我们将 \(b_2\)^=1 再将 \(b_5\)^=1
得到新的一组对应关系是这样的 ↓ qwq
我们再把差分数组滚过去,发现依旧对应 ! 太神奇了!
事实证明这个是对的.(具体证明的话,我不会 qwq.
例题
来一个 差分+二分 结合运用的题目 -->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
(因为树状数组是维护的前缀和啊,所以可以一起用)
推荐一篇很好的文章(讲解树状数组与差分的结合使用)--->这里
Chanis写的也很好啊qwq
如果非要说差分数组的适用范围的话,
它适用于离线的区间修改问题,在线的话就去码 线段Tree 或 Tree状数组就好了 qwq
课下作业
题解戳这里
p3943 星空 xor差分+最短路? (我也没有做啊 qwq
p3948 数据结构 一道差分的简单题 qwq.
这两个题我只码了第二题的代码
第一题有时间再做 qwq
树上差分
其实主要是为了讲这个的qwq
2015,2016两年Noip对于树上差分都有考察 noip2015 运输计划 noip2016 天天爱跑步
这两个题涉及的知识点有着 树上差分+二分+LCA\(\dots\),这是一些进阶的考查 其实是我不太会qwq
所以在这里打算单纯地介绍一下树上差分并讲解一些例题.qwq
前置知识
需要知道的树的性质:
树上任意两个点的路径唯一.
任何子节点的父亲节点唯一.(可以认为根节点是没有父亲的)
如果你认为你知道了这些你就能秒切这些树上差分的题,那你就太低估这个东西了!
树上差分的两种基本操作用到了LCA,不了解LCA的话可以去这里面学一下
思想
类比于差分数组,树上差分利用的思想也是前缀和思想.(在这里应该是子树和思想.
当我们记录树上节点被经过的次数,记录某条边被经过的次数的时候.
如果每次强制dfs去标记的话,时间复杂度将高到爆炸!
因此我们引入了树上差分!
与树上差分在一起的使用的是\(DFS\),因为在回溯的时候,我们可以计算出子树的大小.
(这个应该不用过多解释
定义数组
\(cnt_i\)为节点i被经过的次数.
基本操作
1.点的差分
这个比较简单,所以先讲这个qwq
例如,我们从 \(s-->t\) ,求这条路径上的点被经过的次数.
很明显的,我们需要找到他们的LCA,(因为这个点是中转点啊qwq.
我们需要让\(cnt_s++\),让\(cnt_t++\),而让他们的\(cnt_{lca}--\),\(cnt_{faher(lca)}--\);
可能读着会有些难理解,所以我准备了一个图qwq
绿色的数字代表经过次数.
直接去标记的话,可能会T到不行,但是我们现在在讲啥?树上差分啊!
根据刚刚所讲,我们的标记应该是这样的↓
考虑:我们搜索到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\)
好,时间到,有没有想到如何标记?(只要画图模拟一下就可以啦! 上图!
红色边为需要经过的边,绿色的数字代表经过次数
正常的话,我们的图是这样的.↓
但是由于我们把边塞给了点,因此我们的图应该是这样的↓
但是根据我们点差分的标记方式来看的话显然是行不通的,
这样的话我们会经过\(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
先来一个简单题练练手 --->p3128 最大流
这个题应该算是树上差分的入门题
就是入门题qwq题意简单概括一下
求被经过次数最多的点,(其实概括出来的话,这题就裸了.)显然,裸的点差分.
所以请在课下切掉它qwq.
再来一个简单题练手 --->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 运输计划
(可能是因为我太弱了 qwq
先感谢dalao的讲解 @GMPotlc
读完题,我们发现,这是一道边差分的题.
简单分析
于是建完边我们先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.
图中给出的边均为从根节点到达某节点的距离.
颜色对应
我们发现,实际只要记录的距离仅为LCA下面的红色和绿色路径.
而我们重复经过了LCA上面的边两次."这没用啊"
因此只要减去2*Dis_{LCA}即可.
考虑:
我们需要将被经过次数最多,且边权最大的边删去.
这样能使我们所用总时间最大值尽可能小 (很明显 qwq
要求最大值最小? 很明显,我们想到了二分答案.
解决
既然想到了二分答案,那我们就二分这些路径的长度.(即工作时间.
如果一些路径长度大于当前二分的mid,我们就需要记录这些路径上的边其被经过次数.
(比mid小的路径一定已经合法,我们可以在mid时间内完成任务.)
假设路径长度大于mid的有num个
(我们找到被这些路径共同经过的最大的边权,删去它,使得这些路径长度都小于mid,那么这个mid就是合法的.
小细节:我们可以通过排序得到最大的路径长度,如果这条最长的路径减去被经过次数<=mid,那这个mid就是合法的,我们就可以去寻找更优解.
这里引用题解里的一句话
因为要求求最小时间, 然而可以根据单调性可以通过二分一个时间来判定这个时间能不能成立.
也就是通过二分答案将一个求答案的问题转化为\(log_{2}t_{\max}\)个判定性问题.因此这个题就很简单了
PS:记得每次将标记数组清零.(因为大于mid的路径长度会变化.
(可能做法常数有些大,但是是可以过的.
(也可能是评测机看脸.第一次交T了一个点,第二次交就A掉了 qwq.
上面三个题的代码都在这里
小结
树上差分的裸题还是比较少的(其实是我遇到的比较少吧 qwq.
因此树上差分与其他算法的结合考察还是比较多的
例如刚刚讲的运输计划就是一个 LCA+树上差分+二分.
(其实树上差分问题一定有LCA的 qwq)
BB in last
总的来说,差分数组重点在于思想的运用.
而树上差分一般不会直接考裸题.
所以,我们需要掌握更多的算法. qwq
不会的可以问我 ,直到今年Noip我一直会在.
如果拿到省一的话,我会待的更久,拿不到就滚回去学文化课了 qwq
差分数组 and 树上差分的更多相关文章
- 树上差分 (瞎bb) [树上差分][LCA]
做noip2015的运输计划写了好久好久写不出来 QwQ 于是先来瞎bb一下树上差分 混积分 树上差分有2个常用的功能: (1)记录从点i到i的父亲这条路径走过几次 (2)将每条路径(s,t ...
- [USACO15DEC]最大流Max Flow(树上差分)
题目描述: Farmer John has installed a new system of N−1N-1N−1 pipes to transport milk between the NNN st ...
- 【BZOJ3307】雨天的尾巴 题解(树链剖分+树上差分)
题目链接 题目大意:给定一颗含有$n$个结点的树,每次选择两个结点$x$和$y$,对从$x$到$y$的路径上发放一带$z$类型的物品.问完成所有操作后每个结点发放最多的时哪种物品. 普通的树链剖分貌似 ...
- BZOJ3881 Coci2015Divljak(AC自动机+树上差分+树状数组)
建出AC自动机及其fail树,每次给新加入的串在AC自动机上经过的点染色,问题即转化为子树颜色数.显然可以用dfs序转成序列问题树状数组套权值线段树解决,显然过不掉.事实上直接树上差分,按dfs序排序 ...
- P3250 [HNOI2016] 网络 (树剖+堆/整体二分+树上差分+树状数组)
解法1: 本题有插入路径和删除路径,在每个节点维护插入堆和删除堆,查询时两者top一样则一直弹出.如果每个节点维护的是经过他的路径,显然有些不好处理,正难则反,每个点维护不经过他的路径,那么x节点出了 ...
- Educational Codeforces Round 54 E. Vasya and a Tree(树上差分数组)
https://codeforces.com/contest/1076/problem/E 题意 给一棵树(n<=3e5),m(3e5)次查询,每次查询u,d,x,表示在u的子树中,给距离u&l ...
- BZOJ4999 This Problem Is Too Simple!(树上差分+dfs序+树状数组)
对每个权值分别考虑.则只有单点加路径求和的操作.树上差分转化为求到根的路径和,子树加即可.再差分后bit即可.注意树上差分中根的父亲是0,已经忘了是第几次因为这个挂了. #include<ios ...
- [填坑]树上差分 例题:[JLOI2014]松鼠的新家(LCA)
今天算是把LCA这个坑填上了一点点,又复习(其实是预习)了一下树上差分.其实普通的差分我还是会的,树上的嘛,也是懂原理的就是没怎么打过. 我们先来把树上差分能做到的看一下: 1.找所有路径公共覆盖的边 ...
- 【NOIP2016】【LCA】【树上差分】【史诗级难度】天天爱跑步
学弟不是说要出丧题吗>>所以我就研究了1天lca又研究了1天tj然后研究了一天天天爱跑步,终于写了出来.(最后的平均用时为240ms...比学弟快了1倍...) 题意:给你颗树,然后有m个 ...
随机推荐
- hdu6103 Kirinriki(trick+字符串)
题解: 考虑一开始时,左边从1开始枚举,右边从n开始枚举 我们可以得到一个最大的值k. 但是如果这样依次枚举,复杂度肯定是n^3,是不行的 考虑如何利用上一次的结果,如果我们把1和n同时去掉 就可以利 ...
- 2018牛客多校第一场 B.Symmetric Matrix
题意: 构造一个n*n的矩阵,使得Ai,i = 0,Ai,j = Aj,i,Ai,1+Ai,2+...+Ai,n = 2.求种类数. 题解: 把构造的矩阵当成邻接矩阵考虑. 那么所有点的度数都为2,且 ...
- 02.Java面向对象问题
目录介绍 2.0.0.1 重载和重写的区别?重载和重写绑定机制有何区别?父类的静态方法能否被子类重写? 2.0.0.2 封装.继承.多态分别是什么? 2.0.0.3 接口和抽象类的区别是什么?接口的意 ...
- BZOJ1076 [SCOI2008]奖励关 【状压dp + 数学期望】
1076: [SCOI2008]奖励关 Time Limit: 10 Sec Memory Limit: 128 MB Submit: 3074 Solved: 1599 [Submit][Sta ...
- bzoj进度条
好久没发进度了 这个月没有上个月那么猛,肯能使因为这个月不想水题吧 No. 510 Solved Problems List Solved 368 10001001100210071008101210 ...
- async的用法
package com.example.administrator.myapplication; import android.os.AsyncTask; import android.util.Lo ...
- C++构造函数重载以及默认参数引起的二义性
大家都知道当我们声明一个类时,系统会提供一个默认构造函数.当我们需要提供参数进行对类数据成员进行初始化时,就需要对类的带参构造函数进行重载.同时,如果我们需要调用默认构造函数进行类数据成员的初始化时, ...
- 封装常用的Javascript跨浏览器方法
var EventUntil={ // 跨浏览器的添加事件方法 addHandler:function(element,type,handler){ if(element.addEventListen ...
- Python 进阶学习笔记
把函数作为参数 import math def add(x, y, f): return f(x) + f(y) print add(, , math.sqrt) map(f, list) 函数 接收 ...
- C# 序列化理解 1(转)
序列化又称串行化,是.NET运行时环境用来支持用户定义类型的流化的机制.其目的是以某种存储形成使自定义对象持久化,或者将这种对象从一个地方传输到另一个地方. .NET框架提供了两种串行化的方式: ...