2598.执行操作后的最大MEX

目标

给你一个下标从 0 开始的整数数组 nums 和一个整数 value 。

在一步操作中,你可以对 nums 中的任一元素加上或减去 value 。

  • 例如,如果 nums = [1,2,3] 且 value = 2 ,你可以选择 nums[0] 减去 value ,得到 nums = [-1,2,3] 。

数组的 MEX (minimum excluded) 是指其中数组中缺失的最小非负整数。

  • 例如,[-1,2,3] 的 MEX 是 0 ,而 [1,0,3] 的 MEX 是 2 。

返回在执行上述操作 任意次 后,nums 的最大 MEX 。

示例 1:

输入:nums = [1,-10,7,13,6,8], value = 5
输出:4
解释:执行下述操作可以得到这一结果:
- nums[1] 加上 value 两次,nums = [1,0,7,13,6,8]
- nums[2] 减去 value 一次,nums = [1,0,2,13,6,8]
- nums[3] 减去 value 两次,nums = [1,0,2,3,6,8]
nums 的 MEX 是 4 。可以证明 4 是可以取到的最大 MEX 。

示例 2:

输入:nums = [1,-10,7,13,6,8], value = 7
输出:2
解释:执行下述操作可以得到这一结果:
- nums[2] 减去 value 一次,nums = [1,-10,0,13,6,8]
nums 的 MEX 是 2 。可以证明 2 是可以取到的最大 MEX 。

说明:

  • 1 <= nums.length, value <= 10^5
  • -10^9 <= nums[i] <= 10^9

思路

有一个数组,可以执行 任意 次操作,每次操作可以加上或者减去 value,求数组中缺失的最小非负整数的最大值。

关键点是想清楚,只要余数相同就能够操作成该数字,剩下的就是对余数的计数了。

代码


/**
 * @date 2025-10-16 8:46
 */
public class FindSmallestInteger2598 {

    public int findSmallestInteger(int[] nums, int value) {
        int[] cnt = new int[value];
        for (int num : nums) {
            int rem = num % value;
            rem = rem >= 0 ? rem : rem + value;
            cnt[rem]++;
        }
        int res = 0;
        while (true) {
            int rem = res % value;
            if (cnt[rem] > 0) {
                cnt[rem]--;
            } else {
                return res;
            }
            res++;
        }
    }

}

性能

3350.检测相邻递增子数组II

目标

给你一个由 n 个整数组成的数组 nums ,请你找出 k 的 最大值,使得存在 两个 相邻 且长度为 k 的 严格递增 子数组。具体来说,需要检查是否存在从下标 a 和 b (a < b) 开始的 两个 子数组,并满足下述全部条件:

  • 这两个子数组 nums[a..a + k - 1] 和 nums[b..b + k - 1] 都是 严格递增 的。
  • 这两个子数组必须是 相邻的,即 b = a + k。

返回 k 的 最大可能 值。

子数组 是数组中的一个连续 非空 的元素序列。

示例 1:

输入:nums = [2,5,7,8,9,2,3,4,3,1]
输出:3
解释:
从下标 2 开始的子数组是 [7, 8, 9],它是严格递增的。
从下标 5 开始的子数组是 [2, 3, 4],它也是严格递增的。
这两个子数组是相邻的,因此 3 是满足题目条件的 最大 k 值。

示例 2:

输入:nums = [1,2,3,4,4,4,4,5,6,7]
输出:2
解释:
从下标 0 开始的子数组是 [1, 2],它是严格递增的。
从下标 2 开始的子数组是 [3, 4],它也是严格递增的。
这两个子数组是相邻的,因此 2 是满足题目条件的 最大 k 值。

说明:

  • 2 <= nums.length <= 2 * 10^5
  • -10^9 <= nums[i] <= 10^9

思路

从数组中找出两个相邻的长度相同的严格递增子数组,求子数组的最大长度。

