本题来自左神《程序员代码面试指南》“未排序正数数组中累加和为给定值的最长子数组长度”题目。
题目
题解
本文提供的方法可以做到时间复杂度为O(N)、额外空间复杂度为O(N)。
为了说明解法,先定义s 的概念,s(i)代表子数组arr[0…i]所有元素的累加和。那么子数组arr[j…i(] 0≤j≤i<arr.length)的累加和为s(i)-s(j-1),因为根据定义,s(i)=arr[0…i]的累加和等于arr[0…j-1]的累加和与arr[j…i]的累加和相加,又有arr[0…j-1]的累加和为s(j-1)。所以,arr[j…i]的累加和为s(i)-s(j-1),这个结论是求解这道题的核心。
整个过程只遍历一次arr,具体过程为:
- 设置变量sum=0,表示从0 位置开始一直加到i 位置所有元素的和。设置变量len=0,表示累加和为k 的最长子数组长度。设置哈希表map,其中,key 表示从arr 最左边开始累加的过程中出现过的sum 值,对应的value 值则表示sum 值最早出现的位置。
- 从左到右开始遍历,遍历的当前元素为arr[i]。
- 令sum=sum+arr[i],即之前所有元素的累加和s(i),在map 中查看是否存在sum-k。
- 如果 sum-k 存在,从map 中取出sum-k 对应的value 值,记为j,j 代表从左到右不断累加的过程中第一次加出sum-k 这个累加和的位置。根据之前得出的结论,arr[j+1…i]的累加和为s(i)-s(j),此时s(i)=sum,又有s(j)=sum-k,所以arr[j+1…i]的累加和为k。同时因为map 中只记录每一个累加和最早出现的位置,所以此时的arr[j+1…i]是在必须以arr[i]结尾的所有子数组中,最长的累加和为k 的子数组,如果该子数组的长度大于len,就更新len。
- 如果 sum-k 不存在,说明在必须以arr[i]结尾的情况下没有累加和为k 的子数组。
- 检查当前的sum(即s(i))是否在map 中。如果不存在,说明此时的sum 值是第一次出现的,就把记录(sum,i)加入到map 中。如果sum 存在,说明之前已经出现过sum,map 只记录一个累加和最早出现的位置,所以此时什么记录也不加。
- 令sum=sum+arr[i],即之前所有元素的累加和s(i),在map 中查看是否存在sum-k。
- 继续遍历下一个元素,直到所有的元素遍历完。
大体过程如上,但还有一个很重要的问题需要处理。根据arr[j+1…i]的累加和为s(i)-(j),所以,如果从0 位置开始累加,会导致j+1≥1。也就是说,所有从0 位置开始的子数组都没有考虑过。所以,应该从-1 位置开始累加,也就是在遍历之前先把(0,-1)这个记录放进map,这个记录的意义是如果任何一个数都不加时,累加和为0。这样,从0 位置开始的子数组就被我们考虑到了。
具体过程请参看如下代码中的 maxLength 方法。
package chapter_8_arrayandmatrix;
import java.util.HashMap;
public class Problem_11_LongestSumSubArrayLength {
public static int maxLength(int[] arr, int k) {
if (arr == null || arr.length == 0) {
return 0;
}
HashMap<Integer, Integer> map = new HashMap<Integer, Integer>(); // 含义:<从0位置元素的累加和, 满足此累加和的最小下标>
map.put(0, -1); // 注意不要写成 (0, 0)
int len = 0;
int sum = 0;
for (int i = 0; i < arr.length; i++) {
sum += arr[i];
if (map.containsKey(sum - k)) {
len = Math.max(i - map.get(sum - k), len);
}
if (!map.containsKey(sum)) {
map.put(sum, i);
}
}
return len;
}
public static int[] generateArray(int size) {
int[] result = new int[size];
for (int i = 0; i != size; i++) {
result[i] = (int) (Math.random() * 11) - 5;
}
return result;
}
public static void printArray(int[] arr) {
for (int i = 0; i != arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
public static void main(String[] args) {
int[] arr = generateArray(20);
printArray(arr);
System.out.println(maxLength(arr, 10));
}
}
附:ACM赛制牛客题解(需手动输入输出),即我的解法,已通过全部测试用例
package test;
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
int k = scanner.nextInt();
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = scanner.nextInt();
}
// solution
Map<Integer, Integer> map = new HashMap<>(); // <从0位置元素的累加和, 满足此累加和的最小下标>
map.put(0, -1); // 注意不要写成 (0, 0)
int maxLen = 0, curSum = 0;
for (int i = 0; i < n; i++) {
curSum += arr[i];
if (!map.containsKey(curSum)) {
map.put(curSum, i);
}
int gap = curSum - k;
if (map.containsKey(gap)) {
maxLen = Math.max(i - map.get(gap), maxLen);
}
}
System.out.println(maxLen);
}
}
/*
【示例1】
11 0
1 -2 1 1 1 -1 1 -1 1 -1 2
答案:9
解析:1 [-2 1 1 1 -1 1 -1 1 -1] 2
【示例2】
20 0
-1 1 0 4 5 -2 2 -2 2 -2 2 -4 4 5 -2 -2 -1 1 1 1
答案:12
解析:-1 1 0 4 5 [-2 2 -2 2 -2 2 -4 4 5 -2 -2 -1] 1 1 1
*/