【数据结构】(Python)差分数组。差分数组与树状数组结合

差分数组:

  • 基于原数组构造的辅助数组。
  • **用于区间修改、单点查询。**区间修改的时间复杂度O(1)。单点查询的时间复杂度O(n)。
  • 差分数组的元素:第一个元素等于原数组第一个元素,从第二个元素开始是原数组对应下标的元素与前一个元素的差(即原数组相邻元素的差)。
  • 区间修改:通过修改差分数组中区间的两端点,可快速地修改原数组中整个区间的值。具体操作:修改差分数组中起始下标的元素,恢复结束下标的后一个元素。
  • 单点查询:通过差分数组查询原数组中的元素。具体操作:差分数组中从开头到对应下标元素的累和(前缀和)。
  • 注:① 差分与前缀和互为逆运算 (差分数组是原数组的差分,原数组是差分数组的前缀和)**。**② 本文原数组为a,差分数组为d,下标均从0开始。
  • 补充:二维差分数组,类似一维差分数组,注意行和列。
  • 补充:差分数组结合树状数组 ,可提高效率,也可实现区间修改、区间查询

差分数组的元素:

  • 差分数组的第一个元素(下标0):原数组的第一个元素(下标0)。即d[0]=a[0]。
  • 差分数组的下标i 的元素:原数组的下标i 的元素与前一个元素的差。即d[i]=a[i]-a[i-1]。
python 复制代码
# a为原数组,d为差分数组
n = len(a)
d = [0] * n        # 差分数组初始化
d[0] = a[0]
for i in range(1, n):
    d[i] = a[i] - a[i - 1]

区间修改:区间[l,r]

通过修改差分数组中区间的两端点,可快速修改原数组中整个区间的值。

  • 目标:原数组下标l到下标r都修改。例如:区间[l,r]的元素都+val,即a[l]+val,a[l+1]+val,...,a[r]+val。
  • 具体操作:修改差分数组中区间两端点的值,即差分数组下标l修改、下标r+1恢复。例如:d[l]+val,d[r+1]-val。

此处,区间修改由update函数实现。参数:l为区间起始(左侧)下标,r为区间结束(右侧)下标,val为值(例如:增加的值)。

python 复制代码
def update(l:int, r:int, val:int):
    """区间修改。l为区间起始(左侧)下标,r 为区间结束(右侧)下标,val为值"""
    d[l] += val
    d[r + 1] -= val

单点查询(前缀和):

通过差分数组,查询原数组中的元素。

  • 方法一:前缀和。原数组下标i 的元素为差分数组从下标0到下标i 的元素累和。即a[i]=d[0]+d[1]+...+d[i]。
  • 方法二:原数组下标i 的元素为原数组下标i-1 的元素加上差分数组下标i 的元素。即a[i]=a[i-1]+d[i]。
  • 注:原数组第一个元素等于差分数组第一个元素,即a[0]=d[0]。

此处,单点查询由query函数实现。参数:i为下标。

python 复制代码
# 方法一:前缀和
def query(i:int):
    """单点查询。i为下标,ans为合计"""
    ans = 0
    while i >= 0:
        ans += d[i]
        i -= 1
    return ans

# 方法二
a[0] = b[0]
# n为数组长度
for i in range(1, n):
    a[i] = a[i - 1] + d[i]

补充:二维差分数组:

注:为了便于二维差分数组的理解,此处,二维原数组第一行(行下标0)和第一列(列下标0)的元素均为0。对应二维差分数组的第一行(行下标0)和第一列(列下标0)的元素也均为0。

1、二维差分数组的元素:

python 复制代码
# a为二维原数组,d为二维差分数组
n = len(a)            # 矩阵行数
m = len(a[0])         # 矩阵列数
for i in range(1, n):
    for j in range(1, m):
        d[i][j] = a[i][j] - a[i - 1][j] - a[i][j - 1] + a[i - 1][j - 1]

2、二维差分数组的区间修改:

