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

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

    见:洛谷P3943 星空——题解

  树上差分:将差分搬到了树上。能够有两个差分方向:

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

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

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

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

    前置知识:

      须要知道的树的性质:

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

        二、任何子节点的父亲节点惟一.(能够认为根节点是没有父亲的)

      树上差分的两种基本操做用到了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,且树上差分经常用于求通过某点或边路径的条数。

相关文章
相关标签/搜索