浅析差分及其推广(树上差分与广义差分)

差分数组及树上差分

所谓差分,就是记录当前的元素与之前元素逻辑上的差距。

  最基础的用法是差的差分数组:

   记录当前位置的数与上一位置的数的差值。

     即b[i]=a[i]-a[i-1]     (b为差分数组,a为原数组)

   通过对差分数组求前缀和,可以求出原数组,即:

      

   甚至可以求出前缀和:

    

     (s为原数组,sum为原数组的前缀和数组,b为差分数组)

   可以O(1)优化区间加法:给原数组区间[l,r]的数都加上x,只要在b l处加x,b r+1 处减x。

      有两种理解角度:

        1、从差分定义出发,区间加x使区间左端点与它在原数组上一个数的差距加大了x、使区间右端点的后一个数与区间右端点的数的差距缩小了x,而没有改变区间中相邻2数的差距。

        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的值(从形式上看就是求前缀)。而这种差距不只限于减法的差,还有异或等等。不过一般这种关系应可交换(即对顺序的要求不严格)且对于运算来说有单位元(么元)(或一般化的话就是要能有互相抵消的方法)

    见:洛谷P3943 星空——题解

  树上差分:将差分搬到了树上。可以有两个差分方向:

    1、记录当前节点与父节点的逻辑关系,查询时从上往下求前缀。(不常用,因为在每次路径修改时都要修改一下当前节点的所有子节点,时间、程序复杂度都很高,没有灵魂的差分(不能O(1)实现路径修改))

    2、记录当前节点与它所有子节点总和的逻辑关系,查询时dfs求子树和(或是说以向上为正方向的求前缀)。(路径修改时只要修改一下路径起始点和lca(有时还有lca的父亲),有了灵魂的差分(可O(1)实现路径修改),很常用)

  树上差分分为点差分和边差分,不论哪种差分,差分数组的意义都是当前节点与它儿子节点总和的差距(这里为当前点(或点上的边)被路径经过次数与它的儿子节点(或其上的边)被路径经过次数总和的差,每次新增一个路径,即要求实现路径修改时,起始点与儿子们的差会多一,路径中中间的点与儿子们的差不变。点差分时,lca会比儿子们少1,lca的父亲会比儿子们少1;边差分时,lca会比儿子们少二。用这些逻辑关系从叶子向上推时,若当前点的儿子们的值都是对的,那它也是对的。边界情况就是叶子结点,显然是它的值对的,故可通过回溯推出整个树的值。这样对差分概念的理解有深入了:差分的结构不知限于线性的数组)

    (这里的基础讲解引用自大佬的博客)

    前置知识:

      需要知道的树的性质:

        1、树上任意两个点的路径唯一.

        2、任何子节点的父亲节点唯一.(可以认为根节点是没有父亲的)

      树上差分的两种基本操作用到了LCA,不了解LCA的话可以去这里面学一下

    思想

      类比于差分数组,树上差分利用的思想也是前缀和思想.(在这里应该是子树和思想.

      当我们记录树上节点被经过的次数,记录某条边被经过的次数的时候.

      如果每次强制dfs去标记的话,时间复杂度将高到爆炸!

      因此我们引入了树上差分!

      与树上差分在一起的使用的是 DFS ,因为在回溯的时候,我们可以计算出子树的大小.

      (这个应该不用过多解释

    定义数组

      cnti 为节点i被经过的次数.

    基本操作

      1.点的差分

      这个比较简单,所以先讲这个qwq

      例如,我们从s>t ,求这条路径上的点被经过的次数.

      很明显的,我们需要找到他们的LCA,(因为这个点是中转点啊qwq.

      我们需要让cns++ ,让 cnt++,而让他们的cnlca−,cnfaher(lca)− ;

      可能读着会有些难理解,所以我准备了一个图qwq。绿色的数字代表经过次数.

      

      直接去标记的话,可能会T到不行,但是我们现在在讲啥?树上差分啊!

      根据刚刚所讲,我们的标记应该是这样的↓

    

      考虑:我们搜索到s,向上回溯.

      下面以 u 表示当前节点, soni 代表i的儿子节点.(如果一些 son 不给出下标,即代表当前节点 u 的儿子

      每个 u 统计它的子树大小,顺着路径标起来.(即cnu+=cnson )

      我们会发现第一次从s回溯到它们的LCA时候,cnLCA+=cnt[sonLCA]

      cntLCA=0 ! "不是LCA会被经过一次嘛,为什么是0!"

      别急,我们继续搜另一边.

      继续:我们搜索到t,向上回溯.

      依旧统计每个u的子树大小 cnu+=cnson

      再度回到 LCA 依旧 是 cntLCA+=cnt[sonLCA]

      这个时候cntLCA=1 这就达到了我们要的效果 (是不是特别优秀 ( • ̀ω•́ )✧

      担忧: 万一我们再从 LCA 向上回溯的时候使得其父亲节点的子树和为1怎么办?

      这样我们不就使得其父亲节点被经过了一次? 因此我们需要在cnfaher(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,且树上差分常常用于求经过某点或边路径的条数。

 

posted @ 2019-11-05 20:12  千叶繁华  阅读(523)  评论(0编辑  收藏  举报