3261.统计满足K约束的子字符串数量II

目标

给你一个 二进制 字符串 s 和一个整数 k。

另给你一个二维整数数组 queries ,其中 queries[i] = [li, ri] 。

如果一个 二进制字符串 满足以下任一条件,则认为该字符串满足 k 约束:

  • 字符串中 0 的数量最多为 k。
  • 字符串中 1 的数量最多为 k。

返回一个整数数组 answer ,其中 answer[i] 表示 s[li..ri] 中满足 k 约束 的 子字符串 的数量。

示例 1:

输入:s = "0001111", k = 2, queries = [[0,6]]
输出:[26]
解释:
对于查询 [0, 6], s[0..6] = "0001111" 的所有子字符串中,除 s[0..5] = "000111" 和 s[0..6] = "0001111" 外,其余子字符串都满足 k 约束。

示例 2:

输入:s = "010101", k = 1, queries = [[0,5],[1,4],[2,3]]
输出:[15,9,3]
解释:
s 的所有子字符串中,长度大于 3 的子字符串都不满足 k 约束。

说明:

  • 1 <= s.length <= 10^5
  • s[i] 是 '0' 或 '1'
  • 1 <= k <= s.length
  • 1 <= queries.length <= 10^5
  • queries[i] == [li, ri]
  • 0 <= li <= ri < s.length
  • 所有查询互不相同

思路

定义二进制字符串满足 k 约束的条件是字符串中 0 的个数 或者 1 的个数都不超过 k。求给定字符串满足 k 约束的子字符串数量。子字符串 是字符串中 连续非空 字符序列。

这道题与昨天的 3258.统计满足K约束的子字符串数量I 相比多了一个查询数组,并且字符串的长度也来到了 10^5,返回结果是 long[],暴力枚举会超时。

滑动窗口的时间复杂度为 O(n),不可能对每一次查询都重新滑动一遍。显然需要在滑动的过程中记录下查询的结果。每次滑动我们可以得到一个区间 [l, r],这个区间的所有子数组是合法的。

使用一维数组记录这个区间,使用下标与值的映射,我们有两种选择:

  • 记录的左端点的最大右端点,即 left[l] = r
  • 记录的右端点的最小左端点,即 right[r] = l

对于查询区间 [ql, qr],它与我们已知的合法区间存在两种关系,被包含或者部分相交。

  • 如果是被包含的关系,那么查询区间的所有子数组均合法,子数组个数为 (qr - ql + 1) * (qr - ql + 2) / 2
  • 如果是相交的关系,说明 ql < r[ql, r] 的所有子数组是合法的,剩下的 [r + 1, qr] 的合法子数组如何求?可以在滑动过程中记录以每个元素为终点的合法子数组个数,以前缀和的形式保存。

这里区间的划分与结果集的构成是非常有讲究的。前缀和记录的是以元素为 终点 的合法子数组,如果我们在滑动的过程中根据查询区间终点匹配当前元素,即 qr == r,那么可能的情况为 ql >= l 查询区间被完全包含。如果 ql < l 则查询区间与合法区间相交。如果这时使用前缀和计算 [ql, l],使用公式计算 [l, r] 就错了。因为后面区间使用公式计算的子数组并不包括以前面区间内的元素为起点的子数组,并且前缀和记录的子数组的起点可能在查询的左边界之外。而反过来前面使用公式计算,后面使用前缀和计算,被减去的那部分子数组个数中就包含了以 前面区间所有元素 为终点的子数组,也就是前面使用公式计算的子数组个数。我们不用担心后面通过前缀和计算的子数组的起点超出左边界,如果超出的话,一定是被包含的关系。

核心点在于想清楚这两部分集合的关系, [i, j] 的所有子数组包括了以 b ∈ [i, j] 为终点,a ∈ [i, b] 为起点的子数组。而使用前缀和相减计算的是所有以 c ∈ [m, n] 为终点的合法子数组,起点可以不在该区间,但是不会超出 ql

代码


/**
 * @date 2024-11-13 0:25
 */
public class CountKConstraintSubstrings3261 {