3349.检测相邻递增子数组I 相比,本题求的是满足条件的最大 k 值。上一题使用动态规划记录以当前元素结尾的严格递增子数组的长度,可以取两个相邻递增子数组的最后一个 dp 值,前一个记为 prev,后一个记为 cur。那么最大的长度为 Math.max(cur / 2, Math.min(prev, cur)),即将当前的 cur 拆分为两个递增子数组,或者取 prevcur 的最小值。可以进行空间优化,仅使用两个变量即可。

代码


/**
 * @date 2025-10-15 8:45
 */
public class MaxIncreasingSubarrays3350 {

    public int maxIncreasingSubarrays(List<Integer> nums) {
        int n = nums.size();
        int cur = 1;
        int prev = 0;
        int res = 1;
        for (int i = 1; i < n; i++) {
            if (nums.get(i) > nums.get(i - 1)) {
                cur++;
                res = Math.max(res, Math.max(cur / 2, Math.min(prev, cur)));
            } else {
                prev = cur;
                cur = 1;
            }
        }
        return res;
    }
}

性能

3349.检测相邻递增子数组I

目标

给你一个由 n 个整数组成的数组 nums 和一个整数 k,请你确定是否存在 两个 相邻 且长度为 k 的 严格递增 子数组。具体来说,需要检查是否存在从下标 a 和 b (a < b) 开始的 两个 子数组,并满足下述全部条件:

  • 这两个子数组 nums[a..a + k - 1] 和 nums[b..b + k - 1] 都是 严格递增 的。
  • 这两个子数组必须是 相邻的,即 b = a + k。

如果可以找到这样的 两个 子数组,请返回 true;否则返回 false。

子数组 是数组中的一个连续 非空 的元素序列。

示例 1:

输入:nums = [2,5,7,8,9,2,3,4,3,1], k = 3
输出:true
解释:
从下标 2 开始的子数组为 [7, 8, 9],它是严格递增的。
从下标 5 开始的子数组为 [2, 3, 4],它也是严格递增的。
两个子数组是相邻的,因此结果为 true。

示例 2:

输入:nums = [1,2,3,4,4,4,4,5,6,7], k = 5
输出:false

说明:

  • 2 <= nums.length <= 100
  • 1 <= 2 * k <= nums.length
  • -1000 <= nums[i] <= 1000

思路

判断是否存在两个长度为 k 的相邻的子数组,要求子数组内严格递增。

定义 dp[i] 表示以 i 结尾的递增子数组的最大长度,如果 nums[i] > nums[i - 1], dp[i] = dp[i - 1] + 1,否则 dp[i] = 1。最后只需判断是否存在 i 使得 dp[i] >= k && dp[i + k] >= k 即可。

代码


/**
 * @date 2025-10-14 9:02
 */
public class HasIncreasingSubarrays3349 {

    public boolean hasIncreasingSubarrays(List<Integer> nums, int k) {
        int n = nums.size();
        int[] dp = new int[n];
        dp[0] = 1;
        for (int i = 1; i < n; i++) {
            if (nums.get(i) > nums.get(i - 1)) {
                dp[i] = dp[i - 1] + 1;
            } else {
                dp[i] = 1;
            }
        }
        for (int i = k - 1; i + k < n; i++) {
            if (dp[i] >= k && dp[i + k] >= k) {
                return true;
            }
        }
        return false;
    }

}

性能

2273.移除字母异位词后的结果数组

目标

给你一个下标从 0 开始的字符串 words ,其中 words[i] 由小写英文字符组成。

在一步操作中,需要选出任一下标 i ,从 words 中 删除 words[i] 。其中下标 i 需要同时满足下述两个条件:

  1. 0 < i < words.length
  2. words[i - 1] 和 words[i] 是 字母异位词 。

只要可以选出满足条件的下标,就一直执行这个操作。

在执行所有操作后,返回 words 。可以证明,按任意顺序为每步操作选择下标都会得到相同的结果。

字母异位词 是由重新排列源单词的字母得到的一个新单词,所有源单词中的字母通常恰好只用一次。例如,"dacb" 是 "abdc" 的一个字母异位词。

