硅基计划4.0 算法 前缀和

硅基计划4.0 算法 前缀和


1755615271228



一、一维前缀和

题目链接
这道题就是我们一维前缀和的模板,接下来我会详细讲解原理

我们先来理解题意
题目中说数组从1下标开始,也就是说0下标的元素是默认值,为什么要这么干,稍后解释

输入示例中
第一行第一个数字3代表有三个元素
第二个数字2达标要进行2次查询
第二行是给的输入数组
第三行中1和2指的是从下标1到下标2区间内的元素之和,求和结果是3
第四行中2和3是指是从下标2到下标3区间内的元素之和,求和结果是6

倘若给出2 2,指的就是求下标2一个元素就好

好,题目解析完毕,我们来讲讲算法原理
既然要求的是区间的和,那我们就可以遍历数组,根据你要查询的区间,求出对应区间的和就好
但是,这样时间复杂度就很高,为什么高呢,你想,如果每次查询都从头开始,是不是总共要查询O(n*q)这么多
因此,我们利用数组是正数的原理,可以这样
我们可以先预处理一个前缀和数组dp[],其中dp[i]代表的是从[1,i]位置的元素之和
这就是我们动态规划里的状态表示

好,我们怎么求出dp[]中的每一个元素呢,其实很简单,你想想,我们求的是区间的和
那我们要求当前区间的和,是不是只需要先把从头到当前位置的和先算出来,然后减去前面区间的和,就是当前区间的和啦
比如[1,2,3,4],我们要求最后两个数字的区间的和,是不是只需要把整个数组和算出来,然后减去[1,2]就好

因此我们可以得到递推公式,即状态转移方程dp[i] = dp[i-1]+arr[i]
那我们有了dp[]数组,是不是就可以去使用了,如何使用
我们很容易得出区间公式[i,j] = dp[i] - dp[j-1],为什么要减一呢,因为我们求的是[1,j]区间,包括了j自己
image-20250821101448281

好,我们现在来回答为什么数组下标要从1开始,倘若我求的是原数组[0,2]区间的和,根据公式,那我应该是dp[2] - dp [-1],而-1会导致越界
因此我们dp[0]保持默认值,充当边界作用,而且我们保持默认值0,并不会影响最终结果

import java.util.Scanner;

// 注意类名必须为 Main, 不要有任何 package xxx 信息
public class Main {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        // 注意 hasNext 和 hasNextLine 的区别
        int n = in.nextInt(),m = in.nextInt();
        int [] arr = new int[n+1];
        for(int i = 1;i<arr.length;i++){
            arr[i] = in.nextInt();
        }
        //预处理前缀和数组,数字可能很大,要用到long
        long [] dp = new long[n+1];
        for(int i = 1;i<dp.length;i++){
            dp[i] = dp[i-1]+arr[i];
        }
        
        while(m > 0){
            int l = in.nextInt(),r = in.nextInt();
            System.out.println(dp[r]-dp[l-1]);
            m--;
        }
    }
}

二、二维前缀和

题目链接
这题比一位前缀和复杂的多,我们先来解读输入示例
第一行数字3和4代表是一个三行四列的矩阵,后面的3代表要进行3次查询
第二、三、四行代表矩阵数字
第五行代表从[1,1]位置到[2,2]位置所围成的区域和,为8
第五行代表从[1,1]位置到[3,3]位置所围成的区域和,为25
第五行代表从[1,2]位置到[3,4]位置所围成的区域和,为32

接下来我们讲解算法原理
暴力解法就不说了,遍历子数组区间就好,时间复杂度O(nmq)
我们利用前缀和思想,预处理一个与原数组同等规模的前缀和的二位数组(矩阵)
image-20250821102912992

好,我们如何使用呢,且看图解
image-20250821103426048

我们来分析下我们的时间复杂度,首先要创建前缀和数组,是O(m,n),再查询O(q),总共就是O(m,n)+O(q)

import java.util.Scanner;

// 注意类名必须为 Main, 不要有任何 package xxx 信息
public class Main {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        // 注意 hasNext 和 hasNextLine 的区别
        int n = in.nextInt(),m = in.nextInt(),q = in.nextInt();//n行m列q次查询
        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();
            }
        }
        //初始化前缀和数组
        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]-dp[i-1][j-1]+arr[i][j];
            }
        }
        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]);
            q--;
        }
    }
}

三、矩阵区域求和

题目链接
这道题意思是以一个坐标为中心,向周围扩展K个单位,求这个区域内的和
image-20250821104404657

