差分与前缀和
一维前缀和
对于一个长度为n的数组nums,其下标i处的前缀和 = 从下标0到下标i的值累加得到的新值。这些新值重新组成一个新的数组,称为前缀和数组。我们一般说的前缀和特指前缀和数组,这里为了说明清晰还是区分前缀和与前缀和数组。另外为了方便计算与还原数组,我习惯添加一个前置0,形成长度为n+1的前缀和数组。
一维差分
对于一个长度为n的数组nums,其下标i处的差分=下标i处的值-下标i-1处的值。这些新值组成的新的数组称为差分数组,对于下标0处的值,没有对象可以减去,因此维持不变。同理本笔记也区分差分和差分数组。另外为了方便计算与还原数组,我习惯给差分数组最后加一个0-nums[n-1]的值,相当于给原数组nums添加了一个0的值形成长度为n+1的nums数组
一维前缀和
一维前缀和公式可以总结如下,对于下标i处的元素sum[i]有:(注意长为n的数组nums下标从0开始,到n-1结束)
s u m [ n ] = ∑ i = 0 n − 1 n u m s [ i ] sum[n]=\sum_{i=0}^{n-1}nums[i]
s u m [ n ] = i = 0 ∑ n − 1 n u m s [ i ]
所以一维前缀和数组可以表示为:sumArr = [0,sum[0],sum[1],sum[2]…sum[n-1]]。(加了前置0)
前缀和的应用:前缀和主要应用于多次快速求解数组nums区间[L,R]的和。当我们预处理得到前缀和数组sumArr后,
s u m [ R + 1 ] − s u m [ L ] sum[R+1]-sum[L]
s u m [ R + 1 ] − s u m [ L ]
就是nums区间[L,R]的所有值的和。因为sum[R+1]是nums下标0到下标R的数的和,sum[L]是下标0到下标L-1的和。(注意:因为我的习惯添加了前置0到sumArr数组,所以原nums数组对应到sumArr数组需要加1)。两者相减就是答案。
模板题:
给你一个长度为n的数组nums,再给你一个m行2列的二维数组querys表示m次询问,每个子数组包含两个数字L、R,表示查询nums中L到R所有数之和。
数据范围:1 <= n <= 1 0 5 10^5 1 0 5 ,1 <= m <= 1 0 5 10^5 1 0 5 ,0 <= L <= R <= n - 1。
分析:朴素的做法是对于每次querys[i],遍历L到R的数组,然后累加得到结果即可,分析可知时间复杂度为O ( 1 0 10 ) O(10^{10}) O ( 1 0 1 0 ) 。显然是不符合题目要求的。但是使用前缀和可以达到O ( 1 0 5 ) O(10^{5}) O ( 1 0 5 ) 的时间复杂度,即query次数的时间。符合题目要求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public int [] prefixSum(int [] nums, int [][] querys) { int [] sumArr = new int [nums.length + 1 ]; for (int i = 0 ; i < nums.length; i++) { sumArr[i + 1 ] = sumArr[i] + nums[i]; } int [] ansArr = new int [querys.length]; for (int i = 0 ; i < querys.length; i++) { int left = querys[i][0 ]; int right = querys[i][1 ]; ansArr[i] = sumArr[right + 1 ] - sumArr[left]; } return ansArr; }
1 2 3 一组测试用例 int[ ] nums = { 1 , 2 , 3 , 4 , 5 } ; int[ ] [ ] querys = { { 1 , 3 } , { 2 , 4 } , { 0 , 4 } } ;
一维差分
一维差分的公式可以总结如下,对于下标i处的元素diff[i]有:
d i f f [ i ] = n u m s [ i ] − n u m s [ i − 1 ] diff[i] = nums[i] - nums[i - 1]
d i f f [ i ] = n u m s [ i ] − n u m s [ i − 1 ]
所以一维差分数组可以表示为:sumArr = [diff[0], diff[1], diff[2]…diff[n-1], 0]。(加了后置0)
一维差分的应用:差分主要和前缀和一起搭配使用,先使用差分,处理区间加上一个数,或减去一个数的逻辑,然后使用前缀和,还原整个数组。
例如,对于一个长度为5数组nums={a1, a2, a3, a4, a5}
;先求出其差分数组,补上后置0(个人习惯),得到diffArr = {a1, a2 - a1, a3 - a2, a4 - a3, a5 - a4, 0}
。
假设对nums的区间1到3加上一个数x,相当于对于nums的1下标加上x,在3+1下标上减去x。得到diffArr = {a1, a2 - a1 + x, a3 - a2, a4 - a3, a5 -a4 - x, 0}
。(我们使用前缀和可以将diffArr其还原到newNums = {a1, a2 + x, a3 + x, a4 + x, a5, 0}
。) 。
再次对nums区间0到4减去一个数y,相当于对于nums的0的下标减去y,在4+1下标上加上y,得到diffArr = {a1 - y, a2 - a1 + x, a3 - a2, a4 - a3, a5 - a4 - x, y}
,使用前缀和,还原可得到newNums = {a1 - y, a2 + x - y, a3 + x - y, a4 + x - y, a5 - y, a5}
。在diffArr中下标0到4的5个元素就是最终的答案。
模板题:
给你一个长度为n的数组nums,再给你一个m行4列的二维数组opers表示m次操作,每个子数组包含两个数字oper、L、R、num。oper=0或1,0表示对于L、R的连续区间加上num,1表示对于L、R的连续区间减去num。所有操作后,求最终的数组。
数据范围:1 <= n <= 1 0 5 10^5 1 0 5 ,1 <= m <= 1 0 5 10^5 1 0 5 ,oper=1或2,0 <= L <= R <= n - 1。
分析:朴素的做法是对于每次opers[i],遍历L到R的数组,然后操作得到结果即可,分析可知时间复杂度为O ( 1 0 10 ) O(10^{10}) O ( 1 0 1 0 ) 。显然是不符合题目要求的。但是使用差分可以达到O ( 1 0 5 ) O(10^{5}) O ( 1 0 5 ) 的时间复杂度,即opers次数的时间。符合题目要求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public int [] diffAlgo(int [] nums, int [][] opers) { int [] diffArr = new int [nums.length + 1 ]; for (int i = 0 ; i < nums.length; i++) { if (i == 0 ) { diffArr[i] = nums[i]; } else { diffArr[i] = nums[i] - nums[i-1 ]; } } for (int i = 0 ; i < opers.length; i++) { int left = opers[i][1 ]; int right = opers[i][2 ]; int p = opers[i][3 ]; if (opers[i][0 ] == 0 ) { diffArr[left] += p; diffArr[right + 1 ] -= p; } else { diffArr[left] -= p; diffArr[right + 1 ] += p; } } int [] ans = new int [nums.length]; ans[0 ] = diffArr[0 ]; for (int i = 1 ; i < diffArr.length - 1 ; i++) { ans[i] = ans[i - 1 ] + diffArr[i]; } return ans; }
题目补充:1109. 航班预订统计 - 力扣(LeetCode)
二维前缀和
二维前缀和,只是在一维前缀和的基础上,将数组扩展到二维数组,来求答案。对于一个n行m列的二维数组,展示形式如下:代码的数组是从0开始的,这里按代码习惯展示了。
( a 0 0 a 0 1 … a 0 ( m − 1 ) a 1 0 a 1 1 … a 1 ( m − 1 ) ⋮ ⋮ ⋱ ⋮ a ( n − 1 ) 0 a ( n − 1 ) 1 … a ( n − 1 ) ( m − 1 ) ) \begin{pmatrix}
a{_0}{_0} & a{_0}{_1} & \dots & a{_0}{_{(m-1)}} \\
a{_1}{_0} & a{_1}{_1} & \dots & a{_1}{_{(m-1)}} \\
\vdots & \vdots & \ddots & \vdots \\
a{_{(n-1)}}{_0} & a{_{(n-1)}}{_1} & \dots & a{_{(n-1)}}{_{(m-1)}} \\
\end{pmatrix} ⎝ ⎜ ⎜ ⎜ ⎜ ⎛ a 0 0 a 1 0 ⋮ a ( n − 1 ) 0 a 0 1 a 1 1 ⋮ a ( n − 1 ) 1 … … ⋱ … a 0 ( m − 1 ) a 1 ( m − 1 ) ⋮ a ( n − 1 ) ( m − 1 ) ⎠ ⎟ ⎟ ⎟ ⎟ ⎞
因此,二维前缀和的通项可以表示为:
p r e S u m [ n , m ] = ∑ i = 0 n − 1 ∑ j = 0 m − 1 a i j preSum[n,m] = \sum_{i=0}^{n-1}\sum_{j=0}^{m-1}a_{ij}
p r e S u m [ n , m ] = i = 0 ∑ n − 1 j = 0 ∑ m − 1 a i j
preSum[n,m]表示nums中,nums[0, 0]到nums[n - 1,m - 1]所在的矩阵的和。在前缀和数组上,个人习惯添加前置0 ,所以对应都需要加1,因此可以创建前缀和数组preSumArr = new long[n + 1][m + 1](用long避免溢出)。
对于原数组nums下标(x, y)处的前缀和:p r e S u m A r r [ x + 1 ] [ y + 1 ] = p r e S u m A r r [ x + 1 ] [ y ] + p r e S u m A r r [ x ] [ y + 1 ] − p r e S u m A r r [ x ] [ y ] + n u m s [ x ] [ y ] preSumArr[x + 1][y + 1] = preSumArr[x + 1][y] + preSumArr[x][y+1] - preSumArr[x][y] + nums[x][y] p r e S u m A r r [ x + 1 ] [ y + 1 ] = p r e S u m A r r [ x + 1 ] [ y ] + p r e S u m A r r [ x ] [ y + 1 ] − p r e S u m A r r [ x ] [ y ] + n u m s [ x ] [ y ] 。
如图所示,由于我们程序计算的先后顺序,我们计算前缀和preSumArr[x2][y2]的时候,已经计算了其正左方(x2, y2-1)、正上方(x2-1, y2)前缀和,由上图可知,(x2, y2)到(0,0)的矩形所在的前缀和,可由(x2, y2 -1) +(x2-1, y2) - (x2 - 1, y2 - 1) 所在区间的和+nums[x2][y2](即红色区间+橘色区间-黄色区间+nums[x2][y2])。
对于nums下标(x1, y1)到(x2,y2)的区间和:
a n s = p r e S u m A r r [ x 2 + 1 ] [ y 2 + 1 ] − p r e S u m A r r [ x 2 + 1 ] [ y 1 ] − p r e S u m A r r [ x 1 ] [ y 2 + 1 ] + p r e S u m A r r [ x 1 ] [ y 1 ] ans = preSumArr[x2 + 1][y2 + 1] - preSumArr[x2 + 1][y1] - preSumArr[x1][y2 + 1] + preSumArr[x1][y1] a n s = p r e S u m A r r [ x 2 + 1 ] [ y 2 + 1 ] − p r e S u m A r r [ x 2 + 1 ] [ y 1 ] − p r e S u m A r r [ x 1 ] [ y 2 + 1 ] + p r e S u m A r r [ x 1 ] [ y 1 ] 。
如图所示,白色区域的区间和=白色(到(0,0)的)前缀和 - 红色前缀和 - 橘色前缀和 + 黄色前缀和。由于(x1, y1)要被加入到区间和结果,因此白色区域的区间和 = (x2, y2)- (x2, y1 - 1) - (x1 - 1, y2) + (x1 - 1, y1 - 1)
模板题:
【模板】二维前缀和_牛客网 (nowcoder.com)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public class Main { public static void main (String[] args) { Scanner sc = new Scanner (new BufferedInputStream (System.in)); PrintWriter pw = new PrintWriter (new BufferedOutputStream (System.out)); int n = sc.nextInt(); int m = sc.nextInt(); int q = sc.nextInt(); long [][] preSumArr = new long [n + 1 ][m + 1 ]; for (int i = 0 ; i < n; i++) { for (int j = 0 ; j < m; j++) { preSumArr[i+1 ][j+1 ] = sc.nextInt() + preSumArr[i][j + 1 ] + preSumArr[i + 1 ][j] - preSumArr[i][j]; } } for (int i = 0 ; i < q; i++) { int x1 = sc.nextInt() - 1 ; int y1 = sc.nextInt() - 1 ; int x2 = sc.nextInt() - 1 ; int y2 = sc.nextInt() - 1 ; long ans = preSumArr[x2 + 1 ][y2 + 1 ] - preSumArr[x2 + 1 ][y1] - preSumArr[x1][y2 + 1 ] + preSumArr[x1][y1]; pw.println(ans); } pw.flush(); } }
二维差分
二维差分,同一维差分,在对数组进行区间操作后,最后求区间的值,或打印操作后的数组。
模板题
【模板】二维差分 牛客网 (nowcoder.com)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 public class Main { public static void main (String[] args) { Scanner sc = new Scanner (new BufferedInputStream (System.in)); PrintWriter pw = new PrintWriter (new BufferedOutputStream (System.out)); int n = sc.nextInt(); int m = sc.nextInt(); int q = sc.nextInt(); long [][] rawArr = new long [n + 2 ][m + 2 ]; long [][] diffArr = new long [n + 2 ][m + 2 ]; for (int i = 1 ; i <= n; i++) { for (int j = 1 ; j <= m; j++) { rawArr[i][j] = sc.nextInt(); } } for (int i = 1 ; i <= n; i++) { for (int j = 1 ; j <= m; j++) { diffArr[i][j] = rawArr[i][j] - rawArr[i -1 ][j] - rawArr[i][j-1 ] + rawArr[i-1 ][j-1 ]; } } for (int i = 0 ; i < q; i++) { int x1 = sc.nextInt() - 1 ; int y1 = sc.nextInt() - 1 ; int x2 = sc.nextInt() - 1 ; int y2 = sc.nextInt() - 1 ; int k = sc.nextInt(); diffArr[x1 + 1 ][y1 + 1 ] += k; diffArr[x2 + 2 ][y1 + 1 ] -=k; diffArr[x1 + 1 ][y2 + 2 ] -= k; diffArr[x2 + 2 ][y2 + 2 ] +=k; } for (int i = 1 ; i <= n; i++) { for (int j = 1 ; j <= m; j++) { diffArr[i][j] = diffArr[i][j] + diffArr[i - 1 ][j] + diffArr[i][j-1 ] - diffArr[i-1 ][j-1 ]; pw.print(diffArr[i][j] + " " ); } pw.println(); } pw.flush(); } }