2516.每种字符至少取K个

目标

给你一个由字符 'a'、'b'、'c' 组成的字符串 s 和一个非负整数 k 。每分钟,你可以选择取走 s 最左侧 还是 最右侧 的那个字符。

你必须取走每种字符 至少 k 个,返回需要的 最少 分钟数;如果无法取到,则返回 -1 。

示例 1:

输入:s = "aabaaaacaabc", k = 2
输出:8
解释:
从 s 的左侧取三个字符,现在共取到两个字符 'a' 、一个字符 'b' 。
从 s 的右侧取五个字符,现在共取到四个字符 'a' 、两个字符 'b' 和两个字符 'c' 。
共需要 3 + 5 = 8 分钟。
可以证明需要的最少分钟数是 8 。

示例 2:

输入:s = "a", k = 1
输出:-1
解释:无法取到一个字符 'b' 或者 'c',所以返回 -1 。

说明:

  • 1 <= s.length <= 10^5
  • s 仅由字母 'a'、'b'、'c' 组成
  • 0 <= k <= s.length

思路

有一个仅由 abc 组成的字符串,每一分钟可以选择访问两端(最左侧/最右侧)的未访问字符,求访问 abc 每种字符至少 k 次最少需要多少分钟。

可以将字符串拼接到末尾,问题转化为滑动窗口问题,求使窗口内包含k个 abc 的最小窗口长度。但这里有一个限制,窗口必须包含首尾交界点,就像有一个轴在窗口里面,窗口可以左右延展。

我们可以先向左侧滑动,找到满足条件的最左侧下标,然后枚举左端点向右滑动,直到左边界越过交界点,枚举的过程中延展右边界直到满足条件。

官网题解中使用了逆向思维,先求出字符串中 abc 的个数,窗口在中间滑动,窗口内的 abc 是不选的,使用总数减去窗口内的计数判断是否满足条件。

代码


/**
 * @date 2024-09-27 9:12
 */
public class TakeCharacters2516 {

    public int takeCharacters(String s, int k) {
        if (k == 0) {
            return 0;
        }
        int res = Integer.MAX_VALUE;
        int[] cnt = new int[3];
        char[] chars = s.toCharArray();
        int n = chars.length;
        int l = n - 1;
        while (l >= 0 && (cnt[0] < k || cnt[1] < k || cnt[2] < k)) {
            cnt[chars[l--] - 'a']++;
        }
        if (cnt[0] < k || cnt[1] < k || cnt[2] < k) {
            return -1;
        }
        l++;
        int r = n;
        int doubleN = 2 * chars.length;
        for (; l <= n; l++) {
            while (r < doubleN && (cnt[0] < k || cnt[1] < k || cnt[2] < k)) {
                cnt[chars[r++ % n] - 'a']++;
            }
            res = Math.min(res, r - l);
            if (l == n) {
                break;
            }
            cnt[chars[l] - 'a']--;
        }
        return res;
    }

}

性能

2398.预算内的最多机器人数目

目标

你有 n 个机器人,给你两个下标从 0 开始的整数数组 chargeTimes 和 runningCosts ,两者长度都为 n 。第 i 个机器人充电时间为 chargeTimes[i] 单位时间,花费 runningCosts[i] 单位时间运行。再给你一个整数 budget 。

运行 k 个机器人 总开销 是 max(chargeTimes) + k * sum(runningCosts) ,其中 max(chargeTimes) 是这 k 个机器人中最大充电时间,sum(runningCosts) 是这 k 个机器人的运行时间之和。

请你返回在 不超过 budget 的前提下,你 最多 可以 连续 运行的机器人数目为多少。

示例 1:

输入:chargeTimes = [3,6,1,3,4], runningCosts = [2,1,3,4,5], budget = 25
输出:3
解释:
可以在 budget 以内运行所有单个机器人或者连续运行 2 个机器人。
选择前 3 个机器人,可以得到答案最大值 3 。总开销是 max(3,6,1) + 3 * sum(2,1,3) = 6 + 3 * 6 = 24 ,小于 25 。
可以看出无法在 budget 以内连续运行超过 3 个机器人,所以我们返回 3 。

示例 2:

输入:chargeTimes = [11,12,19], runningCosts = [10,8,7], budget = 19
输出:0
解释:即使运行任何一个单个机器人,还是会超出 budget,所以我们返回 0 。

说明:

  • chargeTimes.length == runningCosts.length == n
  • 1 <= n <= 5 * 10^4
  • 1 <= chargeTimes[i], runningCosts[i] <= 10^5
  • 1 <= budget <= 10^15

思路

