周赛这道题没做出来,但也很开心

昨天第35周双周赛赛有一道题目,没做出来,事后,通过这个题目在力扣上挖出来另外两道题目,而另外两道题目用到的知识点,综合起来,来解决周赛的题目,就能迎刃而解了,所以,虽然周赛的题目没做出来,但学到两个知识点,同样很开心。

周赛题目是这样的。

  1. 使数组和能被 P 整除

给你一个正整数数组 nums,请你移除 最短 子数组(可以为 空),使得剩余元素的 和 能被 p 整除。 不允许 将整个数组都移除。

请你返回你需要移除的最短子数组的长度,如果无法满足题目要求,返回 -1 。

子数组 定义为原数组中连续的一组元素。
示例 1: 输入:nums = [3,1,4,2], p = 6 输出:1
解释:nums 中元素和为 10,不能被 p 整除。我们可以移除子数组 [4] ,剩余元素的和为 6 。
提示:
1 <= nums.length <= 10^5
1 <= nums[i] <= 10^9
1 <= p <= 10^9

暴力解法的话,需要3层循环,外面两层确定要移出的子数组的区间,第三层循环用来求子区间的和,时间复杂度是O(n^3)。如果用前缀和来计算子区间的和的话,能把时间复杂度降低到O(n^2),我就是这样写的。如下面代码清单所示。

class Solution {
    public int minSubarray(int[] nums, int p) {
        int n = nums.length;
        long[] sum = new long[n + 1];
        for (int i = 0; i < n; i++) {
            sum[i+1] = (long)sum[i] + nums[i];
        }
        if (sum[n] % p == 0) {
            return 0;
        }
        
        for (int len = 1; len < n; len++) {
            for (int i = 0; i + len <= n; i++) {
                long remain = sum[n] - (sum[i+len] - sum[i]);
                if (remain % p == 0) {
                    return len;
                }
            }
        }
        return -1;
    }
}

但这种写法还是会超时,看数据规模在10的5次方,要在O(n)时间内完成才有可能不会超时。时间复杂度要降低,那么一般是要用空间换时间,空间的话,能不能用哈希表,哈希表的查询复杂度为O(1),哈希表在这里怎么使用,我在比赛中是没有想到的,需要用到两个知识点:哈希表在子数组中的应用和同余定理。下面的文章会先引入另外两个题目的求解方法,进而探寻周赛的5504题的解法。

和为K的子数组

给定一个整数数组和一个整数 k,你需要找到该数组中和为 k 的连续的子数组的个数。

示例 1 :

输入:nums = [1,1,1], k = 2
输出: 2 , [1,1] 与 [1,1] 为两种不同的情况。
说明 :
数组的长度为 [1, 20,000]。
数组中元素的范围是 [-1000, 1000] ,且整数 k 的范围是 [-1e7, 1e7]。

这个题目暴力方法就是利用前缀和+两层for循环定位子数组,时间复杂度是O(n^2)
sum[i]表示数组nums[0...i]的元素和,那么如果要求某一个子数组,比如nums[i...j]的元素和为sum[j] - sum[i-1],用前缀和的方式可以在O(1)时间得到。
本题中,要使某一个子数组的和为k,用公式描述为sum[j] - sum[i-1]=k,变换一下公式为sum[j] - k = sum[i-1],也就是我们在遍历数组的前缀和的时候,在遍历到sum[j]的时候,在sum[j]之前有多少个sum[j]-k出现。
我们建立一个哈希表map,以前缀和为key,以出现的次数为value,遍历数组的前缀和数组,把所有符合条件map.get(sum[j]-k)相加,即可得到最终的结果。哈希表的初始值为{0:1}, 因为需要考虑前缀和本身的值为k的情况。
进一步的优化,前缀和sum[i]的计算只与前一项前缀和有关,所以可以只用变量preSum来代替。
下面的动画描述了算法和为k的子数组的个数的过程。
在这里插入图片描述
和为k的子数组的个数 – 代码清单:

class Solution {
    public int subarraySum(int[] nums, int k) {
        int n = nums.length;
        // sum[j] - sum[i-1] = k => sum[i-1] = sum[j] - k
        int ans = 0, preSum = 0;
        Map<Integer, Integer> map = new HashMap<>();
        map.put(0, 1);
        for (int i = 0; i < n; i++) {
            preSum += nums[i];
            int needSum = preSum - k;
            ans += map.getOrDefault(needSum, 0);
            map.put(preSum, map.getOrDefault(preSum, 0) + 1);
        }
        return ans;
    }
}

至此,哈希表在求解子数组类的题目的用法清楚了。

和可被 K 整除的子数组

  1. 和可被 K 整除的子数组
    给定一个整数数组 A,返回其中元素之和可被 K 整除的(连续、非空)子数组的数目。

示例:

输入:A = [4,5,0,-2,-3,1], K = 5
输出:7
解释: 有 7 个子数组满足其元素之和可被 K = 5 整除: [4,
5, 0, -2, -3, 1], [5], [5, 0], [5, 0, -2, -3], [0], [0, -2, -3], [-2,
-3]

提示:

1 <= A.length <= 30000
-10000 <= A[i] <= 10000
2 <= K <= 10000