通过修改差分数组矩阵端点的值,修改原数组整个矩阵区间的值。

python 复制代码
def update(i1:int, j1:int, i2:int, j2:int, val:int)
    """(矩阵)区间修改。(i1,j1)为矩阵起始位置,(i2,j2)为矩阵结束位置"""
    d[i1][j1] += val
    d[i2 + 1][j1] -= val
    d[i1][j2 + 1] -= val
    d[i2 + 1][j2 + 1] += val

3、二维差分数组的单点查询(前缀和):

通过差分数组,获取原数组中的元素。

  • 方法一:原数组(i, j)的值为从差分数组开头位置(0, 0)到对应位置(i, j)的所有元素的总和。即a[i][j]=d[0][0]+d[0][1]+...+d[0][j]+d[1][0]+d[1][1]+...+d[1][j]+...+d[i][0]+d[i][1]+...+d[i][j]。
  • 方法二:原数组(i, j)的值为原数组前一行(i-1,j)和原数组前一列(i, j-1)与差分数组中(i, j)的总和。但因原数组(i-1, j)和原数组(i, j-1)两个元素均加上了原数组(i-1, j-1)的值,需减除多加的一次。即a[i][j]=a[i- 1][j] + a[i][j - 1] - a[i - 1][j - 1] + d[i][j]。
python 复制代码
def query(i:int, j:int, a:list):
    """查询原数组的值。i为行下标,j为列下标,a为原数组"""
    return a[i- 1][j] + a[i][j - 1] - a[i - 1][j - 1] + d[i][j]

案例:【力扣】

(难度:中等)1109. 航班预订统计

【Python3】设置差分数组(元素都为0),使用差分数组修改航班区间两端点进行修改整个区间,再计算差分数组的前缀和。

注意:航班编号从1开始,差分数组下标从0开始。

python 复制代码
class Solution:
    def corpFlightBookings(self, bookings: List[List[int]], n: int) -> List[int]:
        # 差分数组
        d = [0] * n
        for l, r, val in bookings:
            d[l - 1] += val
            if r < n:
                d[r] -= val
        # 计算差分数组的前缀和
        res = [0] * n
        res[0] = d[0]
        for i in range(1, n):
            res[i] = res[i - 1] + d[i]
        return res

可以在差分数组的基础上直接计算前缀和,无需额外占用内存空间开辟新数组。

python 复制代码
class Solution:
    def corpFlightBookings(self, bookings: List[List[int]], n: int) -> List[int]:
        # 差分数组
        d = [0] * n
        for l, r, val in bookings:
            d[l - 1] += val
            if r < n:
                d[r] -= val
        # 在差分数组的基础上,直接计算前缀和
        for i in range(1, n):
            d[i] += d[i - 1]
        return d

注:差分数组结合树状数组,可提高效率。题解详见本文"差分数组和树状数组的结合"中的案例。

(难度:困难)1526. 形成目标数组的子数组最少增加次数

【Python3】

解题思路:反向思考,使用差分数组将目标数组的所有元素都变为0。差分数组中的元素都变为0,则对应目标数组的元素也都为0。(因目标数组中的元素是差分数组中元素的前缀和)

  • 根据提示,已知目标数组的元素都为正数,则其差分数组的合计(即目标数组中最后一个元素)一定为正数。
  • 差分数组的第一个元素(即目标数组的第一个元素)也一定为正数。差分数组的其他元素值(差分值)若为负数,则其左侧必定有差分值为正数。
  • 区间修改时,修改差分数组中区间起始下标的值和恢复区间结束下标后一个的值。本题中差分数组的区间起始下标的值减1,区间结束下标后一个的值加1。

本题,统计操作次数即为统计差分值为正数的总和。

  • 若差分值是正数,正数是多少 就操作多少次(每次减1),最终变为0。
  • 若差分值为负数,则其左侧的正数差分值减1的同时 其会加1,最终变为0,无需统计操作次数。
  • 若正数差分值的合计比负数差分值的合计多,则多的正数差分值减1时,虚拟的(不存在)下标n的值加1。
  • 因差分数组的合计为正数,则不存在负数差分值的合计比正数差分值的合计多。