选择连续的 k 个机器人,使开销不超过预算 budget。其中机器人的开销等于其中 所选机器人的最长的充电时间 + k * 所选k个机器人花费之和

直接的想法是二分查找 k,然后使用滑动窗口记录最小的开销 min,如果 min < budget 增大 k,否则减小 k。时间复杂度为 O(nlogn)。

核心点在于滑动窗口的时候 max(chargeTimes) 如何更新。今天又解锁了新词条:单调队列

// todo

代码

性能

2555.两个线段获得的最多奖品

目标

在 X轴 上有一些奖品。给你一个整数数组 prizePositions ,它按照 非递减 顺序排列,其中 prizePositions[i] 是第 i 件奖品的位置。数轴上一个位置可能会有多件奖品。再给你一个整数 k 。

你可以选择两个端点为整数的线段。每个线段的长度都必须是 k 。你可以获得位置在任一线段上的所有奖品(包括线段的两个端点)。注意,两个线段可能会有相交。

  • 比方说 k = 2 ,你可以选择线段 [1, 3] 和 [2, 4] ,你可以获得满足 1 <= prizePositions[i] <= 3 或者 2 <= prizePositions[i] <= 4 的所有奖品 i 。

请你返回在选择两个最优线段的前提下,可以获得的 最多 奖品数目。

示例 1:

输入:prizePositions = [1,1,2,2,3,3,5], k = 2
输出:7
解释:这个例子中,你可以选择线段 [1, 3] 和 [3, 5] ,获得 7 个奖品。

示例 2:

输入:prizePositions = [1,2,3,4], k = 0
输出:2
解释:这个例子中,一个选择是选择线段 [3, 3] 和 [4, 4] ,获得 2 个奖品。

说明:

  • 1 <= prizePositions.length <= 10^5
  • 1 <= prizePositions[i] <= 10^9
  • 0 <= k <= 10^9
  • prizePositions 有序非递减。

思路

x轴的一些整数坐标上放有奖品,相同坐标点上可能有多个奖品。已知所有奖品所在坐标从小到大排序后的数组 prizePositions,问使用两个长度为k的线段最多能覆盖多少个奖品。线段可以相交,但相交区间内的奖品仅计数一次,线段端点处的奖品也计入总数。

最直观的想法是使用滑动窗口,固定区间长度,然后求能够覆盖的最多奖品数。但我们需要的是两个线段所能覆盖的最多奖品,能否记录下第一次求得的区间范围,然后在范围之外(两个线段尽量不相交才能覆盖更多奖品)在用相同的方法求次最多的奖品数。这看上去似乎可行,但具体写完之后会发现一些问题。

  • 如果存在多个最大区间如何处理?可以参考下图,k取3的情况,不同的选择直接影响另一线段的取值。可以用一个列表记录区间范围,然后分别对这些区间范围之外的区间执行同样的算法?时间复杂度为 O(m * l),m 为最大线段个数,l 为区间长度。

  • 就算上面的复杂度可以接受,两个线段获得最多奖品,一定要其中一个线段覆盖最多的奖品吗?这个是不必要的。参考下图,k取2的情况:

针对这道题,这种贪心策略是不行的,局部最优解并不一定是全局最优解。现在我们没有一个明确的目标,最优的线段该如何取?如果我们同时滑动两个窗口呢?暴力写法是先固定一个,然后滑动另一个。时间复杂度为 O(n^2),肯定超时。我们发现这里面有重复的子问题,定义 num[i] 表示以 prizePositions[i] 为起点长度为 k 的线段所能覆盖的奖品数。然后再用一个 max[i] 表示起点大于等于 prizePositions[i] 长度为 k 的线段所能覆盖的最大奖品数。这样我们就能以 O(1) 的复杂度取到固定一个窗口之后,另一个窗口的最大值。枚举固定窗口求出最大值即可。

写完之后发现,保存 num[i] 是不必要的,它只用来更新当前的 max[i]

滑动窗口有两种写法,枚举左边界,循环内部直接到达可能的最右边界。另一种写法是枚举右边界,如果条件不满足,更新左边界直到满足条件。注意确保不要越界。

官网题解提供了二分法的解法,确实遇到有序数组就要想到二分法,但是这里二分找什么呢?大概是二分找所能覆盖的左边界,然后还是动态规划求不超过左边界的另一线段所能覆盖的最大值。官方题解这个解法的java版本好像是其它题目的,给错答案了。

代码


/**
 * @date 2024-09-11 8:57
 */
public class MaximizeWin2555 {

