关于差分,树上差分的浅谈

  因为网上的关于差分的资料比较少,所以我根据我自己的理解编写这篇博文。如果你有什么问题,可以联系我。

--------------------------------------------------------------------------------------------------------

一、差分

  有这样一道题目:给你一个$m×n$的矩阵,然后使用$k$块地毯铺地。每片地毯都给出左下角和右上角坐标。问所有地毯铺完之后,还有多少个整点(所谓整点,即横、纵坐标均为整数的点)没有被地毯覆盖。

  当然,我们很容易写出如下的暴力程序(伪代码):

solve(){
     暴力枚举每张地毯
     将所有被覆盖的点均做上标记
     最后再枚举所有整点,若未被标记则ans+1    
}

  但是,很明显,这个算法并不能拿到满分,因为它的空间复杂度为$\Theta(nm)$,但是时间复杂度却可能达到$\Theta(mnk)$,对一般的比赛来说肯定会无法通过。

  当然,有些大佬可能会说:开$m$棵线段树可以解决此问题。肯定是可以的,但是对于$NOIp$这种比赛来说,考试时间比较紧促,我是不太赞同这种算法的,因为这样子编程复杂度太高,甚至可能出现无法调试成功而影响了其它的题目或是影响自己的心情。

  那么,我们应该如何优化这个算法呢?我们考虑一下,主要的时间就是用在枚举地毯和枚举被地毯覆盖的整点上,我们可以对这里进行优化。因为对于每块地毯,每一行,覆盖的肯定是一个连续区间。所以我们可以考虑一下前缀和。通过前缀和的方式考虑每个点被地毯覆盖的次数。如下面表格所示,假如地毯覆盖了$[2,6]$一段($1$表示地毯在该行的左端点,其中表格第一行为数组下标,第二行为数组值):

1 2 3 4 5 6 7 8
0 1 0 0 0 0 0 0

  通过对数组求前缀和,我们便能得到以下的表格:

 

1 2 3 4 5 6 7 8
0 1 1 1 1 1 1 1

  我们便会发现,通过这种前缀和的形式,能够在$\Theta(1)$的时间里,实行对一行从某个左端点开始一段区间的修改。但是,我们这个题目中地毯除了有左边界,还有右边界啊?不要紧,我们在右边界后面再减去$1$,就可以保证没有被覆盖到的地方不会受到影响。而由于右端点也包括在被覆盖的范围内,所以我们要让$r+1$减去$1$.用上面的表格,如果地毯在该行覆盖了$[2,6]$一段,我们就将原数组修改成以下所示:

1 2 3 4 5 6 7 8
0 1 0 0 0 0 -1 0

  求前缀和之后,数组就变成如下:

1 2 3 4 5 6 7 8
0 1 1 1 1 1 0 0

  这样子我们就会发现,所有被地毯覆盖的点都会变成$1$,而对于每一行,这种操作都是$\Theta(1)$的,所以k块地毯全部考虑完毕的时间复杂度为$\Theta(kn)$,最后每行做前缀和的时间复杂度为$\Theta(nm)$,这样子便对以上暴力算法进行了有效的优化。所以我们可以写出以下的代码(伪代码):

solve(){
    从1号地毯考虑到第k块地毯
    对于每一块地毯,从右上角坐标行数循环到左下角行数
    将每一行进行修改
    对差分数组求前缀和并累计未被覆盖地毯的点    
}

二、树上差分(树的前缀和)

     近年的$NOIp$,似乎对于树上差分的题目考察越来越热(参见$2015$年提高组 运输计划,$2016$年提高组 天天爱跑步)。这些题目都要知道在树上从某个点到另一个点的所有路径。但是,暴力求解这种题目经常会$TLE$。这种题目需要使用树上差分。在讲树上差分之前,首先需要知道树的以下两个性质:

  (1)任意两个节点之间有且只有一条路径。

  (2)根节点确定时,一个节点只有一个父亲节点

  这两个性质都很容易证明。那么我们知道,如果假设我们要考虑的是从$u$到$v$的路径,$u$与$v$的$lca$是$a$,那么很明显,如果路径中有一点$u'$已经被访问了,且$u'$≠$a$,那么$u$'的父亲也一定会被访问,这是根据以上性质可以推出的。所以,我们可以将路径拆分成两条链,$u$->$a$和$a$->$v$。那么树上差分有两种常见形式:(1)关于边的差分;(2)关于节点的差分。

  ①关于边的差分:

  将边拆成两条链之后,我们便可以像差分一样来找到路径了。用$cf[i]$代表从$i$到$i$的父亲这一条路径经过的次数。因为关于边的差分,$a$是不在其中的,所以考虑链$u$->$a$,则就要使$cf[u]++$,$cf[a]--$。然后链$a$->$v$,也是$cf[v]++$,$cf[a]--$。所以合起来便是$cf[u]++$,$cf[v]++$,$cf[a]-=2$。然后,从根节点,对于每一个节点$x$,都有如下的步骤:

  (1)枚举$x$的所有子节点$u$

  (2)$dfs$所有子节点$u$

  (3)$cf[x]+=cf[u]$

  那么,为什么能够保证这样所有的边都能够遍历到呢?因为我们刚刚已经说了,如果路径中有一点$u'$已经被访问了,且$u'$≠$a$,那么$u'$的父亲也一定会被访问。所以$u'$被访问几次,它的父亲也就因为$u'$被访问了几次。所以就能够找出所有被访问的边与访问的次数了。路径求交等一系列问题就是通过这个来解决的。因为每个点都只会遍历一次,所以其时间复杂度为$\Theta(n)$.

  ②关于点的差分:

  还是与和边的差分一样,对于所要求的路径,拆分成两条链。步骤也和上面一样,但是也有一些不同,因为关于点,$u$与$v$的$lca$是需要包括进去的,所以要把$lca$包括在某一条链中,用$cf[i]$表示$i$被访问的次数。最后对$cf$数组的操作便是$cf[u]++$,$cf[v]++$,$cf[a]--$,$cf[father[a]]--$。其时间复杂度也是一样的$\Theta(n)$.

--------------------------------------------------------------------------------------------------------

  通过以上的描述,如果你还是不太能理解,那么以下两个题目可能可以帮助你理解:

  USACO 最大流(树上差分)https://www.luogu.org/problem/show?pid=3128

  NOIp2015 运输计划(树上差分+二分)https://www.luogu.org/problem/show?pid=2680

posted @ 2017-10-22 14:27  Icy_Knight  阅读(...)  评论(...编辑  收藏