python 复制代码
class Solution:
    def minNumberOperations(self, target: List[int]) -> int:
        n = len(target)
        # 差分数组
        d = [0] * n       # 差分数组初始化
        d[0] = target[0]
        for i in range(1, n):
            d[i] = target[i] - target[i - 1]
        # 统计操作次数(统计差分值为正数的总和)
        ans = 0
        # 遍历差分数组
        for i in range(n):
            # 差分值为正数,则为该元素变为0的操作次数
            ans += max(d[i], 0)
        return ans

(简化) 可以不额外占用内存开辟差分数组。可以遍历目标数组时,依次计算对应的差分值,并判断且统计操作次数。时间复杂度O(n),空间复杂度O(1)。

python 复制代码
class Solution:
    def minNumberOperations(self, target: List[int]) -> int:
        n = len(target)
        # ans统计操作次数,初始为目标数组第一个元素值
        ans = target[0]
        # 从下标1开始遍历目标数组,获取对应下标的差分值
        for i in range(1, n):
            # 差分值为正数,则为该元素变为0的操作次数
            ans += max(target[i] - target[i - 1], 0)
        return ans

差分数组和树状数组的结合:

差分数组中,区间修改的时间复杂度为O(1),但单点查询的时间复杂度O(n)。为提高查询的效率,结合树状数组,时间复杂度为O(logn)。

  1. 原数组a通过差分获得差分数组d。
  2. 通过树状数组t的单点修改(树状数组维护)来维护差分数组d。
  3. 使用差分数组的区间修改,而差分数组的区间两端点的修改都通过树状数组的单点修改来完成。
  4. 使用树状数组的单点查询(前缀和)获得差分数组的前缀和即原数组中的元素,以此实现差分数组的单点查询。
  5. 补充:若再额外维护一个数组e,可使用树状数组的单点查询(前缀和)获取原数组的前缀和,实现区间查询。
  6. 注意:原数组a、差分数组d、数组e的下标从0开始,树状数组下标从1开始。

1、差分数组(初始化,维护差分数组的元素)

python 复制代码
# 差分数组初始化
n = len(a)
d = [0] * n
# 维护差分数组的元素
d[0] = a[0]
for i in range(1, n):
    d[i] = a[i] - a[i - 1]

2、树状数组(初始化,维护树状数组的元素)

  • 树状数组下标从1开始,为方便操作,树状数组长度n+1(下标0存在但不使用)。
  • 差分数组下标 i,树状数组下标 i+1。
  • 使用树状数组维护差分数组,此时参数val为差分数组的元素。
  • 即树状数组维护差分数组update(i+1,d[i])。
python 复制代码
# 树状数组
n = len(a)
t = [0] * (n + 1)         # 树状数组初始化,下标从1开始(为方便操作,下标0存在但不使用)

# 树状数组的单点修改(使用树状数组维护差分数组,因此,val为差分数组的元素)
def update(i:int, val:int):
    """树状数组中的单点修改(树状数组维护)"""
    while i <= n:
        t[i] += val
        i += i & -i        # lowbit函数:i & -i,此处未单独设置函数

# 实际维护执行
for i in range(n):
    update(i + 1, d[i])    # 树状数组下标从1开始

3、差分数组的区间修改(通过树状数组的单点修改完成)

差分数组的区间修改,只需修改差分数组中的区间两端点。而此处,区间两端点则都通过树状数组的单点修改完成。

注意下标。

python 复制代码
# 差分数组的区间修改:(区间[l,r],修改值val)
# 此处区间为树状数组对应的下标,而差分数组对应区间为[l-1, r-1]
update(l, val)
update(r + 1, -val)

4、树状数组的前缀和(获取原数组中的元素)

