Java算法-一维前缀和与差分

一、一维前缀和

① 什么是一维前缀和?

📚 其实通过名字就能知道" 一维前缀和 "的意思

通过一个一维数组"arr1"而创建的另一个一维数组"arr2""arr2"的每一个元素都是"arr1"的对应下标元素再加上该下标之前所有元素的和。

📕 简单的讲,可以理解为:每个位置的值是数组中该位置前的值的和

让我们通过这个图片来看一下:

而一维前缀和一般都应用在什么地方呢?让我们看一道例题:

📖 给定一个长度为n的序列a,再给定q组查询,对于每次查询:

"给定一对l,r,你需要输出a数组中的l下标~r下标之间的和"

让我们看一下没有学过"一维差分"的暴力解题思想解题

import java.util.*;

public class Test {
    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);
        int n = scan.nextInt();
        int q = scan.nextInt();
        int[] arr = new int[n + 10];
        for(int i = 1;i <= n;i++) {
        	arr[i] = scan.nextInt();
        }
        while(q-- > 0) {
        	int l = scan.nextInt();
        	int r = scan.nextInt();
        	int num = 0;
        	for(int i = l; i <= r;i++) {
        		num += arr[i];
        	}
        	System.out.println(num);
        }
    }
}

那让我们来分析一下这样解题的时间复杂度

数组的长度为'n',进行'q'次查询,若考虑最坏的情况,每次查询遍历整个数组,那么时间复杂度最坏的情况是O(nq)。

而该题中,n和q最大为1e5,那么O(nq)便能够达到可怕的(1e10),而我们要知道正常情况下,我们的编译器运算速度一秒也就是2e8,而1e10则是远远的超过了2e8:

果不其然,这是一定会超时的。

② 一维前缀和的使用

那么如果我们使用"一维前缀和"来进行解题,就能够大大的提高代码性能

(为了使数组下标对应' l '和' r ',我们通常会从数组的1下标开始赋值:)

由图我们可以看出,在查询内部,我们之前的时间复杂度为O(r-l),而使用一维前缀数组后,将时间复杂度提升到了O(1)!!!这是非常大的提升~

用公式表示大概就是l ~ r 的区间和等于"一维前缀和数组 arr2[r] - arr2[l - 1]"

📚 一维前缀和解题代码

import java.util.*;

public class Test {
    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);
        int n = scan.nextInt();
        int q = scan.nextInt();
        int[] arr1 = new int[n + 10];
        int[] arr2 = new int[n + 10];
        for(int i = 1;i <= n;i++) {
        	//原数组
        	arr1[i] = scan.nextInt();
        	//一维前缀和数组
        	arr2[i] = arr1[i] + arr2[i - 1];
        }
        while(q-- > 0) {
        	int l = scan.nextInt();
        	int r = scan.nextInt();
        	System.out.println(arr2[r] - arr2[l - 1]);
        }
    }
}

经过改良后,也成功通过啦~

小练习:区间次方和(⭐)

问题描述

给定一个长度为 n 的整数数组 a 以及 m 个查询。

每个查询包含三个整数 l,r,k 表示询问 l∼r 之间所有元素的 k 次方和。

请对每个查询输出一个答案,答案对 1e9+7 取模。

输入格式

第一行输入两个整数 n,m 其含义如上所述。

第二行输入 n 个整数 a[1],a[2],...,a[n]。

接下来 m 行,每行输入三个整数 l,r,k 表示一个查询。

输出格式

输出 m 行,每行一个整数,表示查询的答案对1e9+7取模的结果

📕 思路提示

首先,看到对数组区间进行操作,我们就应该下意识地想到去使用"前缀和/差分"的方法来解题~

而每次操作,求对 l ~ r 之间所有元素的 k 次方求和,就很自然的应该去想到使用"前缀和"方法~

对元素的次方k只有1~5,数据并不庞大,也就意味着我们通过将1~5次方的数据全部求出来,放进一个前缀和数组中,然后通过输入的k来获取k次方,l~r代表访问的下标~

📚 代码实现

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);
        int n = scan.nextInt();
        int m = scan.nextInt();
        //储存原数组
        long[] a = new long[n + 10];
        //储存1~5次方前缀和数组
        long[][] s = new long[10][n + 10];
        //i代表第i个元素
        for (int i = 1; i <= n; i++) {
			    a[i] = scan.nextInt();
			//j代表j次方
			for(int j = 1; j <= 5; j++) {
				//(1 ~ 5次方和)
				s[j][i] = (long)Math.pow(a[i],j) + s[j][i - 1];
			}
		}
        while(m-- > 0) {
        	int l = scan.nextInt();
        	int r = scan.nextInt();
        	int k = scan.nextInt();
          long num = s[k][r] - s[k][l - 1];
        	System.out.println(num % 1000000007);
        }
    }
}

没什么问题,也是成功通过啦~

(该题是一道基础题,但需要注意一点:别忘了最后答案要对 1e9+7 取模哦~)

📚 一维前缀和的作用:

📕 一维前缀和的应用非常广泛,特别是在处理数组区间和的问题时非常有用

📕 通过计算数组的前缀和,我们可以在O(1)的时间复杂度内获得任意区间的和