    public int maximizeWin_v1(int[] prizePositions, int k) {
        int n = prizePositions.length;
        if (k * 2 + 1 >= prizePositions[n - 1] - prizePositions[0]) {
            return n;
        }
        int res = 1;
        int[] max = new int[n + 1];
        int l = n - 1;
        for (int r = n - 1; r >= 0; r--) {
            while (l >= 0 && prizePositions[r] - prizePositions[l] <= k) {
                max[l] = Math.max(r - l + 1, max[l + 1]);
                res = Math.max(res, r - l + 1 + max[r + 1]);
                l--;
            }
        }
        return res;
    }

}

性能

2024.考试的最大困扰度

目标

一位老师正在出一场由 n 道判断题构成的考试,每道题的答案为 true (用 'T' 表示)或者 false (用 'F' 表示)。老师想增加学生对自己做出答案的不确定性,方法是 最大化 有 连续相同 结果的题数。(也就是连续出现 true 或者连续出现 false)。

给你一个字符串 answerKey ,其中 answerKey[i] 是第 i 个问题的正确结果。除此以外,还给你一个整数 k ,表示你能进行以下操作的最多次数:

  • 每次操作中,将问题的正确答案改为 'T' 或者 'F' (也就是将 answerKey[i] 改为 'T' 或者 'F' )。

请你返回在不超过 k 次操作的情况下,最大 连续 'T' 或者 'F' 的数目。

示例 1:

输入:answerKey = "TTFF", k = 2
输出:4
解释:我们可以将两个 'F' 都变为 'T' ,得到 answerKey = "TTTT" 。
总共有四个连续的 'T' 。

示例 2:

输入:answerKey = "TFFT", k = 1
输出:3
解释:我们可以将最前面的 'T' 换成 'F' ,得到 answerKey = "FFFT" 。
或者,我们可以将第二个 'T' 换成 'F' ,得到 answerKey = "TFFF" 。
两种情况下,都有三个连续的 'F' 。

示例 3:

输入:answerKey = "TTFTTFTT", k = 1
输出:5
解释:我们可以将第一个 'F' 换成 'T' ,得到 answerKey = "TTTTTFTT" 。
或者我们可以将第二个 'F' 换成 'T' ,得到 answerKey = "TTFTTTTT" 。
两种情况下,都有五个连续的 'T' 。

说明:

  • n == answerKey.length
  • 1 <= n <= 5 * 10^4
  • answerKey[i] 要么是 'T' ,要么是 'F'
  • 1 <= k <= n

思路

有一个字符串 answerKeyTF 组成, 允许我们执行 k 次操作,每次操作可以将字符串中的 T 改为 F 或者 F 改为 T。问 TF 可能的最大连续个数。

这道题没有做出来,想着使用动态规划去做,但是没有找到合适的状态定义。比如 dp[i][j][k] 表示 [0,i]TF 结尾剩余操作次数 k 时的最大连续个数。2 x 10^8 的存储空间肯定不行。

题解说是滑动窗口、或者 前缀和 + 二分查找,也有使用动态规划的。

这道题的难点在于想明白这 k 次操作必须统一,要么全部从 T 改为 F,要么全部从 F 改为 T,才能使连续个数最大。因为如果 T 的连续个数最多,并且存在将 T 改为 F 的操作,那么我们总可以撤回该操作,并将一个 F 改为 T(如果存在的话,如果不存在说明全是T,撤销操作也会加一) 使得连续个数至少加一。

网友题解中的动态规划是这样定义的 dp[i] 表示 [0,i] 中以 answerKey[i] 结尾的连续后缀个数。这里的前提就是遇到不连续的统一从 T 改为 F 或者 从 F 改为 T 使之连续,如果超过了可操作的次数,需要撤回最早的操作,使得当前后缀连续。后缀连续个数可以用当前下标减去最早进行操作的下标计算得到(使用链表保存操作的下标,获取链表head记录的下标后将其删,再将当前下标加入链表末尾)。在计算dp过程中记录其最大值即为最大连续个数。如果对这个动态规划进行存储优化,那就是滑动窗口。

寻找一个窗口,使得窗口内的 T 或者 F 个数小于等于 k,并且使 F 或者 T 的个数最大。滑动窗口的套路一般是枚举右边界,如果条件不满足,更新左边界直到条件满足。

二分的做法本质也是滑动窗口,枚举左边界,二分查找能够到达的最远右边界。

代码


/**
 * @date 2024-09-02 16:55
 */
public class MaxConsecutiveAnswers2024 {

