前缀和的解题思想:
前缀和的题目解题思维比较固定,即当我们循环数组到下标N时,需要用到数组前N-1项的计算的结果(这里不一定非要是和,也可能是积等),此时我们就该考虑是否应该通过计算数组循环过程中的累计值的方式简化解题,如此便有了前缀和的解题思想。
了解了思想,下来就该考虑,这个累计的结果我们该通过什么方式保存起来呢?
题目明确要求不允许使用额外空间的,直接原地修改数组
不限制空间复杂度时,最好额外开辟空间计算,避免数据污染
计算时如果每次只需要获取前一次的累计结果,可以通过数组的方式存储每次获取数组末尾元素的值
如果每次计算需要获取前几次或更多次的结果进行对比时,推荐哈希表的方式,这样可以压缩时间复杂度
例题1: 给定一个整数数组和一个整数 k ,请找到该数组中和为 k 的连续子数组的个数。(Offer 010)
大神的解释:
这道题目非常简洁,就是求数组中何为整数k的连续子数组个数。
如果这道题的取值没有负数,那就是标准的滑窗问题,但因为有了负数,滑窗思想不能用了。
通过分析,这道题应该属于我们上面列举四种情况的最后一种。具体思路如下:
初始化一个空的哈希表和pre_sum=0的前缀和变量
设置返回值ret = 0,用于记录满足题意的子数组数量
循环数组的过程中,通过原地修改数组的方式,计算数组的累加和
将当前累加和减去整数K的结果,在哈希表中查找是否存在
如果存在该key值,证明以数组某一点为起点到当前位置满足题意,ret加等于将该key值对应的value
判断当前的累加和是否在哈希表中,若存在value+1,若不存在value=1
最终返回ret即可
但在这里要注意刚才说到的前缀和边界问题。
我们在计算这种场景时,需要考虑如果以数组nums[0]为开头的连续子数组就满足题意呢?
此时候我们的哈希表还是空的,没办法计算前缀和!所以遇到这类题目,都需要在哈希表中默认插入一个{0:1}的键值对,
用于解决从数组开头的连续子数组满足题意的特殊场景。
下面就开始解题吧!
// 使用前缀和的方法:
class Solution {
public int subarraySum(int[] nums, int k) {
// 创建一个HashMap将前缀和与其出现的次数建立联系
Map<Integer,Integer> map=new HashMap<Integer,Integer>();
// 创建一个变量来记录前n项和
int sum=0;
// 创建一个变量来记录满足条件的子数组的个数
int count=0;
// 为什么一开始就要将(0,1)建立联系?避免以数组nums[0]为开头的连续子数组就满足k
map.put(0,1);
for(int num:nums){
sum=sum+num;
// 判断是否有目前位置与前面的某个位置之间的和能满足条件
// 如果有count的个数加上sum-k对应的键值
count=count+map.getOrDefault(sum-k,0);
map.put(sum,map.getOrDefault(sum,0)+1);
}
return count;
}
}
例题2: 2055. 蜡烛之间的盘子
/*
// 本来想要使用队列来,但看了评论说什么抢银行,忽然觉得前缀和的思路更好
class Solution {
// 创建一个Map来保存每个蜡烛的位置和其左边的盘子数量
Map<Integer,Integer> map=new HashMap();
public int[] platesBetweenCandles(String s, int[][] queries) {
// 创建一个变量来保存蜡烛的数量
int tempcount=0;
// 保存每一根蜡烛左边的盘子的数量
for(int i=0;i<s.length();i++){
if(s.charAt(i)=='*'){
tempcount++;
}
if(s.charAt(i)=='|'){
map.put(i,tempcount);
}
}
// 创建一个数组保存结果
int[] result=new int[queries.length];
if(map.keySet().size()<=1){
return result;
}
for(int i=0;i<queries.length;i++){
int temp=anwser(queries[i][0],queries[i][1],s);
result[i]=temp;
}
return result;
}
public int anwser(int left,int right,String s){
int mostleft=left;
int mostright=right;
// 获取每个区间内最左边的蜡烛的位置
while(s.charAt(mostleft)!='|'&mostleft<=right){
mostleft++;
}
while(s.charAt(mostright)!='|'&mostright>=left){
mostright--;
}
if(mostleft>=mostright){
return 0;
}else{
return map.get(mostright)-map.get(mostleft);
}
}
}
*/
// 上面的方法超时了因为每一次都要,寻找该区间最左边的和最右边的盘子的位置
// 所以可以对盘子的位置进行预处理
class Solution {
// 创建一个Map来保存每个蜡烛的位置和其左边的盘子数量
Map<Integer,Integer> map=new HashMap();
public int[] platesBetweenCandles(String s, int[][] queries) {
// 创建一个变量来保存蜡烛的数量
int tempcount=0;
// 保存每一根蜡烛左边的盘子的数量
for(int i=0;i<s.length();i++){
if(s.charAt(i)=='*'){
tempcount++;
}
if(s.charAt(i)=='|'){
map.put(i,tempcount);
}
}
int len=s.length();
// 创建一个数组保存该位置左边的第一个蜡烛的位置
int[] left=new int[len];
for(int i=0,pd=-1;i<len;i++){
if(s.charAt(i)=='|'){
pd=i;
}
left[i]=pd;
}
// 创建一个数组保存该位置右边的第一个蜡烛的位置
int[] right=new int[len];
for(int i=len-1,pd=-1;i>=0;i--){
if(s.charAt(i)=='|'){
pd=i;
}
right[i]=pd;
}
// 创建一个数组保存结果
int[] result=new int[queries.length];
for(int i=0;i<queries.length;i++){
// 获取该区间左边的第一根蜡烛
int mostleft=right[queries[i][0]];
int mostright=left[queries[i][1]];
// 判断是否存在两根以上的蜡烛
if(mostright==-1||mostleft==-1||mostright<=mostleft){
result[i]=0;
}else{
result[i]=map.get(mostright)-map.get(mostleft);
}
}
return result;
}
}
例题3: 加法变乘法(插乘枚举)
这道题:主要运用了前缀和+双指针解决问题
package one;
import java.util.ArrayList;
import java.util.List;
class Solution_9{
// 救我个人感觉这道题可以使用动态规划+双指针+前缀和
// 创建一个和动态规划数组保存从1到该索引的和
int[] dp=new int[50];
// 创建一个集合保存结果
List<Integer> list=new ArrayList<Integer>();
public void solution(){
// 初始动态规划数组
dp[0]=0;
for(int i=1;i<=49;i++) {
dp[i]=i+dp[i-1];
}
// 使用双指针控制两个乘号插入的位置
for(int i=1;i<=47;i++) {
for(int j=i+1;j<=48;j++) {
if(findsum(i,j)==2015) {
list.add(i);
}
}
}
// 输出结果看看
for(int i=0;i<list.size();i++) {
System.out.println(list.get(i));
}
}
// 创建一个函数根据两个*的位置来求得该算数得和
public int findsum(int i,int j) {
int sum=dp[i-1]+i*(i+1)+(dp[j-1]-dp[i+1])+j*(j+1)+(dp[49]-dp[j+1]);
return sum;
}
}
public class None_9 {
public static void main(String[] args) {
Solution_9 a=new Solution_9();
a.solution();
}
}
例题4:
package three;
import java.util.Scanner;
//连续子区间序列,滑动窗口解决
//因为需要找出所有的可能性所以滑动窗口有点难以使用
//一个新的思路就是滑动窗口加上前缀和思路
public class Three_11 {
public static void main(String[] args) {
// 先创建一个输入实例
Scanner sc=new Scanner(System.in);
int N=sc.nextInt();
int k=sc.nextInt();
int[] nums= new int[N];
for(int i=0;i<N;i++) {
nums[i]=sc.nextInt();
}
// 创建一个变量保存所有符合条件的区间
int count=0;
// 创建一个数组保存从0开始到自身索引之和
int[] dp=new int[nums.length];
dp[0]=nums[0];
for(int i=1;i<nums.length;i++) {
dp[i]=dp[i-1]+nums[i];
}
// 利用双重循环遍历所有可能的区间
for(int right=0;right<nums.length;right++) {
if(dp[right]%k==0) {
count++;
}
for(int left=0;left<right;left++) {
if((dp[right]-dp[left])%k==0) {
count++;
}
}
}
// 输出结果
System.out.print(count);
}
}