示例 1:

输入:words = ["abba","baba","bbaa","cd","cd"]
输出:["abba","cd"]
解释:
获取结果数组的方法之一是执行下述步骤:
- 由于 words[2] = "bbaa" 和 words[1] = "baba" 是字母异位词,选择下标 2 并删除 words[2] 。
  现在 words = ["abba","baba","cd","cd"] 。
- 由于 words[1] = "baba" 和 words[0] = "abba" 是字母异位词,选择下标 1 并删除 words[1] 。
  现在 words = ["abba","cd","cd"] 。
- 由于 words[2] = "cd" 和 words[1] = "cd" 是字母异位词,选择下标 2 并删除 words[2] 。
  现在 words = ["abba","cd"] 。
无法再执行任何操作,所以 ["abba","cd"] 是最终答案。

示例 2:

输入:words = ["a","b","c","d","e"]
输出:["a","b","c","d","e"]
解释:
words 中不存在互为字母异位词的两个相邻字符串,所以无需执行任何操作。

说明:

  • 1 <= words.length <= 100
  • 1 <= words[i].length <= 10
  • words[i] 由小写英文字母组成

思路

定义字母异位词是字母构成完全相同的单词,有一个单词列表,如果相邻的单词是字母异位词,仅保留第一个,删除剩余字母异位词,返回最终结果数组。

判断是否是异位词可以将每个单词的字符排序后进行比较,根据题意仅保留第一个单词即可。

代码


/**
 * @date 2025-10-13 8:51
 */
public class RemoveAnagrams2273 {

    public List<String> removeAnagrams(String[] words) {
        int n = words.length;
        String[] tmp = new String[n];
        for (int i = 0; i < n; i++) {
            char[] chars = words[i].toCharArray();
            Arrays.sort(chars);
            tmp[i] = new String(chars);
        }
        List<String> res = new ArrayList<>();
        res.add(words[0]);
        for (int i = 1; i < n; i++) {
            if (tmp[i].equals(tmp[i - 1])) {
                continue;
            }
            res.add(words[i]);
        }
        return res;
    }

}

性能

3539.魔法序列的数组乘积之和

目标

给你两个整数 M 和 K,和一个整数数组 nums。

一个整数序列 seq 如果满足以下条件,被称为 魔法 序列:

  • seq 的序列长度为 M。
  • 0 <= seq[i] < nums.length
  • 2^seq[0] + 2^seq[1] + ... + 2^seq[M - 1] 的 二进制形式 有 K 个 置位。

这个序列的 数组乘积 定义为 prod(seq) = (nums[seq[0]] * nums[seq[1]] * ... * nums[seq[M - 1]])

返回所有有效 魔法 序列的 数组乘积 的 总和 。

由于答案可能很大,返回结果对 10^9 + 7 取模。

置位 是指一个数字的二进制表示中值为 1 的位。

示例 1:

输入: M = 5, K = 5, nums = [1,10,100,10000,1000000]
输出: 991600007
解释:
所有 [0, 1, 2, 3, 4] 的排列都是魔法序列,每个序列的数组乘积是 1013。

示例 2:

输入: M = 2, K = 2, nums = [5,4,3,2,1]
输出: 170
解释:
魔法序列有 [0, 1],[0, 2],[0, 3],[0, 4],[1, 0],[1, 2],[1, 3],[1, 4],[2, 0],[2, 1],[2, 3],[2, 4],[3, 0],[3, 1],[3, 2],[3, 4],[4, 0],[4, 1],[4, 2] 和 [4, 3]。

示例 3:

输入: M = 1, K = 1, nums = [28]
输出: 28
解释:
唯一的魔法序列是 [0]。

说明:

  • 1 <= K <= M <= 30
  • 1 <= nums.length <= 50
  • 1 <= nums[i] <= 10^8

思路

代码

性能

3186.施咒的最大总伤害

目标

一个魔法师有许多不同的咒语。