通过树状数组的前缀和(区间查询,区间[1,i])来获得差分数组的前缀和,而差分数组的前缀和为原数组中的元素。

以此实现差分数组的单点查询。

  • 差分数组下标 i,树状数组下标 i+1。
python 复制代码
# 树状数组的前缀和(区间查询,区间[1,i])
def query(i:int):
    """树状数组中计算前缀和"""
    ans = 0
    while i > 0:
        ans += t[i]
        i -= i & -i            # lowbit函数:i & -i,此处未单独设置函数
    return ans

# 实际查询执行
a[i] = query(i + 1)            # 查询原数组中下标i的值

案例:【力扣】

(难度:中等)1109. 航班预订统计

【Python3】差分数组和树状数组结合。即使用树状数组维护差分数组,使用差分数组的区间修改来快速修改整个区间的元素(通过树状数组的单点修改完成),再通过树状数组计算前缀和。

python 复制代码
# 类BIT(即树状数组)
class BIT:
    def __init__(self, n:int):
        self.n = n
        self.t = [0] * n

    def update(self, i:int, val:int):
        """单点修改"""
        while i < self.n:
            self.t[i] += val
            i += i & -i           # lowbit函数:i & -i。此处未单独设置函数 

    def query(self, i:int):
        """前缀和"""
        ans = 0
        while i > 0:
            ans += self.t[i]
            i -= i & -i           # lowbit函数:i & -i。此处未单独设置函数
        return ans
    

class Solution:
    def corpFlightBookings(self, bookings: List[List[int]], n: int) -> List[int]:
        # 使用树状数组维护差分数组(初始元素都为0)
        bitree = BIT(n + 1)
        # 差分数组的区间修改(使用树状数组的单点修改完成)
        for l, r, val in bookings:
            # 修改差分数组的区间起始的元素
            bitree.update(l, val)
            # 恢复区间结束的下一个元素。若区间结束元素是差分数组的最后一个元素,则无需恢复
            if r < n: bitree.update(r + 1, -val)
        # 差分数组的前缀和
        res = [0] * n
        for i in range(n):
            res[i] = bitree.query(i + 1)
        return res

补充:5、额外维护数组e(可获取原数组的前缀和)

使用两个树状数组分别维护差分数组和数组e,可实现区间修改、单点查询、区间查询。

  1. 原数组a通过差分获得差分数组d,并初始化数组e。
  2. 使用树状数组分别维护差分数组d和数组e。
  3. 区间修改,差分数组的区间两端点的修改都通过树状数组的单点修改来完成。同时数组e的区间也要做相应修改。
  4. 使用树状数组的单点查询(前缀和)获得差分数组的前缀和即原数组中的元素,以此实现单点查询。
  5. 使用树状数组的单点查询(前缀和)获得原数组的前缀和,以此实现区间查询。

因:原数组的前缀和:s[i] = a[0] + a[1] +...+ a[i]

差分数组的前缀和(原数组的元素):a[i] = d[0] + d[1] +...+ d[i]

因此:s[i] = a[0] + a[1] +...+ a[i]

= d[0] + (d[0] + d[1]) + ...+ (d[0] + d[1] +...+ d[i])

= (i+1)* (d[0] + d[1] +...+ d[i]) - (0 * d[0] + 1 * d[1] + ...+ i * d[i])

=

其中,为差分数组的前缀和,则维护额外的数组e(元素为 i * d[i]),获取数组e的前缀和。

注意:原数组a和差分数组d和数组e下标从0开始。而树状数组下标从1开始,注意实际对应下标。

(5-1)差分数组d和数组e的初始化,以及维护差分数组元素

python 复制代码
# 差分数组d和数组e初始化,a为原数组
n = len(a)
d = [0] * n
e = [0] * n

# 维护差分数组元素
d[0] = a[0]
for i in range(1, n):
    d[i] = a[i] - a[i - 1]

(5-2)两个树状数组的初始化,以及使用树状数组维护差分数组d和数组e

  • 两个树状数组分别用于维护差分数组(元素d[i])和数组e(元素 i * d[i])。
  • 差分数组和数组e下标 i,树状数组下标 i+1。