好,我们要怎么求呢,根据我们刚刚讲的二维前缀和的知识就可以求,一模一样,也是求当前区域的和,但是有几个注意的地方
左上角坐标怎么表示,可以这样i-k,右下角同样i+k
就是关于越界的问题,就像刚刚图示一样,因此我们在求边角位置的时候,要让坐标不越界

//左上角位置
(max(0,i-k),max(0,j-k))
//右下角位置,m代表行,n代表列
(min(m-1,i+k),max(n-1,i+k))

还有,力扣中原数组下标从0开始的,当我们寻找(x,y)位置的值的时候,要去原数组中(x-1,y-1)中寻找
因此我们前缀和递推公式要改成dp[i][j] = dp[i-1][j]+dp[i][j-1]-dp[i-1][j-1]+mat[i-1][j-1];
因此我们左上角和右小角位置修改下

//左上角位置
(max(0,i-k)+1,max(0,j-k)+1)
//右下角位置,m代表行,n代表列
(min(m-1mi+k)+1,max(n-1,i+k)+1)
class Solution {
    public int[][] matrixBlockSum(int[][] mat, int k) {
        int row = mat.length;
        int column = mat[0].length;
        int [] [] dp = new int[row+1][column+1];
        for(int i = 1;i < row+1;i++){
            for(int j = 1;j < column+1;j++){
                dp[i][j] = dp[i-1][j]+dp[i][j-1]-dp[i-1][j-1]+mat[i-1][j-1];
            }
        }
        for(int i = 0;i < row;i++){
            for(int j = 0;j < column;j++){
                int x2 = Math.min(row-1,i+k)+1;
                int y2 = Math.min(column-1,j+k)+1;
                int x1 = Math.max(0,i-k)+1;
                int y1 = Math.max(0,j-k)+1;
                mat[i][j] = dp[x2][y2]-dp[x1-1][y2]-dp[x2][y1-1]+dp[x1-1][y1-1];
            }
        }
        return mat;
    }
}

四、寻找数组中心下标

题目链接
这题可以利用我们的前缀和思想,既然是要找到左右两边值相等的下标,那我们不妨这样
我们创造两个数组,一个是前缀f和,一个是后缀和g
但是由于题目是的是不包括当前下标的值得两边要相等
因此f[i]表示[0,i-1]区间内的和,同样g[i]表示[i+1,n-1]区域内的和
因此我们可以得出递推公式f[i] = f[i-1]+nums[i-1] g[i] = g[i+1]+nums[i+1];
最后我们再判断在什么位置相等就好

但是同样还是存在越界问题,你看,当我们求f(1)的时候根据公式f(1) = f(0)+nums(0)
f(0) = f(-1)+nums(-1),并不存在-1下标
因此我们要初始化f(0) = 0

同样求g(n-2) = g(n-1)+nums(n-1),而g(n-1) = g(n)+nums(n),不存在超过数组长度的下标,因此我们要初始化g(n-1) = 0

因此我们创建前缀和是从左到右,后缀和是从右到左

对于本题,创建数组默认值就是0,因此不需要管了

class Solution{
	public int pivotIndex(int[] nums){
		int n = nums.length;
		int[] lsum = new int[n];//前缀和
		int[] rsum = new int[n];//后缀和
		// 预处理前缀和后缀和数组
		for(int i = 1; i < n; i++){
			lsum[i] = lsum[i - 1] + nums[i - 1];
		}
		for(int i = n - 2; i >= 0; i--){
			rsum[i] = rsum[i + 1] + nums[i + 1];
		}
		// 判断
		for(int i = 0; i < n; i++){
			if(lsum[i] == rsum[i]){
				return i;
			}
		}
		return -1;
	}
}

这是只使用一个前缀和数组代码,注意错位问题

class Solution {
    public int pivotIndex(int[] nums) {
        //注意本题原数组和dp数组位置错开了一位,返回的时候尤其注意
        int length = nums.length;//3,对于下标2
        int [] dp = new int[length+1];//4,对于下标3
        for(int i = 1;i<=length;i++){
            dp[i] = dp[i-1]+nums[i-1];
        }
        for(int i = 1;i<=length;i++){
            int leftSum = dp[i-1];
            int rightSum = dp[length]-leftSum-nums[i-1];
            if(leftSum == rightSum){
                return i-1;
            }
        }
        return -1;
    }
}

五、除自身以外数组乘积