给你一个数组 power ,其中每个元素表示一个咒语的伤害值,可能会有多个咒语有相同的伤害值。

已知魔法师使用伤害值为 power[i] 的咒语时,他们就 不能 使用伤害为 power[i] - 2 ,power[i] - 1 ,power[i] + 1 或者 power[i] + 2 的咒语。

每个咒语最多只能被使用 一次 。

请你返回这个魔法师可以达到的伤害值之和的 最大值 。

示例 1:

输入:power = [1,1,3,4]
输出:6
解释:
可以使用咒语 0,1,3,伤害值分别为 1,1,4,总伤害值为 6 。

示例 2:

输入:power = [7,1,6,6]
输出:13
解释:
可以使用咒语 1,2,3,伤害值分别为 1,6,6,总伤害值为 13 。

说明:

  • 1 <= power.length <= 10^5
  • 1 <= power[i] <= 10^9

思路

有一个数组 power 表示咒语的伤害值,魔法师使用其中任意咒语后,就无法再使用伤害值 +1 +2 -1 -2 的咒语,每个咒语只能使用一次,求伤害值的最大值。

使用哈希表记录伤害值相同的和,创建新数组(不包含重复伤害值),根据伤害值排序。如果不选当前伤害值,状态可以从后面转移而来,如果选则从后面大于当前伤害值 + 2 的转移而来。

代码


/**
 * @date 2025-10-11 8:57
 */
public class MaximumTotalDamage3186 {

    public long maximumTotalDamage(int[] power) {
        Map<Integer, Long> sum = new HashMap<>();
        for (int i : power) {
            sum.merge(i, (long) i, Long::sum);
        }
        int n = sum.size();
        int[] powers = new int[n];
        int i = 0;
        for (Integer damage : sum.keySet()) {
            powers[i++] = damage;
        }
        Arrays.sort(powers);
        long[] mem = new long[n + 1];
        mem[n - 1] = sum.get(powers[n - 1]);
        for (int j = n - 2; j >= 0; j--) {
            int k = j;
            while (k < n && powers[k] - powers[j] <= 2) {
                k++;
            }
            mem[j] = Math.max(mem[j + 1], mem[k] + sum.get(powers[j]));
        }
        return mem[0];
    }

}

性能

3147.从魔法师身上吸取的最大能量

目标

在神秘的地牢中,n 个魔法师站成一排。每个魔法师都拥有一个属性,这个属性可以给你提供能量。有些魔法师可能会给你负能量,即从你身上吸取能量。

你被施加了一种诅咒,当你从魔法师 i 处吸收能量后,你将被立即传送到魔法师 (i + k) 处。这一过程将重复进行,直到你到达一个不存在 (i + k) 的魔法师为止。

换句话说,你将选择一个起点,然后以 k 为间隔跳跃,直到到达魔法师序列的末端,在过程中吸收所有的能量。

给定一个数组 energy 和一个整数k,返回你能获得的 最大 能量。

示例 1:

输入: energy = [5,2,-10,-5,1], k = 3
输出: 3
解释:可以从魔法师 1 开始,吸收能量 2 + 1 = 3。

示例 2:

输入: energy = [-2,-3,-1], k = 2
输出: -1
解释:可以从魔法师 2 开始,吸收能量 -1。

说明:

  • 1 <= energy.length <= 10^5
  • -1000 <= energy[i] <= 1000
  • 1 <= k <= energy.length - 1

思路

n 个魔法师站成一排,每个魔法师会吸取/提供能量并将你传送到第 i + k 个魔法师的位置,任选一个魔法师作为起点,求能够获得的最大能量。

求 k 个间隔为 k 的后缀和,取过程中产生的最大值。

代码


/**
 * @date 2025-10-10 9:03
 */
public class MaximumEnergy3147 {