python 复制代码
# 两个树状数组初始化
n = len(a)
t1 = [0] * (n + 1)         # 用于维护差分数组d
t2 = [0] * (n + 1)         # 用于维护数组e


def update(i:int, val:int, t:list):
    """树状数组中的单点修改(树状数组维护)"""
    while i <= n:             # 此处,树状数组的长度为n+1,最后一个元素下标为n
        t[i] += val           # t为树状数组
        i += i & -i           # lowbit函数:i & -i,此处未单独设置函数

# 实际维护执行
# 差分数组和数组e的下标都从0开始,树状数组下标从1开始
for i in range(n):
    update(i + 1, d[i], t1)                # 维护差分数组d
    update(i + 1, d[i] * i, t2)            # 维护数组e

(5-3)差分数组的区间修改(使用树状数组的单点修改完成)

  • 树状数组区间[l,r],树状数组修改下标 l 和下标 r+1。
  • 即差分数组和数组e对应区间[l-1, r-1],差分数组和数组e中对应修改 下标 l-1 和下标 r。
  • 注意:数组e的元素为d[i]*i。差分数组修改的同时,数组e也修改,且数组e的修改值需乘以对应下标 i 。
python 复制代码
# 差分数组的区间修改(使用树状数组的单点修改完成),区间[l,r]
# 树状数组下标从1开始,原数组和差分数组和数组e下标从0开始。
# 此处区间为树状数组对应的下标,而差分数组和数组e对应区间为[l-1, r-1]
update(l, val, t1)
update(r + 1, -val, t1)
update(l, val * (l - 1), t2)       # 树状数组下标为l,而数组e对应下标为l-1
update(r + 1, -val * r, t2)        # 树状数组下标为r+1,而数组e对应下标为r

(5-4)前缀和(获取原数组中的元素,以及获取原数组的前缀和)

  1. 通过树状数组获取差分数组的前缀和,差分数组的前缀和为原数组中的元素,即a[i] = d[0]+d[1]+...+d[i]。
  2. 通过树状数组获取原数组的前缀和,即 ,此公式 i 从0开始。
  3. 原数组下标 i,树状数组下标 i+1。
python 复制代码
def query(i:int, t:list):
    """树状数组中计算前缀和"""
    ans = 0
    while i > 0:
        ans += t[i]           # t为树状数组
        i -= i & -i           # lowbit函数:i & -i,此处未单独设置函数
    return ans


# 树状数组下标从1开始,原数组和差分数组和数组e下标从0开始
# 实际获取原数组的值
for i in range(n):
    a[i] = query(i + 1, t1)

# 实际获取原数组的前缀和
for i in range(n):
    s[i] = (i + 1) * query(i + 1, t1) - query(i + 1, t2)

(5-5)区间和(获取原数组的区间和)

获取原数组的区间[l,r]的元素和,即s[l,r]=s[r]-s[l-1]。

因树状数组下标从1开始,原数组下标从0开始。对应树状数组的区间为[l+1,r+1]。

python 复制代码
def range_query(l:int, r:int):
    """获取原数组的区间[l,r]的元素和,s[l,r]=s[r]-s[l-1]"""
    # 原数组下标从0开始,树状数组下标从1开始。树状数组对应区间[l+1,r+1]
    return (r + 1) * query(r + 1, t1) - query(r + 1, t2) - (l * query(l, t1) - query(l, t2))

具体的树状数组知识点:树状数组

【Python】代码示例:

注:原数组a、差分数组d、数组e的下标从0开始,树状数组下标从1开始。

python 复制代码
# 类BIT(即树状数组)
class BIT:
    def __init__(self, n):
        self.n = n
        self.t = [0] * n

    def update(self, i, val):
        """单点修改(树状数组维护)"""
        while i < self.n:           # 此处,树状数组的长度为n,最后一个元素下标为n-1
            self.t[i] += val
            i += i & -i

    def query(self, i):
        """前缀和,区间[1,i]"""
        ans = 0
        while i > 0:
            ans += self.t[i]
            i -= i & -i
        return ans


