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具有的性质如下:
- x尽可能离右端点越远越好(代表连续字符更长)
- 同时需要满足以 s [ x ] + k ≥ s [ i ] s[x]+k ≥ s[i] s[x]+k≥s[i](表示中间的字符能够通过不超过 k k k次的转换变成相匹配字符,如从”T”变成”F”)
在二分结束后,最长的连续字符长度就是 i − l + 1 i - l + 1 i−l+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 sum≤k
下面通过一个实例体会一下滑动窗口是如何实现的?
实现
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)