一、差分

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

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

1

2

3

4

5

solve(){

     暴力枚举每张地毯

     将所有被覆盖的点均做上标记

     最后再枚举所有整点,若未被标记则ans+1   

}

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

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

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

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

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

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

1

2

3

4

5

6

solve(){

    从1号地毯考虑到第k块地毯

    对于每一块地毯,从右上角坐标行数循环到左下角行数

    将每一行进行修改

    对差分数组求前缀和并累计未被覆盖地毯的点   

}

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

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

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

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

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

  ①关于边的差分:

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

  (1)枚举xx的所有子节点uu

  (2)dfsdfs所有子节点uu

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

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

  ②关于点的差分:

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

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

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

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

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