阅读视图

发现新文章,点击刷新页面。

每日一题-使数组和能被 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

【套路】前缀和+哈希表(Python/Java/C++/Go)

前置知识

模运算的世界:当加减乘除遇上取模

提示 1

例如 $\textit{nums}=[11,2,5,7,8,9]$,$p=10$,那么把 $[5,7]$ 去掉,剩余的数字相加等于 $30$,可以被 $p$ 整除。

所有元素的和 $42\bmod 10=2$,而 $(5+7)\bmod 10$ 也等于 $2$。

设所有元素的和为 $x$,去掉的元素和为 $y$。要使 $x-y$ 能被 $p$ 整除,根据前置知识中同余的定义,这等价于满足

$$
y \equiv x \pmod p
$$

提示 2

把 $y$ 用 前缀和 表示,问题转换成:在前缀和数组上找到两个数 $s[\textit{left}]$ 和 $s[\textit{right}]$,满足 $\textit{right}-\textit{left}$ 最小且

$$
s[\textit{right}]-s[\textit{left}]\equiv x \pmod p
$$

根据前置知识,将上式移项,得

$$
s[\textit{right}]-x \equiv s[\textit{left}]\pmod p
$$

上式相当于

$$
((s[\textit{right}]-x)\bmod p+p)\bmod p= s[\textit{left}]\bmod p
$$

也可以写成

$$
(s[\textit{right}]\bmod p-x\bmod p+p)\bmod p= s[\textit{left}]\bmod p
$$

提示 3

遍历 $s$ 的同时,用哈希表 $\textit{last}$ 记录 $s[i]\bmod p$ 最近一次出现的下标,如果 $\textit{last}$ 中包含 $(s[i]\bmod p-x\bmod p+p)\bmod p$,设其对应的下标为 $j$,那么 $[j,i)$ 是一个符合题目要求的子数组。

注意:本题可以移除空子数组,所以要先更新 $\textit{last}$,再更新答案。

枚举所有 $i$,计算符合要求的子数组长度的最小值,就是答案。如果没有符合要求的子数组,则返回 $-1$。

代码实现时,可以把答案初始化成 $\textit{nums}$ 的长度 $n$。如果最后答案等于 $n$,则表示没有符合要求的子数组,因为题目不允许将整个数组都移除。

答疑

:为什么不能用双指针(不定长滑动窗口)做?

:使用双指针需要满足单调性,但是 $s[i]\bmod p$ 并不是单调的,所以不能用双指针。具体请看【基础算法精讲 03】

class Solution:
    def minSubarray(self, nums: List[int], p: int) -> int:
        s = list(accumulate(nums, initial=0))
        x = s[-1] % p
        if x == 0:
            return 0  # 移除空子数组(这行可以不要)

        ans = n = len(nums)
        last = {}
        for i, v in enumerate(s):
            last[v % p] = i
            j = last.get((v - x) % p, -n)  # 如果不存在,-n 可以保证 i-j >= n
            ans = min(ans, i - j)
        return ans if ans < n else -1
class Solution {
    public int minSubarray(int[] nums, int p) {
        int n = nums.length;
        int[] s = new int[n + 1];
        for (int i = 0; i < n; i++) {
            s[i + 1] = (s[i] + nums[i]) % p;
        }
        int x = s[n];
        if (x == 0) {
            return 0; // 移除空子数组(这行可以不要)
        }

        int ans = n;
        Map<Integer, Integer> last = new HashMap<>();
        for (int i = 0; i <= n; i++) {
            last.put(s[i], i);
            // 如果不存在,-n 可以保证 i-j >= n
            int j = last.getOrDefault((s[i] - x + p) % p, -n);
            ans = Math.min(ans, i - j);
        }
        return ans < n ? ans : -1;
    }
}
class Solution {
public:
    int minSubarray(vector<int> &nums, int p) {
        int n = nums.size(), s[n + 1];
        s[0] = 0;
        for (int i = 0; i < n; i++) {
            s[i + 1] = (s[i] + nums[i]) % p;
        }
        int x = s[n];
        if (x == 0) {
            return 0; // 移除空子数组(这行可以不要)
        }

        int ans = n;
        unordered_map<int, int> last;
        for (int i = 0; i <= n; ++i) {
            last[s[i]] = i;
            auto it = last.find((s[i] - x + p) % p);
            if (it != last.end()) {
                ans = min(ans, i - it->second);
            }
        }
        return ans < n ? ans : -1;
    }
};
func minSubarray(nums []int, p int) int {
    n := len(nums)
    s := make([]int, n+1)
    for i, v := range nums {
        s[i+1] = (s[i] + v) % p
    }
    x := s[n]
    if x == 0 {
        return 0 // 移除空子数组(这个 if 可以不要)
    }

    ans := n
    last := map[int]int{}
    for i, v := range s {
        last[v] = i
        if j, ok := last[(v-x+p)%p]; ok {
            ans = min(ans, i-j)
        }
    }
    if ans < n {
        return ans
    }
    return -1
}

