前缀和&差分

本文介绍了前缀和的概念及其在二维矩阵求和中的应用,通过预处理实现快速检索子矩阵的元素和。同时,展示了如何利用前缀和解决LeetCode上的304题,以及如何将其应用于随机选择问题,如在给定权重的数组中按概率选取元素。此外,还探讨了差分数组在处理区间增量修改问题上的优势,例如在航班预定系统和拼车场景中的应用。

前缀和

一个只用初始化一次,但是却要反复检索计算多次,需要在最开始做预处理。

=======================以下请看版权声明

for (int i = 0; i < nums.length; i++) {
      presum[i+1] = nums[i] + presum[i];
 }

作者:chefyuan
链接:https://leetcode-cn.com/problems/continuous-subarray-sum/solution/de-liao-wo-ba-qian-zhui-he-miao-de-gan-g-c8kp/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

=======================

=======================以下请看版权声明

将matrix[][]的前缀和数组定义为prefix[][],定义prefix[i][j]表示从[0, 0]位置到[i, j]位置的子矩阵所有元素的和。

                                               S(O,D)=S(O,C)+S(O,B)−S(O,A)+D

如果求 preSum[i][j]preSum[i][j] 表示的话,对应了以下的递推公式:

       preSum[i][j] = preSum[i - 1][j] + preSum[i][j - 1] - preSum[i - 1][j - 1] + matrix[i][j]

根据preSum求子矩形面积:

                                         S(A,D)=S(O,D)−S(O,E)−S(O,F)+S(O,G)

如果要求 [row1, col1][row1,col1] 到 [row2, col2][row2,col2] 的子矩形的面积的话,用 preSum 对应了以下的递推公式:

preSum[row2][col2] - preSum[row2][col1 - 1] - preSum[row1 - 1][col2] + preSum[row1 - 1][col1 - 1]

===================================

作者:fuxuemingzhu
链接:https://leetcode-cn.com/problems/range-sum-query-2d-immutable/solution/ru-he-qiu-er-wei-de-qian-zhui-he-yi-ji-y-6c21/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

所以以上方法用java代码表示就是如下所示:

    private int[][] preSum;

    public NumMatrix(int[][] matrix) {
        if (matrix.length > 0) {
            preSum = new int[matrix.length + 1][matrix[0].length + 1];
            for (int i = 0; i < matrix.length; i++) {
                for (int j = 0; j < matrix[0].length; j++) {
                    preSum[i+1][j+1] = preSum[i][j+1] + preSum[i+1][j]
                                       - preSum[i][j] + matrix[i][j];
                }
            }
        }
    }
    
    public int sumRegion(int row1, int col1, int row2, int col2) {
        return preSum[row2 + 1][col2 + 1] 
               - preSum[row2 + 1][col1] - preSum[row1][col2 + 1]
               + preSum[row1][col1];
    }

leetcode304

这道题可以借鉴在matrix中选择一块小矩阵,计算其中矩阵的元素和

class NumMatrix {
    int[][] sums;
    public NumMatrix(int[][] matrix) {
        int m = matrix.length;
        if (m > 0) {
            int n = matrix[0].length;
            sums = new int[m][n + 1];
            for (int i = 0; i < m; i++) {
                for (int j = 0; j < n; j++) {
                    sums[i][j + 1] = sums[i][j] + matrix[i][j];
                }
            }
        }
    }

    /**
    * row1 col1 左上角的坐标
    * row2 col2 右下角的坐标
    **/
    public int sumRegion(int row1, int col1, int row2, int col2) {
        int sum = 0;
        for (int i = row1; i <= row2; i++) {
            sum += sums[i][col2 + 1] - sums[i][col1];
        }
        return sum;
    }
}

如果针对一个矩阵,要找到这个矩阵中固定大小子矩阵的和,可以借鉴前缀和:

    int[][] sums;
    public int countMatrixPrefix(int[][] matrix, int cnt) {
        int m = matrix.length;
        if (m > 0) {
            int n = matrix[0].length;
            sums = new int[m][n + 1];
            for (int i = 0; i < m; i++) {
                for (int j = 0; j < n; j++) {
                    sums[i][j + 1] = sums[i][j] + matrix[i][j];
                }
            }
        }
        PriorityQueue<Integer> queue = new PriorityQueue<>(Collections.reverseOrder());
        for (int i = 0; i < matrix.length - cnt + 1; i++) {
            for (int j = 0; j < matrix[0].length - cnt + 1; j++) {
                queue.add(sumRegion(i, j, i + cnt - 1, j + cnt - 1));
            }
        }
        return queue.poll();
    }

    private int sumRegion(int row1, int col1, int row2, int col2) {
        int sum = 0;
        for (int i = row1; i <= row2; i++) {
            sum += sums[i][col2 + 1] - sums[i][col1];
        }
        return sum;
    }

