前缀和
前缀和又称累计和,是指将序列中从起始位置到当前位置的所有元素进行求和
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]
解题操作顺序
- 先求前缀和pre
- 判断当前pre-k是否在hashmap中并完成计数
- 最后将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;
}
}