🔥个人主页: 中草药
🔥专栏:【算法工作坊】算法实战揭秘
一.前缀和
题目链接:DP34 【模板】前缀和
代码
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
// 注意 hasNext 和 hasNextLine 的区别
while (in.hasNextInt()) { // 注意 while 处理多个 case
int n = in.nextInt();
int q = in.nextInt();
int[] arr=new int[n+1];
for(int i=1;i<arr.length;i++){
arr[i]=in.nextInt();
}
long[] dp=new long[n+1];
for(int i=1;i<dp.length;i++){
dp[i]=dp[i-1]+arr[i];
}
while(q>0){
int l = in.nextInt();
int r = in.nextInt();
System.out.println(dp[r]-dp[l-1]);
q--;
}
}
}
}
算法原理
-
初始化输入扫描器:程序首先创建一个
Scanner
对象,这个对象可以从标准输入流中读取用户的输入。 -
进入主循环:代码进入一个无限循环,检查是否有更多的整数可以读取。如果输入中还有整数,则继续执行;如果没有,则循环结束。
-
读取参数:在循环内部,程序读取两个整数,第一个整数
n
表示接下来要读取的数组的长度,第二个整数q
表示将要进行的查询次数。 -
创建并填充数组:接着,程序创建了一个大小为
n+1
的整数数组arr
。从索引1
开始,程序读取n
个整数并将它们存储在数组中。这是因为稍后的前缀和数组也会从索引1
开始计算。 -
计算前缀和:程序创建另一个大小为
n+1
的长整型数组dp
,用于存储前缀和。前缀和数组中的每一个元素dp[i]
是数组arr
中从arr[1]
到arr[i]
所有元素的累积和。换句话说,dp[i]
存储的是数组arr
前i
个元素的总和。 -
处理查询:对于
q
次查询,程序读取每一对整数l
和r
,分别代表查询区间的左边界和右边界。然后,程序利用前缀和数组dp
计算区间[l, r]
内所有元素的总和,具体是通过dp[r] - dp[l-1]
得到。这是因为dp[r]
包含了从arr[1]
到arr[r]
的总和,而dp[l-1]
包含了从arr[1]
到arr[l-1]
的总和,两者的差值就是[l, r]
区间内元素的总和。 -
输出结果:对于每一次查询,程序都会输出计算得到的区间和。
-
循环终止条件:当所有的查询都处理完毕,或者输入流中没有更多的整数可以读取时,程序自然退出循环,执行结束。
前缀和算法是一种常用的数据结构技术,用于优化对数组区间求和的操作。通过预处理数组的前缀和,可以将原本可能需要线性时间复杂度的区间求和操作降低到常数时间复杂度 O(1),大大提高了效率。在本例中,dp
数组就是 arr
数组的前缀和表示,使得每次查询区间和的操作都非常高效。
举例
测试用例
输入:
3 2
1 2 4
1 2
2 3
输入解析:
- 第一行包含两个整数
n
和q
,表示数组的长度和需要进行的查询次数。在这个例子中,n = 3
和q = 2
。 - 第二行包含
n
个整数,这里是1 2 4
,构成数组arr
的非零元素部分。
代码运行过程:
-
初始化Scanner:
- 创建
Scanner
对象in
用于读取标准输入。
- 创建
-
读取
n
和q
:- 读入
n = 3
和q = 2
。
- 读入
-
创建并填充数组
arr
:- 创建大小为
n + 1
的数组arr
。 - 从输入读取三个整数
1
,2
,4
并分别存入arr[1]
,arr[2]
,arr[3]
。
- 创建大小为
-
计算前缀和数组
dp
:- 初始化大小为
n + 1
的数组dp
。 - 计算前缀和:
dp[1] = arr[1] = 1
,dp[2] = dp[1] + arr[2] = 1 + 2 = 3
,dp[3] = dp[2] + arr[3] = 3 + 4 = 7
。
- 初始化大小为
-
处理查询:
- 第一次查询:读入
l = 1
和r = 2
。- 输出区间
[1, 2]
的和,即dp[2] - dp[0] = 3 - 0 = 3
。
- 输出区间
- 第二次查询:读入
l = 2
和r = 3
。- 输出区间
[2, 3]
的和,即dp[3] - dp[1] = 7 - 1 = 6
。
- 输出区间
- 第一次查询:读入
总结输出:
- 对于给定的测试用例,代码应该输出两次查询的结果:
- 第一次查询结果为
3
。 - 第二次查询结果为
6
。
- 第一次查询结果为
这就是代码处理给定测试用例的详细流程。在实际运行中,代码会根据输入动态生成这些计算步骤,并最终输出查询结果。
二.二维前缀和
题目链接:DP35 【模板】二维前缀和
代码
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int n = in.nextInt();
int m = in.nextInt();
int q = in.nextInt();
int[][] arr = new int[n + 1][m + 1];
//1.输入数据
for (int i = 1; i < arr.length; i++) {
for (int j = 1; j < arr[0].length; j++) {
arr[i][j] = in.nextInt();
}
}
//2.前缀和数组
long[][] dp = new long[n+1][m+1];
for (int i = 1; i < arr.length; i++) {
for (int j = 1; j < arr[0].length; 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();
int y1=in.nextInt();
int x2=in.nextInt();
int y2=in.nextInt();
System.out.println(dp[x2][y2]-dp[x2][y1-1]-dp[x1-1][y2]+dp[x1-1][y1-1]);
q--;
}
}
算法原理
-
初始化输入读取器: 程序开始时,初始化一个
Scanner
对象,用于从标准输入读取数据。 -
读取矩阵尺寸和查询次数: 首先,程序读取三个整数,分别代表矩阵的行数
n
、列数m
和查询次数q
。 -
输入矩阵数据: 程序随后创建一个
n+1
行m+1
列的二维数组arr
,并读取矩阵的实际数据,忽略第一行和第一列,因为它们将用于构建前缀和数组时的边界条件。 -
构建前缀和数组: 接着,程序构建一个同样大小的二维数组
dp
,但类型为long
以防止整数溢出。dp
数组的每个元素dp[i][j]
将存储从原矩阵的左上角(1,1)
到位置(i,j)
的所有元素的累积和。这个累积和通过以下递推公式计算:dp[i][j] = dp[i-1][j] + dp[i][j-1] + arr[i][j] - dp[i-1][j-1]
这意味着当前位置的前缀和等于上方的前缀和加上左边的前缀和,再加上当前位置的值,最后减去左上方的前缀和以避免重复计算。
-
处理查询: 程序进入一个循环,根据
q
的值处理多次查询。对于每一次查询,读取四个整数x1
,y1
,x2
,y2
,分别代表子矩阵的左上角坐标和右下角坐标。然后,程序利用前缀和数组dp
快速计算并输出子矩阵内所有元素的总和,计算方式如下:sum = dp[x2][y2] - dp[x2][y1-1] - dp[x1-1][y2] + dp[x1-1][y1-1]
这个计算方法背后的理念是,
dp[x2][y2]
包含了整个矩形的和,而dp[x2][y1-1]
和dp[x1-1][y2]
分别表示子矩阵外的左边和上边区域的和,dp[x1-1][y1-1]
则包含了这两个区域重叠部分的和,需要加回以修正减法操作造成的过度扣除。 -
循环终止与退出: 当所有查询都被处理完毕,程序自然退出循环,并结束执行。
通过构建和使用前缀和数组,程序能够非常高效地处理任何子矩阵的和的查询,即使面对大量查询也能保持良好的性能。
举例
测试用例
输入:
3 4 3
1 2 3 4
3 2 1 0
1 5 7 8
1 1 2 2
1 1 3 3
1 2 3 4
输入解析
- 第一行输入是:
3 4 3
,这意味着矩阵有3行4列,且有3次查询。 - 接下来的3行是矩阵的具体数值:
- 第一行:
1 2 3 4
- 第二行:
3 2 1 0
- 第三行:
1 5 7 8
- 第一行:
- 最后的3行是查询的坐标:
- 第一次查询:
1 1 2 2
- 第二次查询:
1 1 3 3
- 第三次查询:
1 2 3 4
- 第一次查询:
代码执行过程
1. 读取输入数据
- 读入
n = 3
,m = 4
,q = 3
。 - 创建一个
4x5
的二维数组arr
,并填充矩阵数据。注意,数组的第一行和第一列是0,用于简化前缀和的计算。
2. 构建前缀和数组
- 创建一个
4x5
的二维数组dp
。 - 计算前缀和数组
dp
:dp[i][j] = dp[i-1][j] + dp[i][j-1] + arr[i][j] - dp[i-1][j-1]
dp[1][1] = arr[1][1] = 1
dp[1][2] = dp[1][1] + arr[1][2] = 1 + 2 = 3
dp[2][2] = dp[1][2] + dp[2][1] + arr[2][2] - dp[1][1] = 3 + 4 + 2 - 1 = 8
dp[3][3]
同理计算
3. 处理查询
- 第一次查询
(1, 1) -> (2, 2)
:sum = dp[2][2] - dp[2][0] - dp[0][2] + dp[0][0]
- 实际上,
dp[2][0]
和dp[0][2]
都为0,dp[0][0]
也为0,因此: sum = dp[2][2] = 8
(注:这里的dp[2][2]
应该是计算后的真实值)
- 第二次查询
(1, 1) -> (3, 3)
:sum = dp[3][3] - dp[3][0] - dp[0][3] + dp[0][0]
- 同理,
dp[3][0]
和dp[0][3]
都为0,dp[0][0]
也还是0,因此: sum = dp[3][3]
(dp[3][3]
需要通过前缀和公式计算)
- 第三次查询
(1, 2) -> (3, 4)
:sum = dp[3][4] - dp[3][1] - dp[1][4] + dp[1][1]
- 这里同样应用前缀和的计算规则,其中
dp[3][4]
,dp[3][1]
,dp[1][4]
,dp[1][1]
都需要从dp
数组中获取。
结果输出
- 根据以上步骤,代码将会输出三次查询的结果:
- 第一次查询的结果是子矩阵
(1, 1) -> (2, 2)
的和,正确计算后为8。 - 第二次查询的结果是子矩阵
(1, 1) -> (3, 3)
的和,正确计算后为25。 - 第三次查询的结果是子矩阵
(1, 2) -> (3, 4)
的和,正确计算后为32。
- 第一次查询的结果是子矩阵
三.寻找数组的中心下标
测试用例:724.寻找数组的中心下标
代码
public int pivotIndex(int[] nums) {
long[] f=new long[nums.length];
long[] g=new long[nums.length];
for(int i=1;i<nums.length;i++){
f[i]=f[i-1]+nums[i-1];
}
for(int i=nums.length-2;i>=0;i--){
g[i]=g[i+1]+nums[i+1];
}
for(int i=0;i<nums.length;i++){
if(f[i]==g[i]){
return i;
}
}
return -1;
}
算法原理
-
前缀和数组
f
:- 创建一个与输入数组
nums
等长的前缀和数组f
。 - 数组
f
中的每个元素f[i]
表示nums
中从索引0
到i-1
的所有元素的和。 - 例如,
f[3]
将包含nums[0] + nums[1] + nums[2]
的值。
- 创建一个与输入数组
-
后缀和数组
g
:- 同样创建一个等长的后缀和数组
g
。 - 数组
g
中的每个元素g[i]
表示nums
中从索引i+1
到最后一个元素的所有元素的和。 - 例如,
g[3]
将包含nums[4] + nums[5] + ...
直到最后一个元素的值。
- 同样创建一个等长的后缀和数组
-
比较前缀和与后缀和:
- 遍历数组
f
和g
的所有元素,比较f[i]
和g[i]
是否相等。 - 如果找到一个索引
i
,满足f[i] == g[i]
,则表明该索引处的元素左侧所有元素的和等于右侧所有元素的和,即找到了中心点。 - 如果没有找到这样的索引,返回
-1
。
- 遍历数组
代码分析
-
初始化前缀和与后缀和数组:
f
和g
数组初始化为long
类型,以防数组元素的和超出int
类型的范围。
-
计算前缀和:
- 从索引
1
开始,使用递推公式f[i] = f[i-1] + nums[i-1]
来计算前缀和。
- 从索引
-
计算后缀和:
- 从倒数第二个元素开始向前,使用递推公式
g[i] = g[i+1] + nums[i+1]
来计算后缀和。
- 从倒数第二个元素开始向前,使用递推公式
-
查找中心点:
- 遍历
f
和g
数组,如果找到f[i] == g[i]
的索引i
,则返回i
。 - 如果遍历结束后没有找到符合条件的索引,返回
-1
。
- 遍历
这种算法的时间复杂度是 O(n),因为它只需要两次遍历来计算前缀和和后缀和,再加上一次遍历来比较它们,总的操作次数线性于数组长度。这种方法避免了在每次查询时都需要重新计算一侧的和,从而显著提高了效率。
举例
测试用例 nums = [1, 7, 3, 6, 5, 6]
四.除自身以为数组的乘积
题目链接:238.除自身以为数组的乘积
代码
public int[] productExceptSelf(int[] nums) {
int[] f=new int[nums.length];
int[] g=new int[nums.length];
int[] ret=new int[nums.length];
f[0]=1;
g[nums.length-1]=1;
for(int i=1;i<f.length;i++){
f[i]=f[i-1]*nums[i-1];
}
for(int i=g.length-2;i>=0;i--){
g[i]=g[i+1]*nums[i+1];
}
for(int i=0;i<nums.length;i++){
ret[i]=f[i]*g[i];
}
return ret;
}
算法原理
这段代码实现了一个功能,即计算一个整数数组 nums
中每个元素的乘积,但是不包括该元素自身。具体来说,对于 nums
数组中的每一个位置 i
,ret[i]
将是除了 nums[i]
之外所有其他元素的乘积。
算法的原理如下:
-
前缀乘积数组
f
:f
数组记录了从nums
数组的起始位置到当前位置(不包括当前位置)的所有元素的乘积。- 初始化
f[0]
为 1,因为没有之前的元素,乘积默认为 1。 - 对于每一个后续的位置
i
,f[i]
被设置为f[i-1]
和nums[i-1]
的乘积。
-
后缀乘积数组
g
:g
数组记录了从nums
数组的当前位置(不包括当前位置)到最后位置的所有元素的乘积。- 初始化
g[nums.length-1]
为 1,因为没有之后的元素,乘积默认为 1。 - 对于每一个之前的位置
i
,g[i]
被设置为g[i+1]
和nums[i+1]
的乘积。
-
结果数组
ret
:ret
数组存储最终的结果,即对于每一个位置i
,ret[i]
是除了nums[i]
外所有元素的乘积。- 因为
f[i]
包含了i
之前所有元素的乘积,而g[i]
包含了i
之后所有元素的乘积,所以将它们相乘就得到了除了nums[i]
以外所有元素的乘积。
这个算法的时间复杂度是 O(n),因为它遍历了数组两次(一次正向,一次反向),空间复杂度也是 O(n),用于存储 f
, g
和 ret
三个辅助数组。不过,可以通过优化减少空间复杂度至 O(1),只需要在原地更新 ret
数组即可。例如,在计算完 f
后,可以使用 f
数组直接计算 ret
,然后再计算 g
,并再次更新 ret
,这样就不需要额外的 g
数组了。
举例
测试用例 nums = [1,2,3,4]
首先初始化三个数组:
f
数组(前缀乘积):[1, _, _, _]
g
数组(后缀乘积):[_ , _, _, 1]
ret
数组(结果):[_, _, _, _]
接下来,我们根据算法步骤来填充这些数组:
步骤 1: 计算前缀乘积数组 f
f[0]
已经被初始化为1
f[1] = f[0] * nums[0] = 1 * 1 = 1
f[2] = f[1] * nums[1] = 1 * 2 = 2
f[3] = f[2] * nums[2] = 2 * 3 = 6
因此,f
数组现在是 [1, 1, 2, 6]
。
步骤 2: 计算后缀乘积数组 g
g[3]
已经被初始化为1
g[2] = g[3] * nums[3] = 1 * 4 = 4
g[1] = g[2] * nums[2] = 4 * 3 = 12
g[0] = g[1] * nums[1] = 12 * 2 = 24
因此,g
数组现在是 [24, 12, 4, 1]
。
步骤 3: 计算结果数组 ret
ret[0] = f[0] * g[0] = 1 * 24 = 24
ret[1] = f[1] * g[1] = 1 * 12 = 12
ret[2] = f[2] * g[2] = 2 * 4 = 8
ret[3] = f[3] * g[3] = 6 * 1 = 6
因此,ret
数组现在是 [24, 12, 8, 6]
。
这就是最终的结果,对于 nums = [1, 2, 3, 4]
,ret
数组 [24, 12, 8, 6]
正确地表示了除当前位置外所有元素的乘积。例如,ret[1]
是 12,这是因为除了 nums[1]
(也就是2)之外,其他所有元素的乘积是 1 * 3 * 4 = 12。同样的逻辑适用于其他元素。
🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀
以上,就是本期的全部内容啦,若有错误疏忽希望各位大佬及时指出💐
制作不易,希望能对各位提供微小的帮助,可否留下你免费的赞呢🌸