前缀和

leetcode528

/**
 * 给你一个 下标从 0 开始 的正整数数组w ,其中w[i] 代表第 i 个下标的权重。
 *
 * 请你实现一个函数pickIndex,它可以随机地从范围 [0, w.length - 1] 内(含0和w.length - 1)选出并返回一个下标。
 * 选取下标i的概率为 w[i] / sum(w) 。
 *
 * 例如,对于 w = [1, 3],挑选下标0的概率为 1 / (1 + 3)= 0.25(即,25%),
 * 而选取下标 1 的概率为 3 / (1 + 3) = 0.75(即,75%)。
 *
 * 这个题是怎么把按照权重随机取值转化成前缀和的呢?
 * 可以看到题目中选择下标的概率是该下标的权重占总权重total的多少
 * 所以题目转化成在[1, total]范围内的所有整数分成n个部分,n为权重的个数
 *      第i个部分恰好包含w[i]个整数,并且这n个部分两两无交集。
 *      然后我们在[i, total]范围内随机选择一个整数,如果整数被包含在第i个部份内,就返回i。
 * 实行以下的划分方法,按照从小到大的顺序依次划分每个部分,例如 w = [3, 1, 2, 4]时,权重和为10
 *      1.那么就将10划分为w.length个部分,每个部分分配到的数字的个数为w[i]的值,如下:
 *          [1,3],[4,4],[5,6],[7,10],此时他们的长度分别为3,1,2,4
 *      2.每个区间左边界是在它之前出现的所有元素和+1,右边界是到它为止的所有元素的和。
 *      3.如果用前缀和数组pre[i]表示w的前缀和:
 *          pre[i] = w[0] + w[1] + ... + w[i]
 *          因此第i个区间的左边界 pre[i] - w[i] + 1,右边界 pre[i]
 *      4.划分完成以后,假设随机到了整数x,那么希望查找到它存在于哪个区间内
 *          可以用二分法查找到最小的i,满足x <= pre[i]
 */
    // 前缀和数组
    int[] pre;
    // 权重和
    int total;

    public WeightRandomChoice(int[] w) {
        pre = new int[w.length];
        pre[0] = w[0];
        for (int i = 0; i < w.length; i++) {
            pre[i] = pre[i - 1] + w[i];
        }
        total = Arrays.stream(w).sum();
    }

    public int pickIndex() {
        int x = (int)(Math.random() * total) + 1;
        return binarySearch(x);
    }

    private int binarySearch(int x) {
        int low = 0;
        int high = pre.length - 1;
        while (low < high) {
            int mid = low + (high - low) / 2;
            if (pre[mid] < x) {
                low = mid + 1;
            } else {
                high = mid;
            }
        }
        return low;
    }

 

差分题

差分原理:

/**
 * 使用差分。
 * 当需要重复在一定区间内统一增量相同的值,任务是将增量叠加得到答案。
 * 差分数组的第i个数为原数组的第i - 1个元素和第i个元素的差值
 * 于是对差分数组求前缀和可以得到原数组
 *
 * 差分数组的性质:
 * 当我们希望
 * 对原数组的某个区间[left, right]施加一个增量increase时,
 * 对差分数组d对应的改变是d[left]增加increase,d[r + 1]减少increase。
 * 这时,对原数组的区间的增量修改,变成了对差分数组的两个位置的修改
 *
 * 并且这种修改是可以叠加的。
 *     当我们需要对原数组进行多次区间的修改时,我们只需要按规则修改差分数组即可
 */

leetcode1109 航班预定系统

    public int[] corpFlightBookings(int[][] bookings, int n) {
        /**
         * 本题应用差分数组的算法过程
         * 1.遍历给定的预定记录数组,完成对差分数组的修改
         * 2.完成差分数组修改后,只需要最后求出差分数组的前缀和即可得到目标数组
         * 3.本题日期从1开始,注意下标对应:
         *      对预定记录booking=[l, r, inc]要让d[l - 1]增加inc,d[r]减少inc
         *      当r = n时无需修改d[r]
         */
        int[] difference = new int[n];
        for (int[] booking : bookings) {
            difference[booking[0] - 1] += booking[2];
            if (booking[1] < n) {
                difference[booking[1]] -= booking[2];
            }
        }
        for (int i = 1; i < n; i++) {
            difference[i] += difference[i - 1];
        }
        return difference;
    }