    /**
     * 前缀和 + 二分查找
     * 其实本质也是滑动窗口,枚举左边界,二分查找最远的右边界
     * O(nlogn) 62ms
     */
    public int maxConsecutiveAnswers_v1(String answerKey, int k) {
        int n = answerKey.length();
        int[] prefix = new int[n + 1];
        for (int i = 1; i <= n; i++) {
            if (answerKey.charAt(i - 1) == 'T') {
                prefix[i] = prefix[i - 1] + 1;
            } else {
                prefix[i] = prefix[i - 1];
            }
        }
        int res = 0;
        for (int i = 1; i <= n; i++) {
            int l = i, r = n;
            int mid = l + (r - l) / 2;
            while (l <= r) {
                int num = mid - i + 1;
                int cnt = prefix[mid] - prefix[i - 1];
                if (num - cnt <= k || cnt <= k) {
                    l = mid + 1;
                } else {
                    r = mid - 1;
                }
                mid = l + (r - l) / 2;
            }
            res = Math.max(res, r - i + 1);
        }
        return res;
    }

    /**
     * 滑动窗口 O(n)
     * 21ms
     */
    public int maxConsecutiveAnswers(String answerKey, int k) {
        return Math.max(process(answerKey, k, 'T'), process(answerKey, k, 'F'));
    }

    public int process(String answerKey, int k, char turnTo) {
        int n = answerKey.length();
        int res = 0;
        List<Integer> optsIndex = new LinkedList<>();
        int cnt = 0;
        for (int i = 0; i < n; i++) {
            if (answerKey.charAt(i) != turnTo) {
                if (k > 0) {
                    k--;
                    optsIndex.add(i);
                    cnt++;
                } else {
                    cnt = i - optsIndex.remove(0);
                    optsIndex.add(i);
                }
            } else {
                cnt++;
            }
            res = Math.max(res, cnt);
        }
        return res;
    }

}

性能

3134.找出唯一性数组的中位数

目标

给你一个整数数组 nums 。数组 nums 的 唯一性数组 是一个按元素从小到大排序的数组,包含了 nums 的所有 非空子数组中 不同元素的个数。

换句话说,这是由所有 0 <= i <= j < nums.length 的 distinct(nums[i..j]) 组成的递增数组。

其中,distinct(nums[i..j]) 表示从下标 i 到下标 j 的子数组中不同元素的数量。

返回 nums 唯一性数组 的 中位数 。

注意,数组的 中位数 定义为有序数组的中间元素。如果有两个中间元素,则取值较小的那个。

示例 1:

输入:nums = [1,2,3]
输出:1
解释:
nums 的唯一性数组为 [distinct(nums[0..0]), distinct(nums[1..1]), distinct(nums[2..2]), distinct(nums[0..1]), distinct(nums[1..2]), distinct(nums[0..2])],即 [1, 1, 1, 2, 2, 3] 。唯一性数组的中位数为 1 ,因此答案是 1 。

示例 2:

输入:nums = [3,4,3,4,5]
输出:2
解释:
nums 的唯一性数组为 [1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3] 。唯一性数组的中位数为 2 ,因此答案是 2 。

示例 3:

输入:nums = [4,3,5,4]
输出:2
解释:
nums 的唯一性数组为 [1, 1, 1, 1, 2, 2, 2, 3, 3, 3] 。唯一性数组的中位数为 2 ,因此答案是 2 。

说明:

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

思路

定义数组的唯一性数组为其所有 子数组 中不同元素个数从小到大排序,求唯一性数组的中位数。

长度为 n 的子数组的个数为 n(n+1)/2,唯一性数组的中位数下标为 n(n+1)/4 - 1 是第 (n(n+1)/2 + 1)/2 个元素。

问题的关键在于,如何快速判断数组中不同元素的个数。我们想要这样一个hash表,可以根据 start end 动态调整其中的元素。

一般来说枚举子数组使用的是双层循环,外层枚举起点,内层从起点开始枚举终点直到结尾,当然也可以外层枚举终点,内层枚举0到终点作为起点,时间复杂度为 O(n^2)。这里的问题在于如何保存区间与对应不重复元素个数的对应关系,以及如何计算不重复元素个数。本来 O(n^2) 就会超时,如果针对每个区间再循环判断,就更不行了。这里其实可以模拟变长的滑动窗口,通过修改窗口中加入与移除元素在map中对应的计数,如果计数为0则删除,这样map里的元素个数即为当前窗口内不重复元素个数。但是并没有保存这个状态(区间对应的不同元素个数)。我们可以将 start, end 压缩到一个long型数字中,倒是也可以记录。假如我们有了这个对应关系,我们还需要将它排序然后取中位数。