    public long[] countKConstraintSubstrings_v1(String s, int k, int[][] queries) {
        int n = s.length();
        char[] chars = s.toCharArray();
        int[] cnt = new int[2];
        long[] res = new long[queries.length];
        long[] prefix = new long[n + 1];
        int[] right = new int[n];
        Arrays.fill(right, n);
        int l = 0;
        for (int i = 0; i < n; i++) {
            cnt[chars[i] - '0']++;
            while (l <= i && cnt[0] > k && cnt[1] > k) {
                right[l] = i;
                cnt[chars[l++] - '0']--;
            }
            prefix[i + 1] += prefix[i] + i - l + 1;
        }
        for (int i = 0; i < queries.length; i++) {
            int ql = queries[i][0];
            int qr = queries[i][1];
            int r = Math.min(right[ql], qr + 1);
            res[i] = (r - ql + 1L) * (r - ql) / 2 + prefix[qr + 1] - prefix[r];

        }
        return res;
    }

}

性能

3258.统计满足K约束的子字符串数量I

目标

给你一个 二进制 字符串 s 和一个整数 k。

如果一个 二进制字符串 满足以下任一条件,则认为该字符串满足 k 约束:

  • 字符串中 0 的数量最多为 k。
  • 字符串中 1 的数量最多为 k。

返回一个整数,表示 s 的所有满足 k 约束 的 子字符串 的数量。

示例 1:

输入:s = "10101", k = 1
输出:12
解释:
s 的所有子字符串中,除了 "1010"、"10101" 和 "0101" 外,其余子字符串都满足 k 约束。

示例 2:

输入:s = "1010101", k = 2
输出:25
解释:
s 的所有子字符串中,除了长度大于 5 的子字符串外,其余子字符串都满足 k 约束。

示例 3:

输入:s = "11111", k = 1
输出:15
解释:
s 的所有子字符串都满足 k 约束。

说明:

  • 1 <= s.length <= 50
  • 1 <= k <= s.length
  • s[i] 是 '0' 或 '1'。

思路

定义二进制字符串满足 k 约束的条件是字符串中 0 的个数 或者 1 的个数都不超过 k。求给定字符串满足 k 约束的子字符串数量。子字符串 是字符串中 连续非空 字符序列。

对于非定长滑动窗口,我们可以枚举左端点,扩展右端点;也可以枚举右端点,收缩左端点。对于这个题,我们需要累加的是子数组个数,即以枚举的元素为起点或终点的子数组个数,实际就是区间内元素个数。如果枚举左端点,右端点指向的是不满足条件的点,需要复杂的判断,非常容易出错。因此选择枚举右端点,收缩左端点。

代码


/**
 * @date 2024-11-12 9:19
 */
public class CountKConstraintSubstrings3258 {

    public int countKConstraintSubstrings(String s, int k) {
        int n = s.length();
        char[] chars = s.toCharArray();
        int[] cnt = new int[2];
        int res = 0, l = 0;
        for (int i = 0; i < n; i++) {
            cnt[chars[i] - '0']++;
            while (l <= i && cnt[0] > k && cnt[1] > k) {
                cnt[chars[l++] - '0']--;
            }
            res += i - l + 1;
        }
        return res;
    }

}

性能

3254.长度为K的子数组的能量值I

目标

给你一个长度为 n 的整数数组 nums 和一个正整数 k 。

一个数组的 能量值 定义为:

  • 如果 所有 元素都是依次 连续 且 上升 的,那么能量值为 最大 的元素。
  • 否则为 -1 。

你需要求出 nums 中所有长度为 k 的 子数组 的能量值。

请你返回一个长度为 n - k + 1 的整数数组 results ,其中 results[i] 是子数组 nums[i..(i + k - 1)] 的能量值。

示例 1:

输入:nums = [1,2,3,4,3,2,5], k = 3
输出:[3,4,-1,-1,-1]
解释:
nums 中总共有 5 个长度为 3 的子数组:
[1, 2, 3] 中最大元素为 3 。
[2, 3, 4] 中最大元素为 4 。
[3, 4, 3] 中元素 不是 连续的。
[4, 3, 2] 中元素 不是 上升的。
[3, 2, 5] 中元素 不是 连续的。

示例 2:

输入:nums = [2,2,2,2,2], k = 4
输出:[-1,-1]

