尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

差分法:高效解决数组区间批量修改的算法利器

差分法:高效解决数组区间批量修改的算法利器 1. 从“笨办法”到“巧方法”为什么我们需要差分法在数据处理、算法竞赛甚至是日常的表格统计工作中我们经常会遇到一类让人头疼的问题频繁地对一个数组或序列的某个连续区间进行“批量修改”。比如给你一个长度为n的数组arr初始值都是0然后给你m个操作每个操作要求你把从下标L到R的所有元素都加上一个值C。操作全部完成后问你最终数组里每个位置的值是多少。最直接、最“老实”的做法是什么当然是每次操作都老老实实地写一个循环从L跑到R把每个arr[i]都加上C。这个方法的代码简单易懂但它的时间复杂度是O(m * n)。如果m和n都很大比如都是10万那么总操作次数就可能高达100亿次这在绝大多数编程环境下都会超时程序会卡死。差分法就是为了优雅且高效地解决这类“区间批量增减”问题而生的。它的核心思想不是直接去操作原始数组而是通过维护一个“变化量”的数组将原本需要对区间内每个元素进行的O(n)次操作压缩成仅仅修改两个端点的O(1)次操作。等所有修改指令都下达完毕后再通过一次“整合”运算一次性得到最终所有位置的结果。这个“整合”过程时间复杂度是O(n)。所以总的时间复杂度就从暴力法的O(m * n)优化到了O(m n)效率的提升是指数级的。你可以把它想象成管理一个大型仓库的库存。原始数组就像是每个货架的实时库存量。如果有一批货物要从A区搬到B区相当于给A区减B区加笨办法是跑去每个货架清点并修改记录。而差分法则像是在仓库门口放一个“今日出入库总表”只在A区入口记一笔“出库”在B区入口记一笔“入库”。等一天工作结束再拿着这个总表去更新每个货架的详细库存。显然后者的效率高得多尤其是在出入库操作非常频繁的时候。2. 差分数组的构建与核心原理拆解理解了差分的动机我们来看看它的数学和编程本质。差分是前缀和的“逆运算”。前缀和数组preSum[i]表示原数组arr从开头到第i个位置通常包括i的所有元素之和。而差分数组diff其定义是diff[i] arr[i] - arr[i-1]其中我们约定arr[-1] 0。也就是说差分数组存储的是相邻两个元素的差值。2.1 差分数组的构建给定一个原始数组arr构建其差分数组diff的公式非常简单diff[0] arr[0] // 对于第一个元素它和“虚拟的”前一个元素0的差就是它本身 diff[i] arr[i] - arr[i-1] // 对于 i 0 的元素这个过程的时间复杂度是O(n)。2.2 差分法的核心魔法区间修改现在假设我们要对原数组arr的区间[L, R]注意在编程中通常使用从0开始的索引内的所有元素都加上一个常数C。按照原始数组的思维我们需要执行for i in range(L, R1): arr[i] C这需要R-L1次操作。而使用差分数组我们只需要执行两步diff[L] C if R1 len(diff): diff[R1] - C只需要2次操作为什么这样是可行的我们来推导一下根据差分数组的定义原数组arr其实是差分数组diff的前缀和arr[i] diff[0] diff[1] ... diff[i]当我们对diff[L]加上C根据上面的前缀和公式这意味着从i L开始往后所有的arr[i]在计算时都会多加上一个C。因为diff[L]被包含在了arr[L], arr[L1], ..., arr[n-1]每一个元素的计算中。但这不仅仅影响了[L, R]区间它把从L到数组末尾的所有元素都加了C。为了把影响范围精确地控制在[L, R]我们需要在R1的位置“打一个补丁”即diff[R1] - C。这个操作的效果是从i R1开始往后所有的arr[i]在计算时都会减掉一个C。这样一加一减对于i R1的元素净变化为(C) (-C) 0。而对于i在[L, R]区间的元素它们只受到了diff[L] C的影响因此都增加了C。这个过程完美地将一个区间操作转化为了两个单点操作。所有m个区间修改操作都只需要在差分数组上进行2m次O(1)的单点更新。2.3 从差分数组还原最终结果在所有修改操作都施加到差分数组diff上之后我们如何得到最终的原数组arr呢正如前面所说执行一次前缀和运算即可arr[0] diff[0] for i in range(1, n): arr[i] arr[i-1] diff[i]这个过程的时间复杂度是O(n)。所以整个算法的流程就是构建差分 - 多次O(1)区间更新 - 一次前缀和还原。其效率优势在多次更新时无比巨大。3. 一维差分法的实战例题与代码实现光说不练假把式我们通过一个经典例题来彻底掌握一维差分。3.1 例题描述假设有一个长度为n的数组初始值全为0。接下来进行m次操作每次操作给出三个整数l,r,c表示将数组中下标从l到r包含两端的每个数都加上c。请你输出进行完所有操作后的数组。输入格式第一行包含两个整数n和m。接下来m行每行包含三个整数l,r,c。输出格式共一行包含n个整数表示最终数组。数据范围1 ≤ n, m ≤ 1000001 ≤ l ≤ r ≤ n−1000 ≤ c ≤ 1000注意这里的l和r通常指的是从1开始计数的位置这与我们编程中从0开始的索引有细微差别需要做简单的转换。3.2 解题思路与代码实现根据差分法的原理我们完全不需要一个真实的、初始全为0的arr数组。因为差分数组diff初始也全为0这正好对应了原数组全为0的状态。步骤初始化一个长度为n2的差分数组diff多开两位是为了方便处理r1的边界避免判断。循环读取m次操作(l, r, c)。将diff[l] c将diff[r1] - c所有操作完成后对diff数组求前缀和得到的结果就是最终的原数组。输出前n个元素。以下是Python代码实现def main(): # 读取n和m n, m map(int, input().split()) # 初始化差分数组长度为 n2 以避免边界检查 diff [0] * (n 2) # 进行m次操作 for _ in range(m): l, r, c map(int, input().split()) # 注意题目输入是从1开始计数我们的数组索引从1开始使用0位置空出或作为辅助 diff[l] c diff[r 1] - c # r1可能等于n1因为我们开了n2的长度所以安全 # 通过前缀和还原原数组并输出 arr [0] * (n 1) # arr也使用1-based索引方便理解 for i in range(1, n 1): arr[i] arr[i - 1] diff[i] print(arr[i], end if i n else \n) if __name__ __main__: main()代码要点与避坑指南索引转换这是新手最容易出错的地方。题目说“下标从l到r”通常意味着从1开始计数。我们的diff和arr数组最好也从索引1开始使用索引0留空或作为计算起点。这样l和r就可以直接用作数组下标心智负担最小。数组大小因为我们需要操作diff[r1]所以数组大小至少要是n2如果使用1-based索引。多开一点空间避免复杂的边界条件判断是竞赛和工程中的常见技巧。输入输出效率在Python中当n和m很大时使用sys.stdin.read()一次性读取所有数据再解析会比循环调用input()快很多。但在本题数据范围内input()足够。还原与输出合并我们可以在计算前缀和的过程中直接输出结果无需额外存储最终数组节省空间。4. 差分法的多维扩展二维差分详解差分法的威力不仅限于一维数组。在图像处理、矩阵计算、二维区域统计等问题中我们经常会遇到需要对一个二维矩阵的某个子矩形区域进行批量加减的操作。二维差分就是解决这类问题的利器。4.1 二维差分数组的定义假设我们有一个二维矩阵原数组a[i][j]其对应的二维差分数组d[i][j]如何定义呢我们可以类比一维差分当前值减前一个值。在二维中一个点的值可以看作是从左上角(1,1)到该点(i,j)的矩形区域内所有差分值的“积分”即二维前缀和。更形式化地说如果我们令s[i][j]是差分数组d的二维前缀和即s[i][j] Σ_{x1}^{i} Σ_{y1}^{j} d[x][y]那么我们的目标是让这个前缀和s[i][j]等于原数组a[i][j]。因此差分数组d可以通过原数组a逆向推导出来但更实用的方法是直接记住它对区间操作的修改方式。4.2 二维区间的修改操作假设我们要对原矩阵a中以(x1, y1)为左上角(x2, y2)为右下角的矩形区域内所有元素都加上c。在二维差分数组d上需要进行四次操作d[x1][y1] c d[x1][y21] - c d[x21][y1] - c d[x21][y21] c这被称为“二维差分的核心公式”。我们可以这样理解d[x1][y1] c相当于对从(x1, y1)开始到矩阵右下角整个大区域都加了c。d[x1][y21] - c为了消除对y方向超出y2的部分的影响。d[x21][y1] - c为了消除对x方向超出x2的部分的影响。d[x21][y21] c由于步骤2和3重复减去了(x21, y21)开始的右下角区域所以需要加回来一次保证该区域不受影响。这四次操作后只有目标矩形区域[x1, x2] x [y1, y2]内的点在计算二维前缀和时净变化为c区域外的点净变化为0。4.3 从二维差分还原矩阵在所有修改操作完成后我们需要通过计算二维前缀和从差分数组d得到最终的原矩阵a。 二维前缀和的递推公式为s[i][j] s[i-1][j] s[i][j-1] - s[i-1][j-1] d[i][j]这里的s[i][j]就是我们最终想要的a[i][j]。计算时通常需要先处理第一行和第一列的边界情况或者像一维一样让数组下标从1开始并初始化s[0][*] s[*][0] 0。4.4 二维差分实战例题题目给定一个n x m的零矩阵进行q次操作。每次操作将一个以(x1, y1)为左上角(x2, y2)为右下角的子矩阵中的所有元素加上一个常数c。输出最终矩阵。Python实现def main(): n, m, q map(int, input().split()) # 多开一行一列方便处理边界 diff [[0] * (m 2) for _ in range(n 2)] # 进行q次区间修改 for _ in range(q): x1, y1, x2, y2, c map(int, input().split()) diff[x1][y1] c diff[x1][y2 1] - c diff[x2 1][y1] - c diff[x2 1][y2 1] c # 计算二维前缀和得到原矩阵 a [[0] * (m 1) for _ in range(n 1)] for i in range(1, n 1): for j in range(1, m 1): # 前缀和公式 a[i][j] a[i - 1][j] a[i][j - 1] - a[i - 1][j - 1] diff[i][j] print(a[i][j], end if j m else \n) if __name__ __main__: main()实操心得数组大小同样为了安全地使用x21和y21差分矩阵diff需要开(n2) x (m2)的大小。索引习惯强烈建议在二维差分/前缀和问题中坚持使用1-based索引。这能让公式s[i][j] s[i-1][j] s[i][j-1] - s[i-1][j-1] d[i][j]对所有i, j 1都成立无需处理繁琐的边界判断。空间与时间二维差分将区间修改的复杂度从O(q * n * m)优化到了O(q n * m)。在n, m, q达到几千的量级时暴力法完全不可行而差分法依然游刃有余。5. 差分法的变种与应用场景深度剖析差分法不仅仅是一个简单的算法模板其思想可以灵活变通应用于多种场景。5.1 差分数组的初始化在我们之前的例子中原数组初始都是0所以差分数组自然也是全0。但如果原数组有一个非零的初始状态init_arr怎么办有两种处理方式视为首次区间操作将构建初始数组的过程看作是进行了n次区间操作每次对区间[i, i]加上init_arr[i]。这样可以直接使用空的差分数组开始。直接构造差分数组根据定义diff[i] init_arr[i] - init_arr[i-1]来初始化。在代码实现上可以巧妙地通过一次插入操作来完成diff [0] * (n 2) for i in range(1, n 1): # 假设init_arr是1-based的初始数组 diff[i] init_arr[i] diff[i 1] - init_arr[i]这相当于对每个位置i进行了[i, i]区间的加操作。我个人更推荐第一种理解方式概念上更统一。5.2 差分与前缀和的结合使用差分和前缀和是一对互逆的操作。有些问题需要先使用前缀和进行快速查询再使用差分进行快速修改。例如维护一个数组需要支持两种操作1查询区间和2区间增加一个值。这就是经典的“树状数组”或“线段树”所能解决的问题。而差分数组结合前缀和可以高效处理“先进行所有修改最后进行所有查询”的离线场景。如果操作是在线、混合的则需要更复杂的数据结构。5.3 应用场景举例航班预订统计有n个航班预订记录bookings[i] [first_i, last_i, seats_i]表示在first_i到last_i的每个航班上都预订了seats_i个座位。求每个航班最终的预订总数。这就是标准的一维差分应用题。会议室安排给定若干会议的时间区间[start_i, end_i)问同一时间正在进行的会议最多有多少个可以将每个会议开始时间点1结束时间点-1然后求前缀和的最大值。这本质上是将“区间”转化为差分事件点。图像模糊处理对图像的每个像素点将其值替换为周围一个矩形区域内像素值的平均值。这可以先通过二维前缀和快速计算任意矩形区域的和然后再进行赋值。虽然不直接是差分但思想同源快速区域统计。资源分配与统计在游戏开发或模拟系统中某个BUFF效果在特定时间区间内生效如每秒回血差分法可以高效计算任意时刻的总生效效果。5.4 差分法的局限与注意事项离线算法标准的差分法适用于“先修改后查询”的离线场景。如果修改和查询穿插进行则需要支持动态区间修改和单点查询的数据结构此时差分数组需要结合树状数组来实现。数据类型注意加减操作可能导致数据溢出尤其是在多次大量加减后。要根据题目要求使用合适的数据类型如Python的int无限精度但C/Java中可能需要使用long long。边界检查始终牢记对r1或x21,y21的索引进行判断防止数组越界。多开数组空间是简单有效的策略。从1开始索引在处理涉及区间的问题时坚持使用1-based索引可以极大减少由下标转换引起的错误让代码逻辑更清晰。输入时如果是从0开始可以先将其转换为1-based。掌握差分法就像掌握了一把解决批量区间更新问题的瑞士军刀。它用简单的预处理和一次整合将大量重复操作打包处理体现了“空间换时间”和“延迟计算”的经典算法思想。理解其原理后无论是二维、三维扩展还是与其他算法如前缀和、树状数组结合你都能触类旁通。下次再遇到需要“给一片区域都加上某个值”的问题时不妨先想想能不能用差分
返回列表