# 主程序
def main():
    a = [5,2,6,1,7,9]
    n = len(a)
    # 打印原数组的元素
    print(f"原数组:                ", end="")
    for i in range(n):
         print(a[i], end="    ")
    print()
    
    # 差分数组d
    d = [0] * n
    d[0] = a[0]
    for i in range(1, n):
        d[i] = a[i] - a[i - 1]
    # 打印差分数组的元素
    print(f"差分数组:              ", end="")
    for i in range(n):
         print(d[i], end="    ")
    print()
    
    # 数组e(元素:d[i] * i)
    e = [0] * n
    
    # 使用树状数组维护差分数组d和数组e
    t1 = BIT(n + 1)
    t2 = BIT(n + 1)
    for i in range(n):
        t1.update(i + 1, d[i])
        t2.update(i + 1, d[i] * i)
        
    # 假设:原数组区间[1,3]都加10,修改差分数组的树状数组和数组e的树状数组
    # 树状数组对应区间[2,4]
    t1.update(2, 10)            # (差分数组)修改区间起始下标的元素
    t1.update(5, -10)           # (差分数组)修改区间结束下标的后一个元素
    t2.update(2, 10 * 1)        # (数组e)修改区间起始下标的元素
    t2.update(5, -10 * 4)       # (数组e)修改区间结束下标的后一个元素
    
    # 打印修改后原数组中所有元素(即差分数组的前缀和)
    print(f"修改后差分数组的前缀和:", end="")
    for i in range(n):
         a[i] = t1.query(i + 1)
         print(a[i], end="    ")
    print()
    # 获取原数组中下标2的元素
    print(f"原数组中下标2的元素:{a[2]}")
    
    # 打印修改后数组e的前缀和
    print(f"修改后数组e的前缀和:   ", end="")
    for i in range(n):
         e[i] = t2.query(i + 1)
         print(e[i], end="    ")
    print()
    
    # 获取原数组中下标2的前缀和
    print(f"修改后原数组的前缀和:  ", end="")
    s = [0] * n                # 记录原数组的前缀和
    for i in range(n):
        s[i] = (i + 1) * t1.query(i + 1) - t2.query(i + 1)
        print(s[i], end="    ")
    print()
    print(f"原数组中下标2的前缀和:{s[2]}")
    
    # 获取原数组中区间和,区间[2,5]
    rq = s[5] - s[1]
    print(f"原数组中区间和,区间[2,5]:{rq}")
    

# 主程序执行
main()

执行结果如下:

相关推荐
如竟没有火炬几秒前
LRU缓存——双向链表+哈希表
数据结构·python·算法·leetcode·链表·缓存
Greedy Alg3 分钟前
LeetCode 236. 二叉树的最近公共祖先
算法
掘根7 分钟前
【Qt】绘图
开发语言·qt
爱吃生蚝的于勒20 分钟前
【Linux】零基础学会Linux之权限
linux·运维·服务器·数据结构·git·算法·github
咖啡续命又一天27 分钟前
python 自动化采集 ChromeDriver 安装
开发语言·python·自动化
huohaiyu1 小时前
synchronized (Java)
java·开发语言·安全·synchronized
兮山与1 小时前
算法3.0
算法
_OP_CHEN1 小时前
C++基础:(九)string类的使用与模拟实现
开发语言·c++·stl·string·string类·c++容器·stl模拟实现
蓝天智能1 小时前
QT MVC中View的特点及使用注意事项
开发语言·qt·mvc
爱编程的化学家1 小时前
代码随想录算法训练营第27天 -- 动态规划1 || 509.斐波那契数列 / 70.爬楼梯 / 746.使用最小花费爬楼梯
数据结构·c++·算法·leetcode·动态规划·代码随想录