差分数组及树上差分html
所谓差分,就是记录当前的元素与以前元素逻辑上的差距。算法
最基础的用法是差的差分数组:数组
记录当前位置的数与上一位置的数的差值。ide
即b[i]=a[i]-a[i-1] (b为差分数组,a为原数组)post
经过对差分数组求前缀和,能够求出原数组,即:优化
甚至能够求出前缀和:url
(s为原数组,sum为原数组的前缀和数组,b为差分数组)spa
能够O(1)优化区间加法:给原数组区间[l,r]的数都加上x,只要在b l处加x,b r+1 处减x。3d
有两种理解角度:code
一、从差分定义出发,区间加x使区间左端点与它在原数组上一个数的差距加大了x、使区间右端点的后一个数与区间右端点的数的差距缩小了x,而没有改变区间中相邻2数的差距。
二、从差分数组的修改对原数组的影响入手:因为差分数组求前缀和得出原数组,当b l加x以后求前缀和,那么原数组自l及之后的数所有比b l加x以前多了x;同理当b r+1减x以后求前缀和,那么原数组自r+1及之后的数所有比b r+1减x以前少了x。总的一看,发现原数组l~r的部分就多了x,其他部分没有变化。
广义差分:差分维护的是相邻元素间的逻辑关系,从而使能从初始状态(a[0])经过差分数组表达的逻辑关系推出某个位置上a的值(从形式上看就是求前缀)。而这种差距不仅限于减法的差,还有异或等等。不过通常这种关系应可交换(即对顺序的要求不严格)且对于运算来讲有单位元(么元)(或通常化的话就是要能有互相抵消的方法)
树上差分:将差分搬到了树上。能够有两个差分方向:
一、记录当前节点与父节点的逻辑关系,查询时从上往下求前缀。(不经常使用,由于在每次路径修改时都要修改一下当前节点的全部子节点,时间、程序复杂度都很高,没有灵魂的差分(不能O(1)实现路径修改))
二、记录当前节点与它全部子节点总和的逻辑关系,查询时dfs求子树和(或是说以向上为正方向的求前缀)。(路径修改时只要修改一下路径起始点和lca(有时还有lca的父亲),有了灵魂的差分(可O(1)实现路径修改),很经常使用)
树上差分分为点差分和边差分,不论哪一种差分,差分数组的意义都是当前节点与它儿子节点总和的差距(这里为当前点(或点上的边)被路径通过次数与它的儿子节点(或其上的边)被路径通过次数总和的差,每次新增一个路径,即要求实现路径修改时,起始点与儿子们的差会多一,路径中中间的点与儿子们的差不变。点差分时,lca会比儿子们少1,lca的父亲会比儿子们少1;边差分时,lca会比儿子们少二。用这些逻辑关系从叶子向上推时,若当前点的儿子们的值都是对的,那它也是对的。边界状况就是叶子结点,显然是它的值对的,故可经过回溯推出整个树的值。这样对差分概念的理解有深刻了:差分的结构不知限于线性的数组)
(这里的基础讲解引用自大佬的博客)
前置知识:
须要知道的树的性质:
一、树上任意两个点的路径惟一.
二、任何子节点的父亲节点惟一.(能够认为根节点是没有父亲的)
树上差分的两种基本操做用到了LCA,不了解LCA的话能够去这里面学一下
思想
类比于差分数组,树上差分利用的思想也是前缀和思想.(在这里应该是子树和思想.
当咱们记录树上节点被通过的次数,记录某条边被通过的次数的时候.
若是每次强制dfs去标记的话,时间复杂度将高到爆炸!
所以咱们引入了树上差分!
与树上差分在一块儿的使用的是 DFS ,由于在回溯的时候,咱们能够计算出子树的大小.
(这个应该不用过多解释
定义数组
cnti 为节点i被通过的次数.
基本操做
1.点的差分
这个比较简单,因此先讲这个qwq
例如,咱们从s−−>t ,求这条路径上的点被通过的次数.
很明显的,咱们须要找到他们的LCA,(由于这个点是中转点啊qwq.
咱们须要让cnt s++ ,让 cnt t++,而让他们的cnt lca−−,cnt faher(lca)−− ;
可能读着会有些难理解,因此我准备了一个图qwq。绿色的数字表明通过次数.
直接去标记的话,可能会T到不行,可是咱们如今在讲啥?树上差分啊!
根据刚刚所讲,咱们的标记应该是这样的↓
考虑:咱们搜索到s,向上回溯.
下面以 u 表示当前节点, soni 表明i的儿子节点.(若是一些 son 不给出下标,即表明当前节点 u 的儿子
每一个 u 统计它的子树大小,顺着路径标起来.(即cnt u+=cnt son )
咱们会发现第一次从s回溯到它们的LCA时候,cnt LCA+=cnt[sonLCA]
cntLCA=0 ! "不是LCA会被通过一次嘛,为何是0!"
别急,咱们继续搜另外一边.
继续:咱们搜索到t,向上回溯.
依旧统计每一个u的子树大小 cnt u+=cnt son
再度回到 LCA 依旧 是 cntLCA+=cnt[sonLCA]
这个时候cntLCA=1 这就达到了咱们要的效果 (是否是特别优秀 ( • ̀ω•́ )✧
担心: 万一咱们再从 LCA 向上回溯的时候使得其父亲节点的子树和为1怎么办?
这样咱们不就使得其父亲节点被通过了一次? 所以咱们须要在cnt faher(lca)−−
这样就达到了标记咱们路径上的点的要求! 厉不厉害 (o゚▽゚)o tql!!
2.边的差分
既然咱们已经get到了点的差分,那么咱们边的差分也是很简单啦!
机房某dalao:"这不和点差分标记方式同样吗?不就是把边塞给点吗? 看我切了它!"
为这位大佬默哀一下 qwq.
的确,咱们对边进行差分须要把边塞给点,可是,这里的标记并非同点差分同样.
PS: 把边塞给点的话,是塞给这条边所连的深度较深的节点. (即塞给儿子节点
先请你们思考 5s ……
好,时间到,有没有想到如何标记?(只要画图模拟一下就能够啦! 上图! 红色边为须要通过的边,绿色的数字表明通过次数
正常的话,咱们的图是这样的.↓
可是因为咱们把边塞给了点,所以咱们的图应该是这样的↓
可是根据咱们点差分的标记方式来看的话显然是行不通的,
不然atherLCA−−>LCA 这一路径也会被标记为通过了1次
所以考虑如何标记咱们的点,来达到通过红色边的状况
聪明的你必定想到了,这样来标记
cnts++ ,cntt++ ,cntLCA−=2
这样回溯的话,咱们便可只通过图中红色边啦!(这里就不详细解释啦,原理其实相同 qwq
把边塞入点中的代码这样写.qwq(顺便在搜索的时候处理便可
1 前置知识 2 须要知道的树的性质: 3
4 树上任意两个点的路径惟一. 5
6 任何子节点的父亲节点惟一.(能够认为根节点是没有父亲的) 7
8 若是你认为你知道了这些你就能秒切这些树上差分的题,那你就过低估这个东西了!
9
10 树上差分的两种基本操做用到了LCA,不了解LCA的话能够去这里面学一下 11
12 思想 13 类比于差分数组,树上差分利用的思想也是前缀和思想.(在这里应该是子树和思想. 14
15 当咱们记录树上节点被通过的次数,记录某条边被通过的次数的时候. 16
17 若是每次强制dfs去标记的话,时间复杂度将高到爆炸!
18
19 所以咱们引入了树上差分!
20
21 与树上差分在一块儿的使用的是 DFSDFS ,由于在回溯的时候,咱们能够计算出子树的大小. 22
23 (这个应该不用过多解释 24
25 定义数组 26 cnt_icnt 27 i 28 为节点i被通过的次数. 29
30 基本操做 31 1.点的差分 32 这个比较简单,因此先讲这个qwq 33
34 例如,咱们从 s-->ts−−>t ,求这条路径上的点被通过的次数. 35
36 很明显的,咱们须要找到他们的LCA,(由于这个点是中转点啊qwq. 37
38 咱们须要让 cnt_s++cnt 39 s 40 ++ ,让 cnt_t++cnt 41 t 42 ++ ,而让他们的 cnt_{lca}--cnt 43 lca 44 −− , cnt_{faher(lca)}--cnt 45 faher(lca) 46 −− ; 47
48 可能读着会有些难理解,因此我准备了一个图qwq 49
50 绿色的数字表明通过次数. 51
52
53
54 直接去标记的话,可能会T到不行,可是咱们如今在讲啥?树上差分啊!
55
56 根据刚刚所讲,咱们的标记应该是这样的↓ 57
58
59
60 考虑:咱们搜索到s,向上回溯. 61
62 下面以 uu 表示当前节点, son_ison 63 i 64 表明i的儿子节点.(若是一些 sonson 不给出下标,即表明当前节点 uu 的儿子 65
66 每一个 uu 统计它的子树大小,顺着路径标起来.(即 cnt_u+=cnt_{son}cnt 67 u 68 +=cnt 69 son 70 ) 71
72 咱们会发现第一次从s回溯到它们的LCA时候, cnt_{LCA}+=cnt[son_{LCA}]cnt 73 LCA 74 +=cnt[son 75 LCA 76 ] 77
78 cnt_{LCA}=0cnt 79 LCA 80 =0 ! "不是LCA会被通过一次嘛,为何是0!"
81
82 别急,咱们继续搜另外一边. 83
84 继续:咱们搜索到t,向上回溯. 85
86 依旧统计每一个u的子树大小 cnt_u+=cnt_{son}cnt 87 u 88 +=cnt 89 son 90 91
92 再度回到 LCALCA 依旧 是 cnt_{LCA}+=cnt[son_{LCA}]cnt 93 LCA 94 +=cnt[son 95 LCA 96 ] 97
98 这个时候 cnt_{LCA}=1cnt 99 LCA 100 =1 这就达到了咱们要的效果 (是否是特别优秀 ( • ̀ω•́ )✧ 101
102 担心: 万一咱们再从 LCALCA 向上回溯的时候使得其父亲节点的子树和为1怎么办?
103
104 这样咱们不就使得其父亲节点被通过了一次? 所以咱们须要在 cnt_{faher(lca)}--cnt 105 faher(lca) 106 −− 107
108 这样就达到了标记咱们路径上的点的要求! 厉不厉害 (o゚▽゚)o tql!!
109
110 这样点的差分应该没什么问题了吧 ,有问题能够问个人哦 qwq (若是我会的话.) 111
112 2.边的差分 113 既然咱们已经get到了点的差分,那么咱们边的差分也是很简单啦!
114
115 机房某dalao:"这不和点差分标记方式同样吗?不就是把边塞给点吗? 看我切了它!"
116
117 为这位大佬默哀一下 qwq. 118
119 的确,咱们对边进行差分须要把边塞给点,可是,这里的标记并非同点差分同样. 120
121 PS: 把边塞给点的话,是塞给这条边所连的深度较深的节点. (即塞给儿子节点 122
123 先请你们思考 5s5s 124
125 \vdots⋮ 126
127 \vdots⋮ 128
129 \vdots⋮ 130
131 好,时间到,有没有想到如何标记?(只要画图模拟一下就能够啦! 上图!
132
133 红色边为须要通过的边,绿色的数字表明通过次数 134
135 正常的话,咱们的图是这样的.↓ 136
137
138
139 可是因为咱们把边塞给了点,所以咱们的图应该是这样的↓ 140
141
142
143 可是根据咱们点差分的标记方式来看的话显然是行不通的, 144
145 这样的话咱们会通过 father_{LCA}--> LCAfather 146 LCA 147 −−>LCA 这一路径. 148
149 所以考虑如何标记咱们的点,来达到通过红色边的状况 150
151 聪明的你必定想到了,这样来标记 152
153 cnt_s++cnt 154 s 155 ++ , cnt_t ++cnt 156 t 157 ++ , cnt_{LCA}-=2cnt 158 LCA 159 −=2
160
161 这样回溯的话,咱们便可只通过图中红色边啦!(这里就不详细解释啦,原理其实相同 qwq 162
163 把边塞入点中的代码这样写.qwq(顺便在搜索的时候处理便可 164
165 void dfs(int u,int fa,int dis) 166 { 167 //u为当前节点,fa为当前节点的父亲节点,dis为从fa通向u的边的边权.
168 depth[u]=depth[fa]+1; 169 f[u][0]=fa;//相信写过倍增LCA的人都能看懂.
170 init[u]=dis;//这里是将边权赋给点.
171 for(int i=1;(1<<i)<=depth[u];i++)f[u][i]=f[f[u][i-1]][i-1];//预处理倍增数组.
172 for(int i=head[u];i;i=edge[i].u) 173 { 174 if(edge[i].v==fa)continue; 175 dfs(edge[i].v,u,edge[i].w); 176 } 177 //这个每一个人的写法不同吧. 178 //因此根据每一个人的代码风格不同,码出来的也不同
179 }
最后总结一下:
差分维护元素与它前面紧邻的一个或多个元素的逻辑关系,并且通常均可从边界由差分维护的逻辑关系推出每个元素。(结构不仅局限于线性,逻辑关系不仅局限于减法的差关系、异或等)
(应用)差分常常用于优化修改相邻元素的操做,并且每每优化的效果很赞(直接到O(1)),但要O(n)处理出差分的前缀和后才能查询。适用于优化一批大量的全是修改连续元素的修改操做。离线算法。常搭配前缀和,对于先修改再询问的题来讲,差分O(1)处理修改,O(n)处理出前缀和,再用前缀和O(1)处理询问。
树上差分基本都会有LCA,且树上差分经常用于求通过某点或边路径的条数。