看了题解使用的是二分+滑动窗口,确实比较绕,我也没有仔细想清楚,这里面有几个关键点:

  • 唯一性数组中元素的取值范围是 1 ~ n,元素递增的步长为1,如果某个子数组比之前的子数组多了2个不同的元素,那么总是可以去掉其中一个使得子数组仅多1个不同元素。
  • 思考 唯一性元素的个数 小于等于 m 的子数组有多少个?找到唯一性元素个数第一次覆盖 (n(n+1)/2 + 1)/2 的 m 就是要找的答案。
  • 假设我们已经知道 m 对应的 cnt,只需要找到第一个大于等于 (n(n+1)/2 + 1)/2 的cnt即可,可以使用二分查找。
  • 问题转化为 计算唯一性元素的个数 小于等于 m 的子数组个数。使用滑动窗口。

而这里需要计算的是子数组中不同元素的个数,

// todo

代码

性能

3086.拾起K个1需要的最少行动次数

目标

给你一个下标从 0 开始的二进制数组 nums,其长度为 n ;另给你一个 正整数 k 以及一个 非负整数 maxChanges 。

Alice 在玩一个游戏,游戏的目标是让 Alice 使用 最少 数量的 行动 次数从 nums 中拾起 k 个 1 。游戏开始时,Alice 可以选择数组 [0, n - 1] 范围内的任何索引 aliceIndex 站立。如果 nums[aliceIndex] == 1 ,Alice 会拾起一个 1 ,并且 nums[aliceIndex] 变成0(这 不算 作一次行动)。之后,Alice 可以执行 任意数量 的 行动(包括零次),在每次行动中 Alice 必须 恰好 执行以下动作之一:

  • 选择任意一个下标 j != aliceIndex 且满足 nums[j] == 0 ,然后将 nums[j] 设置为 1 。这个动作最多可以执行 maxChanges 次。
  • 选择任意两个相邻的下标 x 和 y(|x - y| == 1)且满足 nums[x] == 1, nums[y] == 0 ,然后交换它们的值(将 nums[y] = 1 和 nums[x] = 0)。如果 y == aliceIndex,在这次行动后 Alice 拾起一个 1 ,并且 nums[y] 变成 0 。

返回 Alice 拾起 恰好 k 个 1 所需的 最少 行动次数。

示例 1:

输入:nums = [1,1,0,0,0,1,1,0,0,1], k = 3, maxChanges = 1
输出:3
解释:如果游戏开始时 Alice 在 aliceIndex == 1 的位置上,按照以下步骤执行每个动作,他可以利用 3 次行动拾取 3 个 1 :

游戏开始时 Alice 拾取了一个 1 ,nums[1] 变成了 0。此时 nums 变为 [1,0,0,0,0,1,1,0,0,1] 。
选择 j == 2 并执行第一种类型的动作。nums 变为 [1,0,1,0,0,1,1,0,0,1]
选择 x == 2 和 y == 1 ,并执行第二种类型的动作。nums 变为 [1,1,0,0,0,1,1,0,0,1] 。由于 y == aliceIndex,Alice 拾取了一个 1 ,nums 变为  [1,0,0,0,0,1,1,0,0,1] 。
选择 x == 0 和 y == 1 ,并执行第二种类型的动作。nums 变为 [0,1,0,0,0,1,1,0,0,1] 。由于 y == aliceIndex,Alice 拾取了一个 1 ,nums 变为  [0,0,0,0,0,1,1,0,0,1] 。
请注意,Alice 也可能执行其他的 3 次行动序列达成拾取 3 个 1 。

示例 2:

输入:nums = [0,0,0,0], k = 2, maxChanges = 3
输出:4
解释:如果游戏开始时 Alice 在 aliceIndex == 0 的位置上,按照以下步骤执行每个动作,他可以利用 4 次行动拾取 2 个 1 :

选择 j == 1 并执行第一种类型的动作。nums 变为 [0,1,0,0] 。
选择 x == 1 和 y == 0 ,并执行第二种类型的动作。nums 变为 [1,0,0,0] 。由于 y == aliceIndex,Alice 拾起了一个 1 ,nums 变为 [0,0,0,0] 。
再次选择 j == 1 并执行第一种类型的动作。nums 变为 [0,1,0,0] 。
再次选择 x == 1 和 y == 0 ,并执行第二种类型的动作。nums 变为 [1,0,0,0] 。由于y == aliceIndex,Alice 拾起了一个 1 ,nums 变为 [0,0,0,0] 。

说明:

  • 2 <= n <= 10^5
  • 0 <= nums[i] <= 1
  • 1 <= k <= 10^5
  • 0 <= maxChanges <= 10^5
  • maxChanges + sum(nums) >= k

思路

