该文章通过四个案例讲解子数组前缀信息的构建:
- 构建前缀和
- 构建前缀和出现的最早位置
- 构建前缀和余数的最早位置
- 构建前缀奇偶状态的最早位置
每个案例都根据具体题目讲解,并给出了力扣或牛客的测试链接
本文参考 左神-b站左程云讲解046 教学视频,配合视频食用更佳!
前缀和
得到区域和
https://leetcode.cn/problems/range-sum-query-immutable/
思路:
给定数组 nums[-2, 0, 3, -5, 2, -1],维护一个前缀和数组,可以快速得到 nums 中 [left, right]范围内的累加和,即等于 sum[right + 1] - sum[left]; (前 right 个数累加和 - 前 left-1个数的累加和)
前缀和数组 从 1 开始
sum[i] = sum[i -1] + nums[i -1]
0 位置为 0,代表前 0 个数累加和为 0,这样免去了一个边界讨论
如果不补这个 0,那么 L ~ R 累加和 = (0~R) - (0 ~ L - 1) 要讨论边界
sum = [0, -2, -2, 1, -4, -2, -30]
class NumArray {
public int[] sum;
public NumArray(int[] nums) {
sum = new int[nums.length + 1];
for(int i = 1; i < sum.length; i++){
sum[i] = sum[i - 1] + nums[i - 1];
}
}
public int sumRange(int left, int right) {
return sum[right + 1] - sum[left];
}
}
前缀和最早位置
累加和为 k 的最长子数组
https://www.nowcoder.com/practice/36fb0fd3c656480c92b569258a1223d5
给定一个无序数组arr, 其中元素可正、可负、可0。给定一个整数k,求arr所有子数组中累加和为k的最长子数组长度 输入描述:
第一行两个整数N, k。N表示数组长度,k的定义已在题目描述中给出 第二行N个整数表示数组内的数 输出描述: 输出一个整数表示答案 输入:
5 0 1 -2 1 1 1 输出: 3
思路:
下标为i时的前缀和为sum 即0~i的和为sum
目标为 k,那么只要找前缀和为(sum -k) 出现的最早位置,就是i向左累加和为 k 的最长情况
用哈希表维护,要注意:哈希表必须放入前缀和为 0,位置为-1
假设数组为 [5, -5, 5], k = 5
i= 0 时,arr[0] = 5,0 到 0 的前缀和 sum=5,sum-aim=0,查 map,map 此时为 空,误认为以 0 结尾时凑不出前缀和为 0 的情况,就错过了答案
i = 2 时,同理,前缀和为 5,在 map 中查 0,map 仍然为空,又错过答案
所以要将 [0, -1] 放入 map
public class LongestSubarraySumEqualsAim {
public static int N;
public static int k;
public static int MAXN = 100002;
public static int[] arr = new int[MAXN];
/**
* key: 某个前缀和
* value: 该前缀和出现的最早位置
* 下标为i时的前缀和为sum 即0~i的和为sum,目标为k,那么只要找前缀和为(sum - k)出现的最早位置,就是i向左累加和为aim的最长情况
*/
public static HashMap<Integer, Integer> map = new HashMap<>();
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StreamTokenizer in = new StreamTokenizer(br);
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
while (in.nextToken() != StreamTokenizer.TT_EOF){
N = (int) in.nval;
in.nextToken();
k = (int) in.nval;
for(int i = 0; i < N; i++){
in.nextToken();
arr[i] = (int) in.nval;
}
out.println(compute());
}
out.flush();
br.close();
out.close();
}
public static int compute(){
map.clear();
// 重要 : 0这个前缀和,一个数字也没有的时候,就存在了
map.put(0, -1);
int ans = 0;
for(int i = 0, sum = 0; i < N; i++){
sum += arr[i];
if(!map.containsKey(sum)){
map.put(sum, i);
}
if(map.containsKey(sum - k)){
ans = Math.max(ans, i - map.get(sum - k));
}
}
return ans;
}
}
和为 k 的子数组个数
https://leetcode.cn/problems/subarray-sum-equals-k/
该题思路和上题同理,只不过由长度变为了个数
public int subarraySum(int[] nums, int k) {
HashMap<Integer, Integer> map = new HashMap<>();
map.put(0, 1);
int ans = 0;
for(int i = 0, sum = 0; i < nums.length; i++){
sum += nums[i];
ans += map.getOrDefault(sum - k, 0);
map.put(sum, map.getOrDefault(sum, 0) + 1);
}
return ans;
}
正负一样多的最长子数组
https://www.nowcoder.com/practice/545544c060804eceaed0bb84fcd992fb
给定一个无序数组arr,其中元素可正、可负、可0。求arr所有子数组中正数与负数个数相等的最长子数组的长度
思路:
转换:遇到正数就是 1, 0 还是 0,负数就是-1
就转换为 新数组中,累加和为 0 时的最长子数组
后续思路和上题同理
表现良好的最长时间段
https://leetcode.cn/problems/longest-well-performing-interval/description/
给你一份工作时间表 hours,上面记录着某一位员工每天的工作小时数。 我们认为当员工一天中的工作小时数大于 8
小时的时候,那么这一天就是「劳累的一天」。 所谓「表现良好的时间段」,意味在这段时间内,「劳累的天数」是严格 大于「不劳累的天数」。
请你返回「表现良好时间段」的最大长度。
思路:
临界值为 8,那么可以转化数组,大于 8 时为 1,小于 8 时为 -1
利用数组中只有 1 和-1 ,可以做到一次遍历
class Solution {
public int longestWPI(int[] hours) {
int sum = 0, ans = 0;
//记录每个前缀和出现的最早位置
HashMap<Integer, Integer> map = new HashMap<>();
map.put(0, -1);
for(int i = 0; i < hours.length; i++){
sum += hours[i] > 8 ? 1 : -1;
//如果前缀和大于0 此时0~i就是最长段
if(sum > 0){
ans = i + 1;
}else{
//如果sum<=0,找之前有没有比sum更小的数,如果有,一定是通过+1到达的目前sum
//那么在这段子区间内的累加和就大于0
//map中存储的是最早位置,所以取到的就是累加和为 sum-1 的最长子段
if(map.containsKey(sum - 1)){
ans = Math.max(ans, i - map.get(sum - 1));
}
}
//如果map中没有当前的 前缀和,再加入map,保证是最早位置
//再次出现同样的sum时,不更新
if(!map.containsKey(sum)){
map.put(sum, i);
}
}
return ans;
}
}
前缀和余数的最早位置
使数组和能被 P 整除
https://leetcode.cn/problems/make-sum-divisible-by-p/
给你一个正整数数组 nums,请你移除 最短 子数组(可以为 空),使得剩余元素的 和 能被 p 整除。 不允许 将整个数组都移除。
请你返回你需要移除的最短子数组的长度,如果无法满足题目要求,返回 -1 。 子数组 定义为原数组中连续的一组元素。
思路:
public int minSubarray(int[] nums, int p) {
int mod = 0;
for(int num : nums){
mod = (num + mod) % p;
}
// 累加完再求余的方法会溢出,要使用同余原理
// int sum = 0;
// for(int num : nums){
// sum += num;
// }
// mod = sum % p;
if(mod == 0) return 0;
HashMap<Integer, Integer> map = new HashMap<>();
map.put(0, -1);
int ans = Integer.MAX_VALUE;
for(int i = 0, cur = 0; i < nums.length; i++){
//0...i部分余数
cur = (cur + nums[i]) % p;
int find = cur >= mod ? (cur - mod) : (cur + p - mod);
if(map.containsKey(find)){
//找到每个地方移除数组的长度,取最小的那个
ans = Math.min(ans, i - map.get(find));
}
map.put(cur, i);
}
return ans == nums.length? -1 : ans;
}
前缀奇偶状态的最早位置
每个元音包含偶数次的最长子字符串
https://leetcode.cn/problems/find-the-longest-substring-containing-vowels-in-even-counts/
给你一个字符串 s ,请你返回满足以下条件的最长子字符串的长度:每个元音字母,即 ‘a’,‘e’,‘i’,‘o’,‘u’
,在子字符串中都恰好出现了偶数次。 输入:s = “eleetminicoworoep” 输出:13 解释:最长子字符串是
“leetminicowor” ,它包含 e,i,o 各 2 个,以及 0 个 a,u 。
public int findTheLongestSubstring(String s) {
int ans = 0; //初始设为0,长度最小就为0,不设为MIN_VALUE
// u o i e a 偶数次置0 奇数次置1 5位二进制数 共32种状态
int[] map = new int[32]; //用数组作为哈希表
Arrays.fill(map, -2); //-2表示没出现过
//0 0 0 0 0 初始出现在-1
map[0] = -1;
for(int i = 0, status = 0, m; i < s.length(); i++){
//status 0 ~ i-1 的奇偶状态,下面要判断第i位
m = move(s.charAt(i));
if(m != -1){
status ^= (1 << m); //改变status这个位置的奇偶性
}
//找之前是否出现过相同状态的奇偶性,如果出现了,位置为j
//那么j~i这一段就全是偶数次 0 0 0 0 0
if(map[status] != -2){
ans = Math.max(ans, i - map[status]);
}else{
map[status] = i;
}
}
return ans;
}
public int move(char c){
switch(c){
case 'a': return 0;
case 'e': return 1;
case 'i': return 2;
case 'o': return 3;
case 'u': return 4;
default: return -1;
}
}