前缀和

前缀和

前缀和又称累计和,是指将序列中从起始位置到当前位置的所有元素进行求和

java 复制代码
prefixSum[0] = nums[0]
prefixSum[1] = nums[0] + nums[1]
prefixSum[2] = nums[0] + nums[1] + nums[2]
...
prefixSum[i] = nums[0] + nums[1] + ... + nums[i]

解题操作顺序

  1. 先求前缀和pre
  2. 判断当前pre-k是否在hashmap中并完成计数
  3. 最后将pre加入hashmap

560. 和为 K 的子数组

注意初始添加mp.put(0, 1),记录前缀和为0的次数为1

关键变量解析(pre,mp,count,均为全局变量)

1. pre(前缀和)
  • 含义 :从数组起始位置到当前位置 i 的所有元素之和,即 pre = nums[0] + nums[1] + ... + nums[i]
  • 作用 :将子数组和问题转化为前缀和的差值问题。例如,子数组 nums[j..i] 的和为 pre[i] - pre[j]
2. mp(哈希表)
  • 含义:键为前缀和的值,值为该前缀和出现的次数。
  • 作用:快速查询历史前缀和的出现次数,判断是否存在满足条件的子数组。
3. count(计数器)
  • 含义 :记录满足和为 k 的子数组的总数量。
  • 作用:累加所有可能的子数组组合。
官方题解:
java 复制代码
public class Solution {
    public int subarraySum(int[] nums, int k) {
        int count = 0, pre = 0;
        HashMap < Integer, Integer > mp = new HashMap < > ();
        mp.put(0, 1);
        for (int i = 0; i < nums.length; i++) {
            pre += nums[i];
            if (mp.containsKey(pre - k)) {
                count += mp.get(pre - k);
            }
            mp.put(pre, mp.getOrDefault(pre, 0) + 1);
        }
        return count;
    }
}

我的第一次实现:

也不能用双指针,因为数据中可能包含负数。

java 复制代码
public int subarraySum(int[] nums, int k) {
    int res = 0;
    int[] preSum = new int[nums.length+1];
    preSum[0] = 0;
    for(int i=1;i<=nums.length;i++) {
        preSum[i] = preSum[i-1] + nums[i-1];
        for(int j=0;j<i;j++) {
            if(preSum[j] == preSum[i]-k) {
                res++;
            }
        }
    }
    return res;  
}

437. 路径总和 III

【解法一:两重遍历】

容易想到一种简单直接的两步做法,第一步,遍历所有结点,第二步,对当前结点执行一次以其为根结点的DFS,寻找以其为路径起点的满足条件的路径。下面给出三种在不同的实现。

  • 版本一:BFS+DFS。前一个BFS为第一步,后一个DFS为第二步。
  • 版本二:DFS+DFS。将第一步考察每一个结点的动作用DFS实现,后一个DFS作用不变。
  • 版本三:带返回值的DFS+DFS。在前面两个版本的做法中,将累计量count设置为类变量,在第二步的DFS中搜索到满足条件的路径时立即累计,而无需考虑返回值。本版本不设置类变量,而是通过将当以当前结点为起始的满足要求的路径数量返回,层层返回到root,由root给出最终累计量。

带返回值的版本三相比前两个版本理解上要稍困难些,在这里稍加解释。对于每一个节点node,pathSum(node, targetSum)返回值表示由这个节点为根结点的树中满足要求的路径数量。而nodeSum(node, targetSum)计算从node开始的路径结点数值和为targetSum的数量。即nodeSum(node, targetSum) = count + nodeSum(node.left, targetSum - node.val) + nodeSum(node.right, targetSum - node.val)。细节请看「代码」。

  • 时间复杂度:O(n^2),n为结点数量。一次遍历为O(n),对每个结点,最多将考察O(n)个结点。
  • 空间复杂度:O(n),队列或栈深度。