题目链接
这道题跟刚刚那道题差不多,只是把条件改成算出除当前下标以外的值的乘积
说白了还是可以像刚刚那样去划分区域,我们利用上一题做法,算出前缀积和后缀积再相乘即可
同样我们定义前缀积f和后缀积g
其中f(i)代表就是从[0,i-1]区间内的乘积,g(i)代表就是从[i+1,n-1]区间内的乘积
因此递推公式f(i) = f(i-1)*arr(i-1) g(i) = g(i+1)*arr(i+1)
我们的填表顺序还是f(i)从前往后填,g(i)从后往左填
因此我们的结果数组中ret[i] = f[i]*g[i]
同样我们记得要初始化边界条件,当f(0)时,我们不可以初始化成0,会导致计算结果错误,我们要初始化成f(0) = 1
同样g(n-1) = 1

class Solution {
    public int[] productExceptSelf(int[] nums) {
        int length = nums.length;
        int [] dp1 = new int[length];//前缀积
        int [] dp2 = new int[length];//后缀积
        //初始化边界
        dp1[0] = 1;
        dp2[length-1] = 1;
        //前缀积预处理
        for(int i = 1;i<length;i++){
            dp1[i] = dp1[i-1]*nums[i-1];
        }
        //后缀积预处理
        for(int i = length-2;i>=0;i--){
            dp2[i] = dp2[i+1]*nums[i+1];
        }
        for(int i = 0;i<length;i++){
            nums[i] = dp1[i]*dp2[i];
        }
        return nums;
    }
}

六、和为k的子数组

题目链接
这道题我们看到提示中说数组可能存在负数,因此就不具有单调性了,不能使用滑动窗口
暴力策略就是枚举所有子数组,看看有没有符合要求的,找到一个后不能停止枚举,因为可能还存在其他符合要求的子数组

我们可以利用前缀和思想
image-20250821134758006

你说为什么不去[0,i]区间内寻找,因为你想,我们i下标的值就算很大,它减去了k值,那它是不是相当于把当前的位置占了,我们只能在子区间内寻找符合要求的值了
因此我们刚刚画的图k的范围可能很大,也可能没有

好,我们还要统计出现次数,我们可以利用哈希表统计,那么其下标映射关系<int,int>
第一个int表示前缀和的值,第二个int表示出现次数

好,我们来说说几个细节问题

  1. 前缀和什么时候进入哈希表
    答:我们求当前下标符合要求的子数组,是不是要让哈希表只统计从0下标到我们当前下标区间内的前缀和出现次数
    为什么要这么做,因为如果你统计的不是我们当前区间范围内的前缀和出现次数,当我们往后遍历数组的时候,会导致前面已经统计过了一次,现在又要统计一次,出现了重复统计
    因此我们在进行计算完成统计好次数之后,准备往后遍历的时候,再把当前位置的前缀和放入就好

  2. 是否需要创建一个前缀和数组
    答:不需要,我们不需要统计区间长度,只需要统计次数,因此我们用一个sum变量充当求和值就好

  3. 如果我们数组中的第一个值就是k呢
    答:按照原理,那我们应该就要去[0,-1]区间内寻找,但是这就产生了越界情况
    因此,我们默认前缀和为0的至少出现了一次,即hash<0,1>

class Solution {
    public int subarraySum(int[] nums, int k) {
        Map<Integer,Integer> hash = new HashMap<Integer,Integer>();
        hash.put(0,1);
        int sum = 0;
        int ret = 0;
        for(int x:nums){
            sum += x;
            ret += hash.getOrDefault(sum-k,0);
            hash.put(sum,hash.getOrDefault(sum,0)+1);
        }
        return ret;
    }
}

七、和可被k整除的子数组

题目链接
首先我们先来了解一下同余定理
假设有两个数,他们能被p整除,得到k这个结果
那么根据定理(a-b)%p=k,即a % p = b % p余数相等
这个定理证明可以去网上搜搜,这里就不详述了

在Java中我们负数取模会导致结果是负数,因此我们需要修正a%p+p,但是如果a本身就是正数,会导致我们重复计算,因此我们进行最后修正(a%p+p)%p

好,这一题还是利用前缀和以及哈希表统计次数
image-20250821140911085

但是请注意,这一题并没有给我们区间x的值,因此它是一个随机变化的数,还记得我们刚刚的同余定理吗
我们代入这一题(sum - x) % k = 0 ---> sum % k = x % k
因此我们在求sum-x区间内的符合要求的子数组的时候,实质上就在求[0,i-1]区间内符合要求的子数组

因此我们的问题就转换成在[0,i-1]区间内寻找符合要求的子数组,使得2子数组的值总和能被k整除

因此我们创建的哈希表<int,int>,第一个int代表对应求的前缀和的余数,第二个int代表出现次数,其他还是不变,比如sum变量代表前缀和什么的

