【前缀和+二分、滑动窗口】LeetCode——2024.考试的最大困扰度

2024. 考试的最大困扰度 - 力扣(LeetCode) (leetcode-cn.com)

一位老师正在出一场由 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' 。

【考察点】前缀和+二分、滑动窗口

方法1:前缀和+二分

为什么会想到用二分做?

从题目的表述来看,在添加k次操作后,最长连续字符的结果一定位于区间 [ m , n ] [m, n] [m,n](其中, m m m表示原先字符串中连续字符的最大长度, n n n表示字符串长度)

从结果的单调性质不难想到:针对具有单调性的区间,利用二分求出结果

通常一道题,都会正着做,通过一系列步骤求解出最终结果;
同时,也可以反着来,利用二分枚举每一个可能的潜在结果,验证其正确性,并一步步逼近最终的正确结果

思路

既然已经确定利用二分的思想枚举最终解

现在的问题就在于:

如何验证解的正确性?

首先,需要进行前缀和预处理操作

简单而言,前缀和数组s[i]表示前i个字符中”T”/”F”的数量

接下来,便是遍历整个数组

并以当前的遍历点 i i i作为右端点,0为左端点,二分出可能值 x x x(即在经过不超过 k k k次操作后,可能的最长连续字符长度),则x具有的性质如下:

  1. x尽可能离右端点越远越好(代表连续字符更长)
  2. 同时需要满足以 s [ x ] + k ≥ s [ i ] s[x]+k ≥ s[i] s[x]+ks[i](表示中间的字符能够通过不超过 k k k次的转换变成相匹配字符,如从”T”变成”F”)

在二分结束后,最长的连续字符长度就是 i − l + 1 i - l + 1 il+1 i i i为当前遍历点,即固定的右端点, l l l表示遍历后距离i最远且满足不超过 k k k次操作的位置)

【需要注意的是】,对于T和F需要分别进行上述操作

实现

class Solution {
public:
    bool check(int x, int k, int a[], int sum) {
        if(a[x] + k >= sum) return true;
        else return false;
    }

    int maxConsecutiveAnswers(string a, int k) {
        int n = a.size();
        int sum_T[n+1], sum_F[n+1];

        memset(sum_T, 0, sizeof(sum_T));
        memset(sum_F, 0, sizeof(sum_F));

        int ans = 0;
        for(int i = 1; i <= n; i++) {
						// 前缀和数组
            sum_T[i] = sum_T[i-1] + (a[i-1] == 'T');
            sum_F[i] = sum_F[i-1] + (a[i-1] == 'F');
						// 分别二分T和F
            int l = 0, r = i - 1;
            while(l < r) {
                int mid = (l + r) >> 1;

                if(check(mid, k, sum_T, sum_T[i]))  r = mid;
                else l = mid + 1;
            }
            ans = max(ans, i - l); // 需要注意的细节是,数组是从1开始的,因此这里就是(i-1)-l+1
            
            l = 0, r = i - 1;
            while(l < r) {
                int mid = (l + r) >> 1;

                if(check(mid, k, sum_F, sum_F[i]))  r = mid;
                else l = mid + 1;
            }
            ans = max(ans, i - l);
        }

        return ans;
    }
};

算法复杂度分析

对于遍历一遍字符数组,复杂度是 O ( n O(n O(n)

同时,每个循环内部,需要二分从0~i的区间,复杂度是 O ( l o g n ) O(logn) O(logn)

综上,复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)

方法2:滑动窗口

只要求最大连续指定字符的数目时,本题和「1004. 最大连续1的个数 III」完全一致。

借助滑动窗口的思想,找到满足条件的最大值

具体而言:

  • 首先,维护一个右端点,以及一个中间变量 s u m sum sum以保存从左端点到右端点中不同字符的个数
  • s u m > k sum>k sum>k时,说明当前维护的区间无法满足在 k k k次操作的情况下变为同一字符
  • 进而,挪动左端点,并相应的修改 s u m sum sum的值,直到 s u m ≤ k sum≤k sumk

下面通过一个实例体会一下滑动窗口是如何实现的?
在这里插入图片描述

实现

class Solution {
public:
    int maxConsecutiveAnswers(string a, int k) {
        int n = a.size();
        int ans = 0;

        int t = 0;
        for(int r = 0, l = 0; r < n; r++) {
            t += a[r] == 'F';

            while(t > k) {
                if(a[l++] == 'F')   t--;
            }
            ans = max(ans, r - l + 1);
        }

        t = 0;
        for(int r = 0, l = 0; r < n; r++) {
            t += a[r] == 'T';

            while(t > k) {
                if(a[l++] == 'T')   t--;
            }
            ans = max(ans, r - l + 1);
        }

        return ans;
    }
};

复杂度分析

只需要遍历该字符串一遍,时间算法复杂度为 O ( n ) O(n) O(n)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值