leetcode1094. 拼车 字少的题题狠话不多

    public boolean carPooling(int[][] trips, int capacity) {
        int max = 0;
        // for循环找到最大的下车地点
        for (int[] trip : trips) {
            if (max < trip[2]) {
                max = trip[2];
            }
        }
        int[] place = new int[max + 1]; // 地点数组,i位置有place[i]人上车
        for (int[] trip : trips) {
            place[trip[1]] += trip[0];
            place[trip[2]] -= trip[0];
        }

        int count = 0; // 记录乘客数量
        for (int i = 0; i < place.length; i++) {
            count += place[i];
            if (count > capacity) {
                return false;
            }
        }
        return true;
    }

`倍增`、`ST表`、`前缀和` 和 `差分` 是算法竞赛与高效编程中常用的四种基础技术,它们各自解决不同类别的问题,但在实际应用中经常结合使用。下面我将逐一详细讲解其原理、实现方式、应用场景,并给出 C++ 代码示例。 --- ## 一、前缀和Prefix Sum) ### ✅ 定义: 前缀和是数组的一个预处理技巧,用于**快速计算区间和**。 设原数组为 `a[0..n-1]`,定义前缀和数组 `prefix[i] = a[0] + a[1] + ... + a[i-1]`,则: &gt; 区间 `[l, r]` 的和为:`sum(l, r) = prefix[r+1] - prefix[l]` ### ⏱ 时间复杂度: - 预处理:O(n) - 查询:O(1) ### 💡 应用场景: - 多次查询子数组和 - 子数组和为某值的问题(配合哈希表) ### ✅ C++ 实现: ```cpp #include &lt;iostream&gt; #include &lt;vector&gt; using namespace std; int main() { vector&lt;int&gt; a = {1, 2, 3, 4, 5}; int n = a.size(); // 构建前缀和prefix[0]=0, prefix[i] 表示前 i 个元素的和) vector&lt;int&gt; prefix(n + 1, 0); for (int i = 0; i &lt; n; ++i) { prefix[i + 1] = prefix[i] + a[i]; } // 查询 [1,3] 的和:a[1]+a[2]+a[3] = 2+3+4=9 int l = 1, r = 3; cout &lt;&lt; &quot;Sum from [&quot; &lt;&lt; l &lt;&lt; &quot;,&quot; &lt;&lt; r &lt;&lt; &quot;] is: &quot; &lt;&lt; prefix[r + 1] - prefix[l] &lt;&lt; endl; return 0; } ``` --- ## 二、差分(Difference Array) ### ✅ 定义: 差分前缀和&ldquo;逆运算&rdquo;,用于**高效进行多次区间修改**。 构造差分数组 `d`,使得对 `d` 做前缀和还原出原数组。 若想对原数组 `[l, r]` 加上 `x`,只需: ```cpp d[l] += x; d[r+1] -= x; // 如果 r+1 &lt; n ``` ### ⏱ 时间复杂度: - 单次修改:O(1) - 最终还原:O(n) ### 💡 应用场景: - 多次区间加法操作 - 配合前缀和还原结果 ### ✅ C++ 实现: ```cpp #include &lt;iostream&gt; #include &lt;vector&gt; using namespace std; int main() { vector&lt;int&gt; a = {1, 2, 3, 4, 5}; int n = a.size(); // 构造差分数组 d vector&lt;int&gt; d(n + 1, 0); for (int i = 0; i &lt; n; ++i) { d[i] = a[i] - (i &gt; 0 ? a[i-1] : 0); } // 对 [1,3] 加 2 int l = 1, r = 3, x = 2; d[l] += x; d[r + 1] -= x; // 还原数组 vector&lt;int&gt; res(n); res[0] = d[0]; for (int i = 1; i &lt; n; ++i) { res[i] = res[i-1] + d[i]; } cout &lt;&lt; &quot;After update: &quot;; for (int v : res) cout &lt;&lt; v &lt;&lt; &quot; &quot;; cout &lt;&lt; endl; return 0; } ``` --- ## 三、倍增(Binary Lifting / Doubling) ### ✅ 定义: 倍增是一种通过 **预处理 $ 2^k $ 步信息** 来加速查询的技术。 最典型应用:**快速求树上两个节点的 LCA(最近公共祖先)** 核心思想:每个节点记录它的第 $ 2^k $ 级祖先,从而可以在 $ O(\log n) $ 内跳转任意距离。 ### 🔧 应用场景: - 树上 LCA 查询 - 快速跳跃指针(如并查集优化、字符串匹配) - RMQ 问题的一种解法(替代 ST 表) ### ✅ C++ 实现(简化版倍增跳父亲): ```cpp #include &lt;iostream&gt; #include &lt;vector&gt; #include &lt;cmath&gt; using namespace std; const int MAXN = 1e5 + 5; const int LOG = 20; vector&lt;int&gt; tree[MAXN]; int parent[MAXN][LOG]; int depth[MAXN]; // DFS 预处理深度和直接父节点 void dfs(int u, int p) { parent[u][0] = p; depth[u] = depth[p] + 1; for (int i = 1; i &lt; LOG; ++i) { if (parent[u][i-1] != -1) { parent[u][i] = parent[parent[u][i-1]][i-1]; } else { parent[u][i] = -1; } } for (int v : tree[u]) { if (v != p) { dfs(v, u); } } } // 求 LCA int lca(int u, int v) { if (depth[u] &lt; depth[v]) swap(u, v); // 让 u 上升到和 v 同一层 int diff = depth[u] - depth[v]; for (int i = 0; i &lt; LOG; ++i) { if (diff &amp; (1 &lt;&lt; i)) { u = parent[u][i]; } } if (u == v) return u; for (int i = LOG - 1; i &gt;= 0; --i) { if (parent[u][i] != parent[v][i]) { u = parent[u][i]; v = parent[v][i]; } } return parent[u][0]; } ``` --- ## 四、ST表(Sparse Table)&mdash;&mdash;静态RMQ结构 ### ✅ 定义: ST 表(Sparse Table)是一种基于**倍增思想**的数据结构,用于解决**静态区间最值查询**(Range Minimum/Maximum Query, RMQ),不能处理修改。 &gt; 支持 O(1) 查询任意区间的最小值或最大值,预处理时间 O(n log n) ### 🧠 原理: 构建二维数组 `st[i][k]` 表示从位置 `i` 开始长度为 $ 2^k $ 的区间的最值。 转移方程: ```cpp st[i][k] = max(st[i][k-1], st[i + (1&lt;&lt;(k-1))][k-1]) ``` 查询 `[l, r]` 最值时: - 找最大的 $ k $ 使得 $ 2^k \leq r-l+1 $ - 分两段覆盖:`[l, l+2^k-1]` 和 `[r-2^k+1, r]` ### ✅ C++ 实现(静态区间最大值): ```cpp #include &lt;iostream&gt; #include &lt;vector&gt; #include &lt;cmath&gt; using namespace std; const int MAXN = 1e5 + 5; const int LOG = 20; int st[MAXN][LOG]; int log_table[MAXN]; // 预处理 log2 值 // 预处理 log 值(避免重复调用 log2 函数) void build_log_table(int n) { log_table[1] = 0; for (int i = 2; i &lt;= n; ++i) { log_table[i] = log_table[i / 2] + 1; } } // 构建 ST 表(最大值) void build_st(vector&lt;int&gt;&amp; arr, int n) { for (int i = 0; i &lt; n; ++i) { st[i][0] = arr[i]; } for (int k = 1; (1 &lt;&lt; k) &lt;= n; ++k) { for (int i = 0; i + (1 &lt;&lt; k) &lt;= n; ++i) { st[i][k] = max(st[i][k-1], st[i + (1 &lt;&lt; (k-1))][k-1]); } } } // 查询 [l, r] 的最大值 int query_max(int l, int r) { int len = r - l + 1; int k = log_table[len]; return max(st[l][k], st[r - (1 &lt;&lt; k) + 1][k]); } // 测试 int main() { vector&lt;int&gt; arr = {1, 3, 2, 7, 5, 4}; int n = arr.size(); build_log_table(n); build_st(arr, n); cout &lt;&lt; &quot;Max in [1,4]: &quot; &lt;&lt; query_max(1, 4) &lt;&lt; endl; // 输出 7 cout &lt;&lt; &quot;Max in [2,5]: &quot; &lt;&lt; query_max(2, 5) &lt;&lt; endl; // 输出 5 return 0; } ``` --- ### ✅ 四者对比总结: | 技术 | 功能 | 修改支持 | 查询效率 | 预处理 | 典型用途 | |------|------|-----------|------------|----------|-------------| | **前缀和** | 快速区间求和 | ❌ 不支持 | O(1) | O(n) | 统计、子数组和 | | **差分** | 快速区间加法 | ✅ 支持批量加 | O(n)还原 | O(1) per op | 批量更新数组 | | **倍增** | 快速跳 $2^k$ 步 | ❌(静态) | O(log n) | O(n log n) | LCA、跳跃指针 | | **ST表** | 静态区间最值查询 | ❌ | O(1) | O(n log n) | RMQ 问题 | ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值