zoukankan      html  css  js  c++  java
  • 浅析差分及其推广(树上差分与广义差分)

    差分数组及树上差分

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

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

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

         即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,且树上差分常常用于求经过某点或边路径的条数。

     

  • 相关阅读:
    caffe用到的命令和零碎知识
    Manjaro — ssh出现22端口拒绝访问问题(port 22: Connection refused)
    Linux 解压z01 .z02 .z03... zip分卷
    Manjaro_Windows双系统安装
    Linux 的chsh命令
    mat2json, python读取mat成字典, 保存json
    最便捷的caffe编译方法 ---- cmake+anaconda虚拟环境
    复制跳过软链接
    使用Screen解决ssh连接中断导致的训练中断问题
    Caffe训练时Loss=87.3365问题
  • 原文地址:https://www.cnblogs.com/InductiveSorting-QYF/p/11801262.html
Copyright © 2011-2022 走看看