算法:差分
差分
一、介绍
差分数组就是原始数组相邻元素差构成的数组。其定义为:
主要的性质有:
- \(a_i\) 的值是 \(b_i\) 的前缀和,即 \(a_n = \sum_{i=1}^n{b_i}\)
- 计算 \(a_i\) 的前缀和 \(sum = \sum_{i=1}^n {a_i} =\sum_{i=1}^n \sum_{j=1}^i {b_i} = \sum_{i=1}^n{(n-i+1)b_i}\)
通过下面的例子更直观理解:
序号 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
原始数组 | 0 | 3 | 7 | 2 | 1 | 5 | 9 |
差分数组 | - | 3 | 4 | -5 | -1 | 4 | 4 |
二、作用
那么,差分数组有什么用呢?当我们多次对原始数组的某一个区间里的所有元素都加上一个相同的数后再访问原始数组时,差分数组就起到关键作用了。
如,对上面例子中的原始数组区间 [1, 4] 的数值都加 2,区间 [3,7] 的数值都减 5。我们可以多次访问数组的这些区间然后分别进行加操作,但是,在数据量较大的情况下,如果我们进行枚举,多次遍历区间给其中的元素加上数,那必然会是时间复杂度非常高的解决方案。这个时候我们可以使用差分数组。
我们先看看对原始数组区间 [1, 4] 的数值都加 2,数组的变化情况。
序号 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
原始数组 | 0 | 3+2=5 | 7+2=9 | 2+2=4 | 1+2=3 | 5 | 9 |
差分数组 | - | 3+2=5(原来是 3) | 4(9-5=4 不变) | -5(4-9=-5 不变) | -1(3-4=-1 不变) | 5-3=2(原来是 4) | 4 |
通过上面的例子我们可以看出,当对原始数组区间 [1, 4] 的数值都加 2,原数组的差分数组只有位置 1 和 位置 5 的数发生变化,位置 1 的数值比原来多 2,位置 5 的数值比原来少 2,其他位置的数值不变。同理对于区间 [3,7] 的数值都减 5,即区间每个数都加 -5,一样可以得到:差分数组只有位置 3 和 位置 8 的数发生变化,位置 3 的数值比原来少 5,位置 8 的数值比原来多 5。
因此,假设原始数组为 nums,差分数组为 diff,我们可以得到:
当对原始数组区间 [l, r] 的每一个数加 a,则差分数组会发生的变化是:diff[l] = diff[l] + a、diff[r+1] = diff[r+1] - a。其余位置的数值不变。
三、问题类型
1.已知原数组 nums,并多次(m 次)对其某个区间 [l, r]的数进行加数(a)操作,再访问最后结果(求出更新后的原数组)
步骤:
① 先求原数组的差分数组
② 对 m 个的区间 [l, r],分别进行差分数组的 diff[l]+=a、diff[r+1]-=a
操作
③ 通过 nums[i] = nums[i-1] + diff[i]
,求出新的 nums 数组。
2.不知原数组,已知差分数组起始状态,然后对原数组进行多次(m 次)对其某个区间 [l, r]的数进行加数(a)操作,求得原数组
步骤:
① 直接对 m 个的区间 [l, r],分别进行差分数组的 diff[l]+=a、diff[r+1]-=a
操作
② 通过 nums[i] = nums[i-1] + diff[i]
,求出新的 nums 数组。
【注意】
① 通常会使原数组的首项为 0,即nums[0]=0
,而差分数组从第一项开始,diff[0]的数为任意。
② 若原数组有效数据长度为 n,则真实长度为 n+1,因为添加了首项 0,那么差分数组的长度为 n+2,位置 0 数值为任意(通常设定为 0),而因为要进行 diff[r+1]-=a
操作,若是对原数组的最后一位 nums[n] 加 a,那么就得进行 diff[n+1]-=a
,因此差分数组的长度为 n+2。但最终 diff[n+1] 的数值对求原数组没有影响,因为原数组最后一项 nums[n] = nums[n-1] + diff[n]
四、实例
1. 差分
输入一个长度为 n 的整数序列。
接下来输入 m 个操作,每个操作包含三个整数 l,r,c,表示将序列中 [l,r] 之间的每个数加上 c。
请你输出进行完所有操作后的序列。
输入格式
第一行包含两个整数 n 和 m。
第二行包含 n 个整数,表示整数序列。
接下来 m 行,每行包含三个整数 l,r,c,表示一个操作。
输出格式
共一行,包含 n 个整数,表示最终序列。
数据范围
1≤n,m≤100000,
1≤l≤r≤n,
−1000≤c≤1000,
−1000≤整数序列中元素的值≤1000
输入样例:
6 3
1 2 2 1 2 1
1 3 1
3 5 1
1 6 1
输出样例:
3 4 5 3 4 2
[题目分析]:先不考虑输入输出问题,假设原数组 nums=[1, 2, 2, 1, 2, 1],进行三次区间加数操作,输出最后的数组。则此为第一种类型的问题,按照三个步骤即可。
[算法]:先求原数组的差分数组,再进行三次差分数组的家属操作,最后根据公式求得新的原数组。
nums=[1, 2, 2, 1, 2, 1]
ops=[[1,3,1],[3,5,1],[1,6,1]]
#原数组增加首项 0
nums=[0]+nums
n=len(nums)
#差分数组比原数组(增加完首项0)的长度多 1
diff=[0]*(n+1)
#求差分数组
for i in range(1,n):
diff[i]=nums[i]-nums[i-1]
#进行三次操作
for i,(l,r,a) in enumerate(ops):
diff[l]+=a
diff[r+1]-=a
#根据差分数组求原始数组
for i in range(1,n):
nums[i]=nums[i-1]+diff[i]
return nums[1:]
[复杂度分析]:
- 时间复杂度:T(n)=O(n),n 为数组 nums 的长度
- 空间复杂度:S(n)=O(n),差分数组空间。
2. 题目链接:1109. 航班预订统计
这里有 n 个航班,它们分别从 1 到 n 进行编号。
有一份航班预订表 bookings ,表中第 i 条预订记录 bookings[i] = [firsti, lasti, seatsi] 意味着在从 firsti 到 lasti (包含 firsti 和 lasti )的 每个航班 上预订了 seatsi 个座位。
请你返回一个长度为 n 的数组 answer,里面的元素是每个航班预定的座位总数。
示例 1:
输入:bookings = [[1,2,10],[2,3,20],[2,5,25]], n = 5
输出:[10,55,45,25,25]
解释:
航班编号 1 2 3 4 5
预订记录 1 : 10 10
预订记录 2 : 20 20
预订记录 3 : 25 25 25 25
总座位数: 10 55 45 25 25
因此,answer = [10,55,45,25,25]
[题目分析]:根据题意,很明显多次都一个数组的区间进行了加数操作,因此考虑使用差分数组。没有原始数组,但是通过题意可以的原始数组航班座位数 ans[i],表示第 i 个航班上预定的座位数。那么在预定之前,应该都为 0,因此原始数组为元素全为 0 的数组,那么差分数组也是数值均为 0 的数组。
有了差分数组,在进行加数操作以及根据公式求原数组即可。
[算法]:初始化原数组和差分数组,元素均为 0,只是初始数组的长度为 n+1,因为要存放 ans[n],则差分数组的长度就为 n+2。然后按照类型 2 的步骤即可。
点击查看代码
class Solution:
def corpFlightBookings(self, bookings: List[List[int]], n: int) -> List[int]:
#初始化
ans=[0]*(n+1)
dis=[0]*(n+2)
#加数操作
for i,(first,last,seats) in enumerate(bookings):
dis[first]+=seats
dis[last+1]-=seats
#根据公式求原数组
for i in range(1,n+1):
ans[i]=ans[i-1]+dis[i]
return ans[1:]
[复杂度分析]:
- 时间复杂度:T(n)=O(n+m),n 为数组nums的长度,m 为 bookings 数组的长度。
- 空间复杂度:S(n)=O(n),数组空间。
3. 题目链接:1674. 使数组互补的最少操作次数
给你一个长度为 偶数 n 的整数数组 nums 和一个整数 limit 。每一次操作,你可以将 nums 中的任何整数替换为 1 到 limit 之间的另一个整数。
如果对于所有下标 i(下标从 0 开始),nums[i] + nums[n - 1 - i] 都等于同一个数,则数组 nums 是 互补的 。例如,数组 [1,2,3,4] 是互补的,因为对于所有下标 i ,nums[i] + nums[n - 1 - i] = 5 。
返回使数组 互补 的 最少 操作次数。
[题目分析]:如何找到所有位置的互补的数以及各自需要操作的次数,是解决问题的两大关键。
根据要求,1 <= nums[i] <= limit
,则对于任意 i、n-i-1 位置上的两个数 A、B 其和的范围为 [2, 2*limit]
,即这个区间内的所有数都有可能为互补数的和。那么任意两个数变为该区间中任一个数,需要进行多少次操作?
有以下三种情况:
(1)互补数的和就是 A+B ,两个数不用操作,即操作 0 次。
(2)互补数的和范围在 [1+min(A,B), limit+max(A,B)]
且不等于 A+B 的区间内,两个数只有一个改变,即操作 1 次。
因为: 1<=A,B<=limit
,假设 A=min(A,B)
,则 B>A>=1,即 B>1,若和为1+min(A,B)
,则 B 需要变成 1,其他不等于 B 的数同理。 对于limit+max(A,B)
同理可证。
(3)互补数的和范围在 [2, 2*limit] 且排除(1)、(2)的区间内,两个数均要改变,即操作两次。
通过情况分析,我们可以使用一个 res[x] 的数组,x 表示 nums[i] + nums[n - 1 - i] 的和,则 x 的有效范围是 [2, 2*limit],其值表示 nums[i] + nums[n - 1 - i] 的和为 x 操作的次数。而根据上面三种情况,我们需要对该数组的不同区间进行加数操作:
(1)对区间 [2, 2*limit] 内所有的数加 2,因为要操作两次
(2)对区间 [1+min(A,B), limit+max(A,B)] 内所有的数减 1,因为在(1)中已经加 2 了,而该区间表示只用操作 1 次,因此 2-1=1,需要减 1.
(3)对区间 [A+B] 的数减 1,原因同(2)。
[算法]:原数组即为 res,初始原数组和差分数组均为 0 数组,因为原数组表示操作数,初始为 0。依次遍历 nums[i] + nums[n - 1 - i] 直至两数相遇,分别令两数为 A、B,每次都对三个区间进行差分数组加数操作,最后再根据公式求得原数组,返回其最小值
点击查看代码
def minMoves(self, nums: List[int], limit: int) -> int:
#初始化,注意长度关系
#res=[0]*(2*limit+1)
diff=[0]*(2*limit+2)
n=len(nums)
for i in range(n//2):
first,second=nums[i],nums[n-1-i]
#区间(1) 差分数组加数操作
l,r=2,2*limit
diff[l]+=2
diff[r+1]-=2
#区间(2)
l,r=1+min(first,second),limit+max(first,second)
diff[l]+=-1
diff[r+1]-=-1
#区间(3)
l=r=first+second
diff[l]+=-1
diff[r+1]-=-1
#为节省空间,可不用求出原数组,只保存其中最小值即可
ans,sm=n,0
for i in range(2,2*limit+1):
sm+=diff[i]
#res[i+1]=res[i]+diff[i]
ans=min(ans,sm)
return ans
[复杂度分析]:
- 时间复杂度:T(n)=O(n),n 为数组nums的长度
- 空间复杂度:S(n)=O(n),差分数组空间