这道题目用前缀和+两层循环做同样会超时,这里需要用到同余的性质。
同余定理

给定一个正整数m,如果两个整数a和b满足a-b能够被m整除,即(a-b)/m得到一个整数,那么就称整数a与b对模m同余,记作a≡b(mod m)。

sum[i]表示数组nums[0...i]的元素和,那么如果要求某一个子数组,比如nums[i...j]的元素和为sum[j] - sum[i-1],用前缀和的方式可以在O(1)时间得到。
本题中,要使某一个子数组能被K整除,用公式描述为(sum[j] - sum[i-1]) % K = 0,由同余定理可知,sum[j]sum[i-1]是同余的,也就是我们在遍历数组的前缀和的时候,在遍历到sum[j]的时候,要求在sum[j]之前有多少个sum[j] % K出现。
我们建立一个哈希表map,以前缀和对K的余数为key,以出现的次数为value,遍历数组的前缀和数组,把所有符合条件map.get(sum[j] % K)相加,即可得到最终的结果。哈希表的初始值为{0:1}, 因为需要考虑前缀和本身能被K整除的情况。
数组中有负数存在,在java语言中,负数的模运算还是负值,所以要进行处理,比如对于数a进行模运算,a为负数,要进行的处理为:(K + a % K) % K
代码清单如下。

class Solution {
    public int subarraysDivByK(int[] A, int K) {
        int n = A.length;

        int ans = 0, preSum = 0;
        Map<Integer, Integer> map = new HashMap<>();
        map.put(0, 1);
        for (int i = 0; i < n; i++) {
            preSum = (K + (preSum + A[i]) % K) % K;
            int count = map.getOrDefault(preSum, 0);
            ans += count;
            map.put(preSum, count + 1);
        }
        return ans;
    }
}

现在,同余定理也掌握了,再回到周赛的题目。

使数组和能被 P 整除

  1. 使数组和能被 P 整除 给你一个正整数数组 nums,请你移除 最短 子数组(可以为 空),使得剩余元素的 和 能被 p 整除。 不允许 将整个数组都移除。

请你返回你需要移除的最短子数组的长度,如果无法满足题目要求,返回 -1 。

子数组 定义为原数组中连续的一组元素。

示例 1:

输入:nums = [3,1,4,2], p = 6
输出:1
解释:nums 中元素和为 10,不能被 p 整除。我们可以移除子数组
[4] ,剩余元素的和为 6 。
示例 2:

输入:nums = [6,3,5,2], p = 9
输出:2
解释:我们无法移除任何一个元素使得和被 9 整除,最优方案是移除子数组[5,2] ,剩余元素为 [6,3],和为 9 。
示例 3:

输入:nums = [1,2,3], p = 3
输出:0
解释:和恰好为 6 ,已经能被 3 整除了。所以我们不需要移除任何元素。
示例 4:

输入:nums = [1,2,3], p = 7
输出:-1
解释:没有任何方案使得移除子数组后剩余元素的和被 7 整除。
示例 5:
输入:nums = [1000000000,1000000000,1000000000], p = 3
输出:0

提示:

1 <= nums.length <= 105
1 <= nums[i] <= 109
1 <= p <= 109

分析
同样使用前缀和以及哈希表。为写程序方便,设sum[i]表示数组nums[0...i-1]的元素和对p取余的结果,sum[0]=0sum[n]表示整个数组的和对p取余,如果sum[n]为0,说明数组本身就能被p整除,无需移除子数组。
如果sum[n]不为0,设sum[n]r,那么我们要移除的子数组的和(对p取余)也应该是r,这样才能保证移除子数组后,sum[n]为0。
假设我们要移除的子数组为nums[i...j],移除子数组部分的和为sum[j+1] - sum[i],有如下推导关系。
在这里插入图片描述
也就是我们在遍历数组的前缀和的时候,在遍历到sum[j+1]的时候,要求在sum[j+1]之前出现sum[j+1]-sum[n]的位置,进而得到要移除的数组的长度。
我们建立一个哈希表map,以前缀和对p的余数为key,以下标位置为value,遍历数组的前缀和数组,找出所有符合条件map.get((sum[j+1]-sum[n]+p) % p)对应的下标,维护一个变量ans记录最短的子数组,即可得到最终的结果。哈希表的初始值为{0:0}
代码清单如下:

class Solution {
    public int minSubarray(int[] nums, int p) {
        int n = nums.length;
        int[] sum = new int[n + 1];
        for (int i = 0; i < n; i++) {
            sum[i+1] = (sum[i] + nums[i]) % p;
        }
        if (sum[n] == 0) {
            return 0;
        }
        int ans = n + 1;
        Map<Integer, Integer> map = new HashMap<>();
        map.put(0, 0);
        for (int i = 1; i <= n; i++) {
            int needSum = (sum[i] - sum[n] + p) % p;
            if (map.containsKey(needSum)) {
                ans = Math.min(ans, i - map.get(needSum));        
            }
            map.put(sum[i], i);
        }
        return ans >= n ? -1 : ans;
    }
}

至此,大功告成。学到两个知识点:

  • 哈希表在解决子数组问题上的应用
  • 同余定理的使用

扫二维码关注我,一起讨论算法。
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值