当前位置: 首页 > article >正文

优选算法的睿智之林:前缀和专题(一)

专栏:算法的魔法世界

个人主页:手握风云

目录

一、前缀和

二、例题讲解

2.1. 一维前缀和

2.2. 二维前缀和

2.3. 寻找数组的中心下标

2.4. 除自身以外数组的乘积


一、前缀和

        前缀和算法是一种用于处理数组或序列数据的算法,其核心思想是通过预先计算出数组中每个位置之前所有元素的和(即前缀和),从而快速解决一些与区间和相关的问题。它在处理数组求和、区间统计等场景下非常高效。

二、例题讲解

2.1. 一维前缀和

        我们需要注意一个细节,上面题目的数组给定下标是从1开始的。我们先来想一个暴力解法——模拟。每次查询都从l下标一直遍历到r下标,最坏情况下就是q次都是从头遍历到尾,所以暴力解法的时间复杂度为O(n*q),n、q的最大值都为10^{5},一定会超时。

        第二种解法就是利用前缀和算法。第一步,我们先预处理出一个前缀和数组int[] dp,dp[i]代表的是arr数组区间[1,i]的和。而数组dp中每一个元素的算法不用再从头加到尾,直接利用递推公式dp[i] = dp[i-1] + arr[i]。

        第二步是利用前缀和数组。比如我们要计算[2,5]区间的和,我们没必要直接访问下标2——5。根据下图可以得出,dp[r] - dp[l-1]。我们预处理前缀和的时间复杂度为O(n),每次查询可以直接获取下标,那么q次查询的时间复杂度为O(q*1),整体时间复杂度为O(n)+O(q)

        前面提到了一个细节,就是数组下标为什么要从1开始?如果我们要查询[0,2]区间的和,那么l-1就会为-1,就会无法访问到数组的第一个元素,从而方便处理边界问题。

        完整代码实现:

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        while (in.hasNextInt()) {
            int n = in.nextInt(), q = in.nextInt();
            int[] arr = new int[n + 1];
            for (int i = 1; i < n + 1; i++) {
                arr[i] = in.nextInt();
            }

            //预处理一个前缀和数组
            long[] dp = new long[n + 1];
            for (int i = 1; i < n + 1; i++) {
                dp[i] = dp[i - 1] + arr[i];
            }

            //使用前缀和数组
            while (q > 0) {
                int l = in.nextInt(), r = in.nextInt();
                System.out.println(dp[r] - dp[l - 1]);
                q--;
            }
        }
    }
}

2.2. 二维前缀和

        暴力解法与上一道题一样,也是利用模拟在指定区间内遍历,时间复杂度为O(n*m*q)。跟上一道题一样,也是会超时的。

        同一维前缀和一样,我们第一步还是先预处理一个与给定矩阵arr[][]同等规模的前缀和矩阵。dp[i][j]里的每个元素代表着矩阵arr[][]的阴影面积。

        关于如何求出dp[i][j]的值,我们没有必要再去遍历矩阵arr,因为遍历的时间复杂度会与暴力求解的一样高。我们可以利用完全平方公式的思想,下图中的元素之和,就是四个区域A、B、C、D的和。A区域的和可以很好的算出来:dp[i-1][j-1],但是B、C区域的和又不好算了。我们退而求其次,利用A+B+A+C-A的来算。A+B:dp[i-1][j];A+C:dp[i][j-1]。

        第二步就是利用二维前缀和矩阵。题目要求我们算出(x1,y1)——(x2,y2)里面的值直接A+B+C+D-(A+B)-(A+C)+A求出结果。

        完整代码实现:

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);

        //1. 读取输入的数据
        int n = in.nextInt(), m = in.nextInt(), q = in.nextInt();
        int[][] arr = new int[n + 1][m + 1];
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                arr[i][j] = in.nextInt();
            }
        }

        //2. 预处理一个前缀和矩阵
        long[][] dp = new long[n + 1][m + 1];
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1] + arr[i][j] - dp[i - 1][j - 1];
            }
        }

        //3. 使用前缀和矩阵
        while (q-- > 0) {
            int x1 = in.nextInt(), y1 = in.nextInt(), x2 = in.nextInt(), y2 = in.nextInt();
            System.out.println(dp[x2][y2] - dp[x1 - 1][y2] - dp[x2][y1 - 1] + dp[x1 - 1][y1 - 1]);
        }
    }
}