    public int maximumEnergy(int[] energy, int k) {
        int n = energy.length;
        int res = Integer.MIN_VALUE;
        for (int i = 0; i < k; i++) {
            int sum = 0;
            for (int j = n - 1 - i; j >= 0; j -= k) {
                sum += energy[j];
                res = Math.max(res, sum);
            }
        }
        return res;
    }

}

性能

3494.酿造药水需要的最少总时间

目标

给你两个长度分别为 n 和 m 的整数数组 skill 和 mana 。

在一个实验室里,有 n 个巫师,他们必须按顺序酿造 m 个药水。每个药水的法力值为 mana[j],并且每个药水 必须 依次通过 所有 巫师处理,才能完成酿造。第 i 个巫师在第 j 个药水上处理需要的时间为 timeij = skill[i] * mana[j]。

由于酿造过程非常精细,药水在当前巫师完成工作后 必须 立即传递给下一个巫师并开始处理。这意味着时间必须保持 同步,确保每个巫师在药水到达时 马上 开始工作。

返回酿造所有药水所需的 最短 总时间。

示例 1:

输入: skill = [1,5,2,4], mana = [5,1,4,2]
输出: 110
解释:
药水编号 开始时间 巫师 0 完成时间 巫师 1 完成时间 巫师 2 完成时间 巫师 3 完成时间
0  0  5 30 40 60
1 52 53 58 60 64
2 54 58 78 86 102
3 86 88 98 102 110
举个例子,为什么巫师 0 不能在时间 t = 52 前开始处理第 1 个药水,假设巫师们在时间 t = 50 开始准备第 1 个药水。时间 t = 58 时,巫师 2 已经完成了第 1 个药水的处理,但巫师 3 直到时间 t = 60 仍在处理第 0 个药水,无法马上开始处理第 1个药水。

示例 2:

输入: skill = [1,1,1], mana = [1,1,1]
输出: 5
解释:
第 0 个药水的准备从时间 t = 0 开始,并在时间 t = 3 完成。
第 1 个药水的准备从时间 t = 1 开始,并在时间 t = 4 完成。
第 2 个药水的准备从时间 t = 2 开始,并在时间 t = 5 完成。

示例 3:

输入: skill = [1,2,3,4], mana = [1,2]
输出: 21

说明:

  • n == skill.length
  • m == mana.length
  • 1 <= n, m <= 5000
  • 1 <= mana[i], skill[i] <= 5000

思路

有 n 个巫师按顺序酿造 m 个药水,每个药水必须按顺序依次由 n 个巫师处理。第 i 个巫师处理第 j 个药水的时间为 skill[i] * mana[j]。求酿造所有药水所需的最短总时间。

定义 dp[i][j] 表示巫师 j - 1 制作完第 i - 1 瓶药的最短时间,状态转移方程为 dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]) + skill[j - 1] * mana[i - 1],即前面巫师的完成时间以及当前巫师上一轮的完成时间。当前药水完成之后需要倒序更新各个巫师的完成时间,因为状态转移方程只能保证最后一个巫师完成的时间是正确的,前面巫师的完成时间应该由最后一个完成时间来倒推。如果不更新会导致下一瓶药水的制作时间偏早。

// todo 学习其它题解

代码


/**
 * @date 2025-10-09 9:02
 */
public class MinTime3494 {

    public long minTime(int[] skill, int[] mana) {
        int n = skill.length;
        int m = mana.length;
        long[][] dp = new long[m + 1][n + 2];
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]) + skill[j - 1] * mana[i - 1];
            }
            for (int j = n - 1; j >= 1; j--) {
                dp[i][j] = dp[i][j + 1] - skill[j] * mana[i - 1];
            }
        }
        return dp[m][n];
    }

}

性能

2300.咒语和药水的成功对数

目标

给你两个正整数数组 spells 和 potions ,长度分别为 n 和 m ,其中 spells[i] 表示第 i 个咒语的能量强度,potions[j] 表示第 j 瓶药水的能量强度。

同时给你一个整数 success 。一个咒语和药水的能量强度 相乘 如果 大于等于 success ,那么它们视为一对 成功 的组合。