也可以不用前缀和数组,一边遍历 $\textit{nums}$ 一边计算前缀和。

class Solution:
    def minSubarray(self, nums: List[int], p: int) -> int:
        x = sum(nums) % p
        if x == 0:
            return 0  # 移除空子数组(这行可以不要)

        ans = n = len(nums)
        s = 0
        last = {s: -1}  # 由于下面 i 是从 0 开始的,前缀和下标就要从 -1 开始了
        for i, v in enumerate(nums):
            s += v
            last[s % p] = i
            j = last.get((s - x) % p, -n)  # 如果不存在,-n 可以保证 i-j >= n
            ans = min(ans, i - j)  # 改成手写 min 会再快一些
        return ans if ans < n else -1
class Solution {
    public int minSubarray(int[] nums, int p) {
        long t = 0;
        for (int v : nums) {
            t += v;
        }
        int x = (int) (t % p);
        if (x == 0) {
            return 0; // 移除空子数组(这行可以不要)
        }

        int n = nums.length;
        int ans = n;
        int s = 0;
        Map<Integer, Integer> last = new HashMap<>();
        last.put(s, -1); // 由于下面 i 是从 0 开始的,前缀和下标就要从 -1 开始了
        for (int i = 0; i < n; i++) {
            s = (s + nums[i]) % p;
            last.put(s, i);
            // 如果不存在,-n 可以保证 i-j >= n
            int j = last.getOrDefault((s - x + p) % p, -n);
            ans = Math.min(ans, i - j);
        }
        return ans < n ? ans : -1;
    }
}
class Solution {
public:
    int minSubarray(vector<int> &nums, int p) {
        int x = reduce(nums.begin(), nums.end(), 0LL) % p;
        if (x == 0) {
            return 0; // 移除空子数组(这行可以不要)
        }

        int n = nums.size(), ans = n, s = 0;
        // 由于下面 i 是从 0 开始的,前缀和下标就要从 -1 开始了
        unordered_map<int, int> last{{s, -1}};
        for (int i = 0; i < n; i++) {
            s = (s + nums[i]) % p;
            last[s] = i;
            auto it = last.find((s - x + p) % p);
            if (it != last.end()) {
                ans = min(ans, i - it->second);
            }
        }
        return ans < n ? ans : -1;
    }
};
func minSubarray(nums []int, p int) int {
    x := 0
    for _, v := range nums {
        x += v
    }
    x %= p
    if x == 0 {
        return 0 // 移除空子数组(这个 if 可以不要)
    }

    n := len(nums)
    ans, s := n, 0
    // 由于下面 i 是从 0 开始的,前缀和下标就要从 -1 开始了
    last := map[int]int{s: -1}
    for i, v := range nums {
        s += v
        last[s%p] = i
        if j, ok := last[(s-x+p)%p]; ok {
            ans = min(ans, i-j)
        }
    }
    if ans < n {
        return ans
    }
    return -1
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$,其中 $n$ 为 $\textit{nums}$ 的长度。
  • 空间复杂度:$\mathcal{O}(n)$。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

使数组和能被 P 整除

方法一:前缀和

定理一:给定正整数 $x$、$y$、$z$、$p$,如果 $y \bmod p = x$,那么 $(y - z) \bmod p = 0$ 等价于 $z \bmod p = x$。

证明:$y \bmod p = x$ 等价于 $y = k_1 \times p + x$,$(y-z) \bmod p = 0$ 等价于 $y - z = k_2 \times p$,$z \bmod p = x$ 等价于 $z = k_3 \times p + x$,其中 $k_1$、$k_2$、$k_3$ 都是整数,那么给定 $y = k_1 \times p + x$,有 $y - z = k_2 \times p \leftrightarrow z = (k_1 - k_2) \times p + x \leftrightarrow z = k_3 \times p + x$。

定理二:给定正整数 $x$,$y$,$z$,$p$,那么 $(y - z) \bmod p = x$ 等价于 $z \bmod p = (y - x) \bmod p$。

证明:$(y - z) \bmod p = x$ 等价于 $y - z = k_1 \times p + x$,其中 $k_1$ 是整数,经过变换有 $z = y - k_1 \times p - x = k_2 \times p + (y - x) \bmod p - k_1 \times p = (k_2 - k_1) \times p + (y - x) \bmod p$,等价于 $z \bmod p = (y - x) \bmod p$。

记数组和除以 $p$ 的余数为 $x$,如果 $x=0$ 成立,那么需要移除的最短子数组长度为 $0$。

记前 $i$ 个元素(不包括第 $i$ 个元素)的和为 $\textit{f}i$,我们考虑最右元素为 $\textit{nums}[i]$ 的所有子数组,假设最左元素为 $\textit{nums}[j]~(0 \le j \le i)$,那么对应的子数组和为 $\textit{f}{i+1}-\textit{f}j$,对应的长度为 $i-j+1$。由定理一可知,如果剩余子数组和能被 $p$ 整除,那么 $(\textit{f}{i+1}-\textit{f}j) \bmod p = x$。同时由定理二可知,$\textit{f}j \bmod p = (\textit{f}{i+1} - x) \bmod p$。因此当 $\textit{f}{i+1}$ 已知时,我们需要找到所有满足 $\textit{f}j \bmod p = (\textit{f}{i+1} - x) \bmod p$ 的 $\textit{f}_j$($0 \le j \le i$),从中找到最短子数组。

由于需要移除最短子数组,因此对于所有 $f_j$($0 \le j \le i$),只需要保存 $f_j \bmod p$ 对应的最大下标。

有些编程语言对负数进行取余时,余数为负数,因此计算 $f_{i+1} - x$ 除以 $p$ 的余数时,使用 $f_{i+1} - x + p$ 替代。

###Python

class Solution:
    def minSubarray(self, nums: List[int], p: int) -> int:
        x = sum(nums) % p
        if x == 0:
            return 0
        y = 0
        index = {0: -1}
        ans = len(nums)
        for i, v in enumerate(nums):
            y = (y + v) % p
            if (y - x) % p in index:
                ans = min(ans, i - index[(y - x) % p])
            index[y] = i
        return ans if ans < len(nums) else -1

###C++

class Solution {
public:
    int minSubarray(vector<int>& nums, int p) {
        int x = 0;
        for (auto num : nums) {
            x = (x + num) % p;
        }
        if (x == 0) {
            return 0;
        }
        unordered_map<int, int> index;
        int y = 0, res = nums.size();
        for (int i = 0; i < nums.size(); i++) {
            index[y] = i; // f[i] mod p = y,因此哈希表记录 y 对应的下标为 i
            y = (y + nums[i]) % p;
            if (index.count((y - x + p) % p) > 0) {
                res = min(res, i - index[(y - x + p) % p] + 1);
            }
        }
        return res == nums.size() ? -1 : res;
    }
};

###Java

class Solution {
    public int minSubarray(int[] nums, int p) {
        int x = 0;
        for (int num : nums) {
            x = (x + num) % p;
        }
        if (x == 0) {
            return 0;
        }
        Map<Integer, Integer> index = new HashMap<Integer, Integer>();
        int y = 0, res = nums.length;
        for (int i = 0; i < nums.length; i++) {
            index.put(y, i); // f[i] mod p = y,因此哈希表记录 y 对应的下标为 i
            y = (y + nums[i]) % p;
            if (index.containsKey((y - x + p) % p)) {
                res = Math.min(res, i - index.get((y - x + p) % p) + 1);
            }
        }
        return res == nums.length ? -1 : res;
    }
}

###C#

public class Solution {
    public int MinSubarray(int[] nums, int p) {
        int x = 0;
        foreach (int num in nums) {
            x = (x + num) % p;
        }
        if (x == 0) {
            return 0;
        }
        IDictionary<int, int> index = new Dictionary<int, int>();
        int y = 0, res = nums.Length;
        for (int i = 0; i < nums.Length; i++) {
            // f[i] mod p = y,因此哈希表记录 y 对应的下标为 i
            if (!index.ContainsKey(y)) {
                index.Add(y, i);
            } else {
                index[y] = i;
            }
            y = (y + nums[i]) % p;
            if (index.ContainsKey((y - x + p) % p)) {
                res = Math.Min(res, i - index[(y - x + p) % p] + 1);
            }
        }
        return res == nums.Length ? -1 : res;
    }
}

###C

#define MIN(a, b) ((a) < (b) ? (a) : (b))

typedef struct {
    int key;
    int val;
    UT_hash_handle hh;
} HashItem; 

HashItem *hashFindItem(HashItem **obj, int key) {
    HashItem *pEntry = NULL;
    HASH_FIND_INT(*obj, &key, pEntry);
    return pEntry;
}

bool hashAddItem(HashItem **obj, int key, int val) {
    if (hashFindItem(obj, key)) {
        return false;
    }
    HashItem *pEntry = (HashItem *)malloc(sizeof(HashItem));
    pEntry->key = key;
    pEntry->val = val;
    HASH_ADD_INT(*obj, key, pEntry);
    return true;
}

bool hashSetItem(HashItem **obj, int key, int val) {
    HashItem *pEntry = hashFindItem(obj, key);
    if (!pEntry) {
        hashAddItem(obj, key, val);
    } else {
        pEntry->val = val;
    }
    return true;
}

int hashGetItem(HashItem **obj, int key, int defaultVal) {
    HashItem *pEntry = hashFindItem(obj, key);
    if (!pEntry) {
        return defaultVal;
    }
    return pEntry->val;
}

void hashFree(HashItem **obj) {
    HashItem *curr = NULL, *tmp = NULL;
    HASH_ITER(hh, *obj, curr, tmp) {
        HASH_DEL(*obj, curr);  
        free(curr);             
    }
}

int minSubarray(int* nums, int numsSize, int p) {
     int x = 0;
    for (int i = 0; i < numsSize; i++) {
        x = (x + nums[i]) % p;
    }
    if (x == 0) {
        return 0;
    }
    HashItem *index = NULL;
    int y = 0, res = numsSize;
    for (int i = 0; i < numsSize; i++) {
        hashSetItem(&index, y, i); // f[i] mod p = y,因此哈希表记录 y 对应的下标为 i
        y = (y + nums[i]) % p;
        if (hashFindItem(&index, (y - x + p) % p)) {
            int val = hashGetItem(&index, (y - x + p) % p, 0);
            res = MIN(res, i - val + 1);
        }
    }
    hashFree(&index);
    return res == numsSize ? -1 : res;
}

###JavaScript

var minSubarray = function(nums, p) {
    let x = 0;
    for (const num of nums) {
        x = (x + num) % p;
    }
    if (x === 0) {
        return 0;
    }
    const index = new Map();
    let y = 0, res = nums.length;
    for (let i = 0; i < nums.length; i++) {
        index.set(y, i); // f[i] mod p = y,因此哈希表记录 y 对应的下标为 i
        y = (y + nums[i]) % p;
        if (index.has((y - x + p) % p)) {
            res = Math.min(res, i - index.get((y - x + p) % p) + 1);
        }
    }
    return res === nums.length ? -1 : res;
};

###go

func minSubarray(nums []int, p int) int {
    sum := 0
    mp := map[int]int{0: -1}
    for _, v := range nums {
        sum += v
    }
    rem := sum%p
    if rem == 0 {
        return 0
    }
    minCount := len(nums)
    sum = 0
    for i := 0; i < len(nums); i++ {
        sum += nums[i]
        tempRem := sum%p
        k := (tempRem - rem + p) % p
        if _, ok := mp[k]; ok {
            minCount = min(minCount, i - mp[k])
        }
        mp[tempRem] = i
    }
    
    if minCount >= len(nums) {
        return -1
    }
    
    return minCount
}

func min(a, b int) int {
    if a < b {
        return a
    }
    return b
}

复杂度分析

  • 时间复杂度:$O(n)$,其中 $n$ 是数组 $\textit{nums}$ 的长度。遍历数组 $\textit{nums}$ 需要 $O(n)$ 的时间。

  • 空间复杂度:$O(n)$。保存哈希表需要 $O(n)$ 的空间。

做一题送一题,力扣上不少类似题

首先,这道题的思路和 974 一样(当然有一些变化)。强烈建议对这个问题不熟悉的同学,看一下 974,搞懂以后,再回来看这道题:

974. 和可被 K 整除的子数组


假设 nums 的和除以 P,余数是 mod

如果 mod == 0,答案就是 0

如果 mod != 0,答案变成了找原数组中的最短连续子数组,使得其数字和除以 P,余数也是 mod


由于是求解连续子数组和的问题,很容易想到使用前缀和。

我们可以扫描一遍整个数组,计算到每个元素的前缀和。

假设当前前缀和除以 P 的余数是 curmod,为了找到一段连续子数组对 P 的余数是 mod,我们需要找到一段前缀和,对 P 的余数是 targetmod。其中 targetmod 的求法是:

如果 curmod >= mod,很简单:targetmod = curmod - mod

如果 curmod < mod,我们需要加上一个 Ptargetmod = curmod - mod + P

这样,我们可以保证,当前前缀和减去目标前缀和,剩余的数组对 P 的余数是 mod。我们只需要找最短的这样的数组就好。


最后,为了快速找到一段对 P 的余数为 targetmod 的前缀和,我们使用一个哈希表 table,来存储之前前缀和对 P 的余数和所在的索引。(key 为余数;value 为索引)。

table 在遍历过程中更新,以保证每次在 table 中查找到的,是离当前元素最近的索引,从而保证找到的是“最短”的连续子数组。


我的参考代码(C++):

class Solution {
public:
    int minSubarray(vector<int>& nums, int p) {

        long long sum = 0;
        for(int e: nums) sum += (long long)e;
        long long mod = sum % (long long)p;

        if(mod == 0ll) return 0;

        int res = nums.size();
        unordered_map<long long, int> table;
        table[0ll] = -1;

        sum = 0;
        for(int i = 0; i < nums.size(); i ++){
            sum += (long long)nums[i];
            long long curmod = sum % (long long)p;
            table[curmod] = i;

            long long targetmod = curmod >= mod ? (curmod - mod) : (curmod - mod + p);
            if(table.count(targetmod))
                res = min(res, i - table[targetmod]);
        }
        return res == nums.size() ? -1 : res;
    }
};

觉得有帮助请点赞哇!

mac电脑安装nvm

方案一、常规安装 下载安装脚本:在终端中执行以下命令来下载并运行 NVM 的安装脚本3: bash 配置环境变量:安装完成后,需要配置环境变量。如果你的终端使用的是 bash,打开或创建~/.ba

2025年CSS新特性大盘点

大家好,我是 Immerse,一名独立开发者、内容创作者、AGI 实践者。 关注公众号:沉浸式趣谈,获取最新文章(更多内容只在公众号更新) 个人网站:https://yaolifeng.com 也同步
❌