二、一维差分

① 什么是一维差分?

📚 一维差分的定义

一维差分是指对一个一维数组进行变换,使得原数组中连续元素之间的差值保存在另一个数组中,这个数组称为差分数组。

用图表示

📚 那么一维差分的作用又是什么呢?同样的,让我们看一个小例题

问题描述

给定一个长度为n的序列a再给定m组操作

"每次操作给定3个正整数l,r,d,表示对a(l~r)中的所有数增加d"

最终输出操作结束后的序列 a

同样的,让我们看一下暴力解法

📖 暴力解法的思路大概就是

进行m组操作,每次操作都使用for循环,对(l~r)的区间进行遍历,并且依次将区间内的元素+d。

📚 代码实现

import java.util.*;

public class Test {
    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);
        int n = scan.nextInt();
        int m = scan.nextInt();
        int[] a = new int[n + 10];
        for(int i = 1;i <= n;i++) {
        	a[i] = scan.nextInt();
        }
        while(m-- > 0) {
        	int l = scan.nextInt();
        	int r = scan.nextInt();
        	int d = scan.nextInt();
        	for(int i = l;i <= r;i++) {
        		a[i] += d;
        	}
        }
        for(int i = 1;i <= n;i++) {
        	System.out.print(a[i] + " ");
        }
    }
}

同样的,和一维前缀和的例题一样,使用暴力解法会导致"超时"

那么让我们来一起分析一下暴力解法的时间复杂度

进行了"m"组操作,每次操作都对"l~r"的区间进行操作,我们假设最坏的情况,每次操作都遍历整个数组也就是说时间复杂度等于O(mn),最大的时候也是能够到达1e10,仍然远远大于编译器一秒钟的运算(2e8)

② 一维差分的使用

📖 那么我们该如何改进呢

让我们来看一下,一维差分的实现:

定义差分数组的公式:arr2[i] = arr1[i] - arr1[i - 1];

这个应该还是很好理解的,只需要前一项减去后一项就是两者之差~

而求差分数组,是为了对差分数组的首尾进行修改,然后再还原数组,从而达到将"m"次操作中的时间复杂度O(n)减小成O(1)~

具体我们还是看图

由此,我们将之前的"从l~r位置进行遍历"改变成了"首尾进行操作",使O(nm)的时间复杂度减少成了O(m)。

📚 代码实现

import java.util.*;
//1:无需package
//2: 类名必须Main, 不可修改

public class Test {
 public static void main(String[] args) {
     Scanner scan = new Scanner(System.in);
     int i = 0;
     int a = scan.nextInt();
     int b = scan.nextInt();
     int[] arr1 = new int[a + 3];
     int[] arr2 = new int[a + 3];
     int[] arr3 = new int[a + 3];
     for(i = 1;i <= a;i++){
         arr1[i] = scan.nextInt();
         arr2[i] = arr1[i] - arr1[i - 1];
     }
     for(i = 0;i < b;i++){
         int n1 = scan.nextInt();
         int n2 = scan.nextInt();
         int n = scan.nextInt();
         arr2[n1] = arr2[n1] + n;
         arr2[n2 + 1] = arr2[n2 + 1] - n;
     }
     for(i = 1;i <= a;i++){
         arr2[i] = arr2[i] + arr2[i - 1];
         //c1 = a1 + c0(0)
         //c2 = a2 + c1(a1)
         //c3 = a3 + c2(a1 + a2)
     }
     for(i = 1;i <= a;i++){
         System.out.print(arr2[i] + " ");
     }
   }
}

也是成功通过了所有测试用例~并无超时现象~

小练习:小蓝的操作(⭐⭐)

问题描述

一个数组 a 中共包含 n 个数,问最少多少次操作,可以让 a 数组所有数都变成 1 。

操作的内容是:

每次操作可以任选一个区间使得区间内的所有数字减 1 。 数据保证一定有解。

📚 思路分析

同样的,看到对数组区间进行操作,下意识想到使用(前缀和/差分)进行求解,而此题问"需要多少次操作使得a[]数组中所有数变成1",显然是对数组内容进行了操作,于是便选择使用"差分"。

想要使得所有的数都变成1,那么最后我们得到的"差分数组"一定就是

📖 这样的一个数组,其原理是:

第一个数字为1,而后面的元素,每一位都与前一位相等,也就是相当于"全为1"。

我们来用他的样例输入进行一下讲解

📚 代码实现

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);
        int a = scan.nextInt();
        int num = 0;
        int[] arr1 = new int[a + 5];
        int[] arr2 = new int[a + 5];
        for (int i = 1; i <= a; i++) {
            arr1[i] = scan.nextInt();
            arr2[i] = arr1[i] - arr1[i - 1];
        }
        for(int i = 1;i <= a;i++) {
            if(arr2[i] > 0) {
                num += arr2[i];
            }
        }
       System.out.println(num - 1);
    }

那么这次关于"一维前缀和与差分"就为大家分享到这里啦~有什么讲解不清楚或者需要修正的地方,还请大家多多在评论区指出,我这个小白也会虚心改正的~那我们下次再见啦

评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值