关于算法:那些小而美的算法技巧前缀和差分数组

4次阅读

共计 2786 个字符,预计需要花费 7 分钟才能阅读完成。

读完本文,你能够去力扣拿下如下题目:

1109. 航班预订统计

———–

前文 前缀和技巧详解 写过的前缀和技巧是十分罕用的算法技巧, 前缀和次要实用的场景是原始数组不会被批改的状况下,频繁查问某个区间的累加和

没看过前文没关系,这里简略介绍一下前缀和,外围代码就是上面这段:

class PrefixSum {
    // 前缀和数组
    private int[] prefix;
    
    /* 输出一个数组,结构前缀和 */
    public PrefixSum(int[] nums) {prefix = new int[nums.length + 1];
        // 计算 nums 的累加和
        for (int i = 1; i < prefix.length; i++) {prefix[i] = prefix[i - 1] + nums[i - 1];
        }
    }

    /* 查问闭区间 [i, j] 的累加和 */
    public int query(int i, int j) {return prefix[j + 1] - prefix[i];
    }
}

prefix[i] 就代表着 nums[0..i-1] 所有元素的累加和,如果咱们想求区间 nums[i..j] 的累加和,只有计算 prefix[j+1] - prefix[i] 即可,而不须要遍历整个区间求和。

本文讲一个和前缀和思维十分相似的算法技巧「差分数组」, 差分数组的次要实用场景是频繁对原始数组的某个区间的元素进行增减

比如说,我给你输出一个数组 nums,而后又要求给区间 nums[2..6] 全副加 1,再给 nums[3..9] 全副减 3,再给 nums[0..4] 全副加 2,再给 …

一通操作猛如虎,而后问你,最初 nums 数组的值是什么?

惯例的思路很容易,你让我给区间 nums[i..j] 加上 val,那我就一个 for 循环给它们都加上呗,还能咋样?这种思路的工夫复杂度是 O(N),因为这个场景下对 nums 的批改十分频繁,所以效率会很低下。

这里就须要差分数组的技巧,相似前缀和技巧结构的 prefix 数组,咱们先对 nums 数组结构一个 diff 差分数组,diff[i] 就是 nums[i]nums[i-1] 之差

int[] diff = new int[nums.length];
// 结构差分数组
diff[0] = nums[0];
for (int i = 1; i < nums.length; i++) {diff[i] = nums[i] - nums[i - 1];
}

通过这个 diff 差分数组是能够反推出原始数组 nums 的,代码逻辑如下:

int[] res = new int[diff.length];
// 依据差分数组结构后果数组
res[0] = diff[0];
for (int i = 1; i < diff.length; i++) {res[i] = res[i - 1] + diff[i];
}

这样结构差分数组 diff,就能够疾速进行区间增减的操作 ,如果你想对区间 nums[i..j] 的元素全副加 3,那么只须要让 diff[i] += 3,而后再让 diff[j+1] -= 3 即可:

原理很简略,回忆 diff 数组反推 nums 数组的过程,diff[i] += 3 意味着给 nums[i..] 所有的元素都加了 3,而后 diff[j+1] -= 3 又意味着对于 nums[j+1..] 所有元素再减 3,那综合起来,是不是就是对 nums[i..j] 中的所有元素都加 3 了

只有破费 O(1) 的工夫批改 diff 数组,就相当于给 nums 的整个区间做了批改。屡次批改 diff,而后通过 diff 数组反推,即可失去 nums 批改后的后果。

当初咱们把差分数组形象成一个类,蕴含 increment 办法和 result 办法:

class Difference {
    // 差分数组
    private int[] diff;

    public Difference(int[] nums) {
        assert nums.length > 0;
        diff = new int[nums.length];
        // 结构差分数组
        diff[0] = nums[0];
        for (int i = 1; i < nums.length; i++) {diff[i] = nums[i] - nums[i - 1];
        }
    }

    /* 给闭区间 [i,j] 减少 val(能够是正数)*/
    public void increment(int i, int j, int val) {diff[i] += val;
        if (j + 1 < diff.length) {diff[j + 1] -= val;
        }
    }

    public int[] result() {int[] res = new int[diff.length];
        // 依据差分数组结构后果数组
        res[0] = diff[0];
        for (int i = 1; i < diff.length; i++) {res[i] = res[i - 1] + diff[i];
        }
        return res;
    }
}

这里留神一下 increment 办法中的 if 语句:

public void increment(int i, int j, int val) {diff[i] += val;
    if (j + 1 < diff.length) {diff[j + 1] -= val;
    }
}

j+1 >= diff.length 时,阐明是对 nums[i] 及当前的整个数组都进行批改,那么就不须要再给 diff 数组减 val 了。

算法实际

这里看一下力扣第 1109 题「航班预订统计」:

函数签名如下:

int[] corpFlightBookings(int[][] bookings, int n)

这个题目就在那绕弯弯,其实它就是个差分数组的题,我给你翻译一下:

给你输出一个长度为 n 的数组 nums,其中所有元素都是 0。再给你输出一个 bookings,外面是若干三元组 (i,j,k),每个三元组的含意就是要求你给 nums 数组的闭区间 [i-1,j-1] 中所有元素都加上 k。请你返回最初的 nums 数组是多少?

PS:因为题目说的 n 是从 1 开始计数的,而数组索引从 0 开始,所以对于输出的三元组 (i,j,k),数组区间应该对应 [i-1,j-1]

这么一看,不就是一道规范的差分数组题嘛?咱们能够间接复用方才写的类:

int[] corpFlightBookings(int[][] bookings, int n) {
    // nums 初始化为全 0
    int[] nums = new int[n];
    // 结构差分解法
    Difference df = new Difference(nums);

    for (int[] booking : bookings) {
        // 留神转成数组索引要减一哦
        int i = booking[0] - 1;
        int j = booking[1] - 1;
        int val = booking[2];
        // 对区间 nums[i..j] 减少 val
        df.increment(i, j, val);
    }
    // 返回最终的后果数组
    return df.result();}

这道题就解决了。

其实我感觉差分数组和前缀和数组都是比拟常见且奇妙的算法技巧,别离实用不同的常见,而且是会者不难,难者不会。所以,对于差分数组的应用,你学会了吗?

学会的话,分享 / 点赞 / 在看三连走起?

正文完
 0