2.3. 寻找数组的中心下标

        本题是要我们查找一个下标,这个下标左侧的元素之和与右侧的元素之和相等。如果存在这样一个下标则返回这个下标值,如果没有返回-1。

        我们先来思考暴力解法。枚举每一个下标,计算左侧的元素之和与右侧的元素之和是否相等。我们需要从头到尾遍历数组下标,再去遍历左侧和右侧的元素,所以时间复杂度为O(n^{2})

        接下来对暴力解法进行优化。我们需要求的是某段数组元素的和,那么就可以利用前缀和思想,i左侧的元素之和f(i) = f(i-1) + nums[i-1],i右侧的元素之和g(i) = g(i+1) + nums[i+1]。只要比较出f(i) = g(i),就返回i。

        还有一些细节:1. 防止数组越界访问,如果i=0,那么f(i-1)就是[-1,0]这段区间的和,f(0)=0;同理,g(n-1)=0。2. f是依赖与i-1,所以f数组是从左向右填写;g是依赖于i+1,所以g数组是从右向左填写的。

        完整代码实现:

public class Solution {
    public int pivotIndex(int[] nums) {
        int n = nums.length;
        int[] f = new int[n];
        int[] g = new int[n];

        //预处理前缀和数组
        for (int i = 1; i < n; i++) {
            f[i] = f[i - 1] + nums[i - 1];
        }
        for (int i = n - 2; i >= 0; i--) {
            g[i] = g[i + 1] + nums[i + 1];
        }

        for (int i = 0; i < n; i++) {
            if (f[i] == g[i]) {
                return i;
            }
        }
        return -1;
    }
}

2.4. 除自身以外数组的乘积

        暴力解法与上一题类似,枚举数组下标,再遍历下标的左右两侧计算乘积。时间复杂度也与上一题一样,也是O(n^{2})

        利用前缀和的思想,预处理出i左侧区间的乘积和右侧区间的乘积,到某个下标时,直接从前缀与后缀里进行拿值就可以。算法原理与上一题完全一样的,只不过这道题是要求乘积。前缀积f[i] = f[i - 1] * nums[i - 1];g[i] = g[i+1] * nums[i+1]。细节处理上,与上一题不同的是,边界f[0]与g[n-1]要赋值成1。

        完整代码实现:

public class Solution {
    public int[] productExceptSelf(int[] nums) {
        int n = nums.length;
        int[] f = new int[n];
        int[] g = new int[n];

        f[0] = g[n - 1] = 1;
        for (int i = 1; i < n; i++)
            f[i] = f[i - 1] * nums[i - 1];
        for (int i = n - 2; i >= 0; i--)
            g[i] = g[i + 1] * nums[i + 1];
        int[] ret = new int[n];
        for (int i = 0; i < n; i++)
            ret[i] = f[i] * g[i];
        return ret;
    }
}

http://www.kler.cn/a/597882.html

相关文章:

  • 2.2.盈亏平衡分析
  • MySQL 索引下推
  • React 18 如何定义变量,及赋值 与渲染
  • Linux常用的命令
  • [AI速读]混合验证方案:如何高效解决RISC-V向量扩展的验证难题
  • Nginx如何处理请求
  • Modbus协议编程读写流程图大全
  • DeepSeek对KVM环境下创建共享iSCSI存储的指导
  • 使用单调栈在O(n)时间复杂度内计算直方图中的最大矩形面积
  • 数据可视化在商业智能中的应用:从数据到洞察的桥梁
  • 信息安全基础
  • 2059-Authentication plugin ‘caching_sha2_password‘ cannot be loaded
  • 六十天前端强化训练之第二十八天之Composition 函数完全指南
  • 学习c++多线程前,回顾一下Linux的多线程
  • 圆弧插补相关算法汇总(C++和ST源代码)
  • CUDA 学习(4)——CUDA 编程模型
  • Android 系统进程启动Activity方法说明
  • 爬虫:scrapy面试题大全(60个scrapy经典面试题和详解)
  • 多线程编程中什么时候使用锁和原子操作
  • C#单例模式