请你返回一个长度为 n 的整数数组 pairs,其中 pairs[i] 是能跟第 i 个咒语成功组合的 药水 数目。

示例 1:

输入:spells = [5,1,3], potions = [1,2,3,4,5], success = 7
输出:[4,0,3]
解释:
- 第 0 个咒语:5 * [1,2,3,4,5] = [5,10,15,20,25] 。总共 4 个成功组合。
- 第 1 个咒语:1 * [1,2,3,4,5] = [1,2,3,4,5] 。总共 0 个成功组合。
- 第 2 个咒语:3 * [1,2,3,4,5] = [3,6,9,12,15] 。总共 3 个成功组合。
所以返回 [4,0,3] 。

示例 2:

输入:spells = [3,1,2], potions = [8,5,8], success = 16
输出:[2,0,2]
解释:
- 第 0 个咒语:3 * [8,5,8] = [24,15,24] 。总共 2 个成功组合。
- 第 1 个咒语:1 * [8,5,8] = [8,5,8] 。总共 0 个成功组合。
- 第 2 个咒语:2 * [8,5,8] = [16,10,16] 。总共 2 个成功组合。
所以返回 [2,0,2] 。

说明:

  • n == spells.length
  • m == potions.length
  • 1 <= n, m <= 10^5
  • 1 <= spells[i], potions[i] <= 10^5
  • 1 <= success <= 10^10

思路

n 个咒语 和 m 瓶药水,对于每一个咒语,如果它的强度 spells[i] 与 药水能量强度 potions[j] 的乘积 大于等于 success 称为一个成功的组合,返回每个咒语的成功组合数。

将药水按强度排序,二分查找最后一个不成功组合的下标,成功的组合数为 m - (index + 1)

代码


/**
 * @date 2025-10-09 11:32
 */
public class SuccessfulPairs2300 {

    public int[] successfulPairs(int[] spells, int[] potions, long success) {
        Arrays.sort(potions);
        int n = potions.length;
        for (int i = 0; i < spells.length; i++) {
            int index = bs(potions, spells[i], success);
            spells[i] = n - 1 - index;
        }
        return spells;
    }

    public int bs(int[] potions, long spell, long success) {
        int r = potions.length - 1;
        int l = 0;
        int m = l + (r - l) / 2;
        while (l <= r) {
            if (spell * potions[m] >= success) {
                r = m - 1;
            } else {
                l = m + 1;
            }
            m = l + (r - l) / 2;
        }
        return r;
    }

}

性能

1518.换水问题

目标

超市正在促销,你可以用 numExchange 个空水瓶从超市兑换一瓶水。最开始,你一共购入了 numBottles 瓶水。

如果喝掉了水瓶中的水,那么水瓶就会变成空的。

给你两个整数 numBottles 和 numExchange ,返回你 最多 可以喝到多少瓶水。

示例 1:

输入:numBottles = 9, numExchange = 3
输出:13
解释:你可以用 3 个空瓶兑换 1 瓶水。
所以最多能喝到 9 + 3 + 1 = 13 瓶水。

示例 2:

输入:numBottles = 15, numExchange = 4
输出:19
解释:你可以用 4 个空瓶兑换 1 瓶水。
所以最多能喝到 15 + 3 + 1 = 19 瓶水。

提示:

  • 1 <= numBottles <= 100
  • 2 <= numExchange <= 100

思路

有 numBottles 瓶水,numExchange 个空瓶可以兑换一瓶水,问最多可以喝几瓶水。

依题意模拟即可。空瓶数 = 剩余未兑换空瓶 + 新兑换瓶数。

代码


/**
 * @date 2025-10-01 19:05
 */
public class NumWaterBottles1518 {

    public int numWaterBottles(int numBottles, int numExchange) {
        int res = numBottles;
        while (numBottles >= numExchange) {
            int change = numBottles / numExchange;
            res += change;
            numBottles = numBottles % numExchange + change;
        }
        return res;
    }
}

性能