有一个二进制(元素不是0就是1)数组nums,选择一个固定的位置aliceIndex,如果该位置元素值为1,则可以拾起并将元素置0。接下来可以采取行动:

  1. 任选一个不等于aliceIndex且值为0的元素置1
  2. 将任意相邻且元素值不等的元素交换,如果其中一个位置是aliceIndex,且交换后的值为1,则可以拾起这个1并将元素置0

问恰好拾起k个1所需最小行动次数。

很明显行动1要选与aliceIndex相邻的,这样才可以用行动2将1拾起。

我们首先面对的问题是aliceIndex怎么选,要拾取1就需要将1都通过行动2移动到aliceIndex周围,如果拾取一个1的行动次数大于2的话就需要考虑使用行动1直接在aliceIndex周围设置1再拾取。

// todo

代码

性能

2831.找出最长等值子数组

目标

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

如果子数组中所有元素都相等,则认为子数组是一个 等值子数组 。注意,空数组是 等值子数组 。

从 nums 中删除最多 k 个元素后,返回可能的最长等值子数组的长度。

子数组 是数组中一个连续且可能为空的元素序列。

示例 1:

输入:nums = [1,3,2,3,1,3], k = 3
输出:3
解释:最优的方案是删除下标 2 和下标 4 的元素。
删除后,nums 等于 [1, 3, 3, 3] 。
最长等值子数组从 i = 1 开始到 j = 3 结束,长度等于 3 。
可以证明无法创建更长的等值子数组。

示例 2:

输入:nums = [1,1,2,2,1,1], k = 2
输出:4
解释:最优的方案是删除下标 2 和下标 3 的元素。 
删除后,nums 等于 [1, 1, 1, 1] 。 
数组自身就是等值子数组,长度等于 4 。 
可以证明无法创建更长的等值子数组。

说明:

  • 1 <= nums.length <= 10^5
  • 1 <= nums[i] <= nums.length
  • 0 <= k <= nums.length

思路

给定一个数组 nums 和一个正整数k,最多可以删除数组中k个元素,问数组中最长的等值子数组有多长,所谓等值子数组就是数组中的值完全相同。

直接的想法是记录每个相同元素的下标index,然后计算它们之间的距离 gap,使用滑动窗口来计算最多可以消除的 gap 数量。

注意不能优先消除序列中距离小的 gap,也就是说只能按照顺序去消除,否则可能会截断等值子数组。

刚开始使用哈希表记录相同元素的下标序列,结果提示超出内存限制,后来改用数组解决了问题。

Map在不断添加元素时可能需要进行扩容,每次扩容都需要重新分配更大的内存空间。 但我直接指定初始大小还是超出限制。

原来用了两个哈希表,indexMapgapMap,后来 indexMap 改成了 List[]gapMap 则直接在循环中计算并添加到 ArrayList 中。

注意这不是一个固定大小的窗口,如果按照固定大小的窗口去实现还是会超时。

代码

/**
 * @date 2024-05-23 0:11
 */
public class LongestEqualSubarray2831 {

    public int longestEqualSubarray_v1(List<Integer> nums, int k) {
        if (nums.size() == 0) {
            return 0;
        }
        int res = 0;
        List<Integer>[] indexMap = new List[100001];
        for (int i = 0; i < nums.size(); i++) {
            int val = nums.get(i);
            if (indexMap[val] == null) {
                indexMap[val] = new ArrayList<>();
            }
            indexMap[val].add(i);
        }
        for (List<Integer> index : indexMap) {
            if (index == null) {
                continue;
            }
            int i = 0;
            ArrayList<Integer> gaps = new ArrayList<>();
            for (int j = 1; j < index.size(); j++) {
                gaps.add(index.get(j) - index.get(i++) - 1);
            }
            int cnt = k;
            int gapNums = 0;
            int left = 0;
            int right = 0;
            while (right < gaps.size()) {
                while (cnt >= 0 && right < gaps.size()) {
                    cnt -= gaps.get(right);
                    if (cnt >= 0) {
                        right++;
                    }
                }
                gapNums = Math.max(gapNums, right - left);
                while (left < gaps.size() && cnt < 0) {
                    cnt += gaps.get(left);
                    left++;
                }
                right++;
            }

            res = Math.max(res, gapNums + 1);
        }
        return res;
    }
}

性能

又是勉强通过。//todo 性能分析与优化

1052.爱生气的书店老板

目标

有一个书店老板,他的书店开了 n 分钟。每分钟都有一些顾客进入这家商店。给定一个长度为 n 的整数数组 customers ,其中 customers[i] 是在第 i 分钟开始时进入商店的顾客数量,所有这些顾客在第 i 分钟结束后离开。