示例 3:

输入:nums = [3,2,3,2,3,2], k = 2
输出:[-1,3,-1,3,-1]

说明:

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

思路

有一个整数数组 nums,如果其子数组中的元素是连续且递增的,即公差为 1 的数列,定义子数组的能量值为子数组的最大元素,否则能量为 -1。返回所有长度为 k 的子数组的能量值。

显然需要使用滑动窗口,关键在于元素移入移出后如何判断是否连续且递增。我们可以使用队列记录不满足规则元素的 前一个 元素下标。当元素移入窗口时,判断元素与前一个元素是否满足规则,不满足则将前一个元素下标加入队列。窗口滑动时,先判断被移出的元素下标是否与队首相同,如果相同将队首元素删除。最后判断队列是否为空,如果为空则能量为新加入的元素,否则为 -1

官网题解使用一个计数器记录连续递增的元素个数,初始化结果数组元素为 -1,当计数器大于等于 k 时,记录 i - k + 1 位置上的能量值为当前元素,否则将计数器重置为 1。

代码


/**
 * @date 2024-11-06 5:50
 */
public class ResultsArray3254 {

    public int[] resultsArray(int[] nums, int k) {
        int n = nums.length;
        if (k == 1) {
            return nums;
        }
        int[] res = new int[n - k + 1];
        Queue<Integer> q = new ArrayDeque<>();
        int l = 0, i = 0;
        for (int r = 1; r < n; r++) {
            int prev = r - 1;
            if (nums[r] - nums[prev] != 1) {
                q.offer(prev);
            }
            if (r - l + 1 > k) {
                if (!q.isEmpty() && l == q.peek()) {
                    q.poll();
                }
                l++;
            }
            if (r - l + 1 == k) {
                if (q.isEmpty()) {
                    res[i++] = nums[r];
                } else {
                    res[i++] = -1;
                }
            }
        }
        return res;
    }

}

性能

3171.找到按位或最接近K的子数组

目标

给你一个数组 nums 和一个整数 k 。你需要找到 nums 的一个 子数组,满足子数组中所有元素按位或运算 OR 的值与 k 的 绝对差 尽可能 小 。换言之,你需要选择一个子数组 nums[l..r] 满足 |k - (nums[l] OR nums[l + 1] ... OR nums[r])| 最小。

请你返回 最小 的绝对差值。

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

示例 1:

输入:nums = [1,2,4,5], k = 3
输出:0
解释:
子数组 nums[0..1] 的按位 OR 运算值为 3 ,得到最小差值 |3 - 3| = 0 。

示例 2:

输入:nums = [1,3,1,3], k = 2
输出:1
解释:
子数组 nums[1..1] 的按位 OR 运算值为 3 ,得到最小差值 |3 - 2| = 1 。

示例 3:

输入:nums = [1], k = 10
输出:9
解释:
只有一个子数组,按位 OR 运算值为 1 ,得到最小差值 |10 - 1| = 9 。

说明:

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

思路

寻找子数组使子数组元素按位或运算的值 or 尽可能地接近 k,即求 |k - or| 的最小值。

暴力求解的基本逻辑是,外层枚举右端点,内层从后向前枚举左端点,使用 nums[j] 保存 子数组 [j, i] 的或值,通过判断 (nums[j] | right) != nums[j] 决定 right 是否对或值有贡献,如果没有贡献,那么可以不再继续向前枚举左端点,因为前面的或值已经累加了后面的值,如果对子数组 [j, i] 的或值没有贡献,那么对前面的 [j - 1, i] 包含了 [j, i] 的或值更没有贡献。

代码


/**
 * @date 2024-10-09 8:59
 */
public class MinimumDifference3171 {

    public int minimumDifference_v1(int[] nums, int k) {
        int n = nums.length;
        int res = Integer.MAX_VALUE;
        for (int i = 0; i < n; i++) {
            int right = nums[i];
            for (int j = i - 1; j >= 0 && ((nums[j] | right) != nums[j]); j--) {
                nums[j] |= right;
                res = Math.min(res, Math.abs(nums[j] - k));
            }
            res = Math.min(res, Math.abs(right - k));
        }
        return res;
    }

}

性能

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

代码

性能