java 复制代码
// 版本一:BFS + DFS
class Solution {
    int count, num, targetSum;
    public int pathSum(TreeNode root, int targetSum) {
        if(root == null) return 0;
        this.targetSum = targetSum;
        Queue<TreeNode> q = new ArrayDeque<>();
        q.add(root);
        while(!q.isEmpty()){ // BFS遍历所有结点
            TreeNode head = q.remove();
            check(head, 0); // 考察以当前结点为起始的满足要求的路径数量
            if(head.left != null) q.add(head.left);
            if(head.right != null) q.add(head.right);
        }
        return count;
    }
    private void check(TreeNode node, int sum){
        if(node == null) return;
        sum = sum + node.val;
        if(sum == targetSum) count++; // 一旦满足,立即累计
        check(node.left, sum);
        check(node.right, sum);
    }
}
// 版本二:DFS + DFS(不带返回值)
class Solution {
    int count, num, targetSum;
    public int pathSum(TreeNode root, int targetSum) {
        if(root == null) return 0;
        this.targetSum = targetSum;
        dfs(root); // DFS遍历所有结点
        return count;
    }
    private void dfs(TreeNode node){
        if(node == null) return;
        check(node, 0); // 考察以当前结点为起始的满足要求的路径数量
        dfs(node.left);
        dfs(node.right);
    }
    private void check(TreeNode node, int sum){
        if(node == null) return;
        sum = sum + node.val;
        if(sum == targetSum) count++; // 一旦满足,立即累计
        check(node.left, sum);
        check(node.right, sum);
    }
}
// 版本三:DFS + DFS(带返回值)
class Solution {
    public int pathSum(TreeNode root, int targetSum) {
        if(root == null) return 0;
        int count = nodeSum(root, targetSum);
        return count + pathSum(root.left, targetSum) + pathSum(root.right, targetSum);
    }
    private int nodeSum(TreeNode node, int targetSum){
        if(node == null) return 0;
        int count = 0, val = node.val;
        if(val == targetSum) count++;
        return count + nodeSum(node.left, targetSum - val) + nodeSum(node.right, targetSum - val);
    }
}

【解法二:前缀和】

解法一中存在许多明显的重复计算,考虑如何利用已经计算过的信息。类似在数组中找连续子数组和为targetSum,本题也在父子链路上体现出从某点到某点的连续性。这启发我们使用前缀和方法处理。使用dfs遍历一次整棵树,实时地计算从root到当前结点的前缀和。在同一条路径上,更短的前缀和已经被计算出来了,为了对前缀和求差以查找是否存在pre_i - pre_j = targetSum(pre_i为当前结点前缀和,pre_j为当前路径上i节点之前的结点j的前缀和),我们需要保存之前结点的前缀和。map是一个不假思索的选择,key保存前缀和,value保存对应此前缀和的数量。需要注意的是,前缀和求差的对象是同一条路径上的结点,因此在dfs遍历树的过程中,当到达叶子结点,之后向上返回时,路径退缩,使得当前结点将退出后续路径。对前缀和求差的前提是要保证map中所保存的前缀和均为同一路径上的结点的前缀和,因此需要删除返回前的节点所代表的前缀和。

  • 时间复杂度:O(n),n为结点总数,只需一次遍历。
  • 空间复杂度:O(n),map空间。
java 复制代码
// 也可以用有返回值的dfs(返回值表示到当前结点位置的累计量),
// 但为了专注于「前缀和」解法本身,如下实现的dfs不带返回值,
// 而以类变量count,在找到满足要求的前缀和时立即累计。
class Solution {
    int targetSum, count = 0;
    Map<Integer, Integer> map;
    public int pathSum(TreeNode root, int targetSum) {
        if(root == null) return 0;
        this.targetSum = targetSum;
        this.map = new HashMap<>();
        map.put(0, 1); // 表示前缀和为0的节点为空,有一个空。否则若pre_i = targetSum,将错过从root到i这条路径。
        dfs(root, 0);
        return count;
    }
    private void dfs(TreeNode node, int preSum){
        if(node == null) return;
        preSum += node.val;
        count += map.getOrDefault(preSum - targetSum, 0); // #1 累计满足要求的前缀和数量
        map.put(preSum, map.getOrDefault(preSum, 0) + 1); // #2 先累计再put(先#1,再#2)
        dfs(node.left, preSum);
        dfs(node.right, preSum);
        map.put(preSum, map.get(preSum) - 1); // 路径退缩,去掉不再在路径上的当前结点的前缀和。必存在,无需使用getOrDefault。
    }
}

我的答案:官方测试用例中有一个例子,使用int型会数值越界,所以改成了Long类型

java 复制代码
class Solution {
	long targetSum;
	int count = 0;
	HashMap <Long,Integer> mp = new HashMap<>();
    public int pathSum(TreeNode root, long targetSum) {

    	this.targetSum = targetSum;
    	if(root == null) {
    		return 0;
    	}
    	mp.put((long) 0,1);
    	dfs(root, 0);
    	return count;
        
    }
    public void dfs(TreeNode root,long preSum){
    	if(root==null) {
    		return;
    	}
    	preSum+=root.val;
    	count+=mp.getOrDefault(preSum-targetSum,0);
		mp.put(preSum, mp.getOrDefault(preSum, 0)+1);
    	dfs(root.left, preSum);
    	dfs(root.right, preSum);
    	mp.put(preSum, mp.get(preSum)-1);
    	preSum-=root.val;
    }
	
}