在某些时候,书店老板会生气。 如果书店老板在第 i 分钟生气,那么 grumpy[i] = 1,否则 grumpy[i] = 0。

当书店老板生气时,那一分钟的顾客就会不满意,若老板不生气则顾客是满意的。

书店老板知道一个秘密技巧,能抑制自己的情绪,可以让自己连续 minutes 分钟不生气,但却只能使用一次。

请你返回 这一天营业下来,最多有多少客户能够感到满意 。

示例 1:

输入:customers = [1,0,1,2,1,1,7,5], grumpy = [0,1,0,1,0,1,0,1], minutes = 3
输出:16
解释:书店老板在最后 3 分钟保持冷静。
感到满意的最大客户数量 = 1 + 1 + 1 + 1 + 7 + 5 = 16.

示例 2:

输入:customers = [1], grumpy = [0], minutes = 1
输出:1

说明:

  • n == customers.length == grumpy.length
  • 1 <= minutes <= n <= 2 * 10^4
  • 0 <= customers[i] <= 1000
  • grumpy[i] == 0 or 1

思路

这道题要我们求满意顾客的最大值,老板会在指定的分钟生气,生气时,当前进入的顾客就会不满意,老板可以有一次忍耐的机会在一段时间内不生气。

刚开始想复杂了,以为顾客不是马上离开,而是在i分钟后离开。其实是第i分钟结束的时候就离开了。

因此,我们只需要计算老板忍耐的时间内,原本生气影响的顾客最大值,然后再加上不生气时的顾客总数即可。

这就是一个滑动窗口问题,窗口大小为忍耐时间,记录窗口内的不满意顾客的最大值。

这里面可以优化的点是使用乘法来代替条件判断,这样可以避免分支预测失败导致额外的性能损失。

代码

/**
 * @date 2024-04-23 8:40
 */
public class MaxSatisfied1052 {

    public int maxSatisfied_v1(int[] customers, int[] grumpy, int minutes) {
        int n = customers.length;
        int total = 0;
        for (int i = 0; i < n; i++) {
            total += customers[i] * (1 - grumpy[i]);
        }
        int unsatisfiedInWindow = 0;
        for (int i = 0; i < minutes; i++) {
            unsatisfiedInWindow += customers[i] * grumpy[i];
        }
        int max = unsatisfiedInWindow;
        for (int i = minutes; i < n; i++) {
            unsatisfiedInWindow = unsatisfiedInWindow
                    - customers[i - minutes] * grumpy[i - minutes]
                    + customers[i] * grumpy[i];
            max = Math.max(max, unsatisfiedInWindow);
        }
        return total + max;
    }
}

性能

2009.使数组连续的最少操作数

目标

给你一个整数数组 nums 。每一次操作中,你可以将 nums 中 任意 一个元素替换成 任意 整数。

如果 nums 满足以下条件,那么它是 连续的 :

  • nums 中所有元素都是 互不相同 的。
  • nums 中 最大 元素与 最小 元素的差等于 nums.length - 1 。

比方说,nums = [4, 2, 5, 3] 是 连续的 ,但是 nums = [1, 2, 3, 5, 6] 不是连续的 。

请你返回使 nums 连续 的 最少 操作次数。

示例 1:

输入:nums = [4,2,5,3]
输出:0
解释:nums 已经是连续的了。

示例 2:

输入:nums = [1,2,3,5,6]
输出:1
解释:一个可能的解是将最后一个元素变为 4 。
结果数组为 [1,2,3,5,4] ,是连续数组。

示例 3:

输入:nums = [1,10,100,1000]
输出:3
解释:一个可能的解是:
- 将第二个元素变为 2 。
- 将第三个元素变为 3 。
- 将第四个元素变为 4 。
结果数组为 [1,2,3,4] ,是连续数组。

说明:

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

思路

这道题让我们求将一个数组变为连续数组所需的最小操作,所谓连续数组指数组中元素各不相同,且max - min = nums.length - 1,也就是说如果将数组从小到大排序,会得到一个公差为1的数列。

我们首先将数组排序,然后初始的数组中会有不连续的边界,也会有相等的元素。如何处理才能使数列连续,并且保证操作数最小。

我陷入了这个困境,尝试了好几个小时,试图处理各种特殊情况。比如我会先找到最大的连续子序列并且记录边界的差值,然后以它为中心分别从后面、前面获取元素来填充这个边界。并且还要计数相同元素来填充边界,这里填充的时机也会对最小的操作有影响。有时需要先填充,有时则需要最后。总之,并没有一个统一的,明确的算法来满足所有情况。