class Solution {
    public int subarraysDivByK(int[] nums, int k) {
        Map<Integer,Integer> hash = new HashMap<Integer,Integer>();
        hash.put(0 % k,1);
        int sum = 0;
        int ret = 0;
        for(int x : nums){
            sum += x;
            int r = (sum%k+k)%k;
            ret += hash.getOrDefault(r,0);
            hash.put(r,hash.getOrDefault(r,0)+1);
        }
        return ret;
    }
}

八、连续数组

题目链接
这一题我们可以这么想,如果我们统计个数,会非常麻烦,因为你的区间是不断变化的,你的哈希表每次都要更新
但是,如果我们把0换成-1,假如区间内01个数对等,那么它们和就会为0

因此我们哈希表<int,int>,第一个int代表前缀和,第二个int代表对应的下标

其次,我们要等到i下标数值参与计算后,往后走的时候再放入哈希表

再者,如果出现前缀和重复的情况,你想,我们遍历是从前往后遍历
我们只有选择下标值更小的i,才可以使得ij区间内所围成的长度最大

还有,关于计算长度,由于我们的j小标是包含在随机变量x里面的,因此我们的长度并不包括在内,因此我们的长度就是j+1~i,可以看我刚刚的图示

最后,关于我们的默认前缀和余数,它的值是0我们知道,但是它应该放入哪个下标呢?
答案是-1下标,为什么?这样在我们计算最长子数组的时候,就是求的0~i+1的长度了

class Solution {
    public int findMaxLength(int[] nums) {
        Map<Integer, Integer> hash = new HashMap<Integer, Integer>();
        hash.put(0, -1); // 默认存在⼀个前缀和为 0 的情况
        int sum = 0, ret = 0;
        for(int i = 0; i < nums.length; i++){
            sum += (nums[i] == 0 ? -1 : 1); // 计算当前位置的前缀和
            if(hash.containsKey(sum)){
                ret = Math.max(ret, i - hash.get(sum));
            }else{
                hash.put(sum, i);
            }
        }
        return ret;
    }
}

前缀和确实比较抽象,如果发现文章有错误,欢迎指正哦

END
### 开关频率与效率分析 开关(如MOSFET和IGBT)在功率电子系统中广泛应用,其开关频率与效率特性是设计和优化的关键因素。以下是对开关频率与效率特性的详细分析。 #### 1. 开关频率对效率的影响 开关频率的高低直接影响到系统的效率表现。当开关频率增加时,开关的开关损耗会显著上升,因为开关损耗与频率成正比[^1]。然而,较高的开关频率可以减少导通损耗,这是因为导通损耗主要与电流平方和导通时间相关。在实际应用中,例如DDR3内存供电场景下,800kHz被作为经验值选择,因为它能够在典型负载条件下实现效率(80%-90%+)和温升之间的较优平衡[^1]。 #### 2. 开关与宽禁带半导体的对比 相比于碳化(SiC)等宽禁带半导体器件,开关在高频应用中的表现存在明显劣势。SiC MOSFET具有更快的开关速度,这使得其开关损耗相较于传统IGBT降低了约77%[^2]。因此,在高频操作条件下,SiC器件能够更高效地转换电能,同时降低热损耗并提高整体系统的效率。此外,由于SiC器件支持更高的开关频率,还可以减小EMI滤波器的体积,进一步优化系统设计。 #### 3. 开关的应用限制 尽管开关在低频和中频领域仍然占据主导地位,但在高频应用场景中,其效率提升的空间有限。这主要是由于材料本身的物理限制,导致其无法有效支持极高的开关频率而不显著增加损耗。因此,在需要高效率和高频操作的场合,工程师通常会选择SiC或GaN等新型半导体材料作为替代方案。 ```python # 示例:计算不同开关频率下的损耗变化 def calculate_loss(frequency, switching_loss_per_cycle, conduction_loss): total_switching_loss = frequency * switching_loss_per_cycle return total_switching_loss + conduction_loss # 假设数据 frequency_1 = 500e3 # 500kHz frequency_2 = 800e3 # 800kHz switching_loss_per_cycle = 1e-6 # 每周期开关损耗 conduction_loss = 0.1 # 导通损耗 loss_500kHz = calculate_loss(frequency_1, switching_loss_per_cycle, conduction_loss) loss_800kHz = calculate_loss(frequency_2, switching_loss_per_cycle, conduction_loss) print(f"Loss at 500kHz: {loss_500kHz} W") print(f"Loss at 800kHz: {loss_800kHz} W") ``` 通过上述代码示例可以看出,随着开关频率从500kHz提升至800kHz,总损耗有所增加,这反映了开关频率对效率的直接影响。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值