看了官网的解答,应该使用滑动窗口思想。之前一直有看到这个题型分类,没想到这就是所谓的滑动窗口。

当我遇到困难的时候会有一种执念,不愿意去看题解,想知道沿着自己的思路下去为什么不行,到最后真的有点沮丧。这样我想起了迪杰斯特拉的一个采访。

An interview with Edsger W. Dijkstra

The computer science luminary, in one of his last interviews before his death in 2002, reflects on a programmer’s life.

There’s a curious story behind your “shortest path” algorithm.

In 1956 I did two important things, I got my degree and we had the festive opening of the ARMAC(Automatic Calculator Mathematical Centre, 自动计算器数学中心).

We had to have a demonstration. Now the ARRA(Automatic Relay Calculator Amsterdam, 自动继电器计算器 阿姆斯特丹), a few years earlier, had been so unreliable that the only safe demonstration we dared to give was the generation of random numbers, but for the more reliable ARMAC I could try something more ambitious. For a demonstration for noncomputing people you have to have a problem statement that non-mathematicians can understand; they even have to understand the answer. So I designed a program that would find the shortest route between two cities in the Netherlands, using a somewhat reduced roadmap of the Netherlands, on which I had selected 64 cities (so that in the coding six bits would suffice to identify a city).

What’s the shortest way to travel from Rotterdam to Groningen? It is the algorithm for the shortest path, which I designed in about 20 minutes. One morning I was shopping in Amsterdam with my young fiancée, and tired, we sat down on the café terrace to drink a cup of coffee and I was just thinking about whether I could do this, and I then designed the algorithm for the shortest path. As I said, it was a 20-minute invention. In fact, it was published in 1959, three years later. The publication is still quite nice. One of the reasons that it is so nice was that I designed it without pencil and paper. Without pencil and paper you are almost forced to avoid all avoidable complexities.

Eventually that algorithm became, to my great amazement, one of the cornerstones of my fame. I found it in the early 1960s in a German book on management science—“Das Dijkstra’sche Verfahren” [“Dijkstra’s procedure”].

Suddenly, there was a method named after me. And it jumped again recently because it is extensively used in all travel planners. If, these days, you want to go from here to there and you have a car with a GPS and a screen, it can give you the shortest way.

1956年,迪杰斯特拉与未婚妻在阿姆斯特丹逛商场累了,坐在咖啡厅的露台上喝咖啡,他想到了一个问题"从鹿特丹到格罗宁根最短的旅行路线是什么?",并且在20分钟内构思了最短路径算法。该算法在三年后被发表在一篇三页的论文中,题为 A note on two problems in connexion with graphs。Dijkstra 因其对开发结构化编程语言的基本贡献而获得 1972 年图灵奖,但最短路径算法仍然是他最著名的工作。

During an interview in 2001, Edsger Wybe Dijkstra revealed that he designed the algorithm in just 20 minutes while shopping in Amsterdam with his fiancée in 1956.

He was inspired by the question, "What's the shortest way to travel from Rotterdam to Groningen?"

He designed it without pencil and paper. He even mentioned that the advantage of designing without pencil and paper is that you are almost forced to avoid all avoidable complexities.

The algorithm was published three years later in a three-page article titled "A note on two problems in connexion with graphs".

https://twitter.com/LinuxHandbook/status/1754392486657798198

也许这就是天赋吧。我只是个普通人,不可能解决所有问题,没有什么可沮丧的。要努力站在巨人的肩膀上,知识并不是免费的,需要投入时间。所以没有必要死磕一个问题,快速高效的爬上去,然后当面对新的未知的问题时,再花费精力研究才对。

思考的意义可能就是让自己对困难、问题有了更深的体会,看解答要弄明白问题是如何解决的。

就以这个题为例,困难点在于如何使操作最小。虽然知道长度是固定的,但是并没有意识到如何遍历所有可能的操作方法并取其最小。陷入了具体的、面向过程的、针对案例而设计的算法之中。当我先入为主的认为,应该以最大连续子数组为中心不变,从两端借数填充这个错误的指导思想进行实现时,就注定得不到正确答案。

之所以开始会认为应该保持最大的连续子数组不变,是因为观察了几个测试案例,想当然的认为,既然最后要变成连续数组,那么保持最大的不变,操作数会最小,但其实并非如此。并且,先从后面填还是前面也是没有道理的,都是根据具体的测试案例写的。

也许其它时间,灵光一闪也会想到滑动窗口算法也说不定。如果之前接触过这个概念,那这个题还是挺简单的。

代码

// todo: 过几天自己实现一遍

性能

// todo