考试的最大困扰度 图解题解
这道题到底在问什么
- 输入
- answerKey="TTFTTFTT", k=1
- 输出
- 5
- 输入
- answerKey="TTFF", k=2
- 输出
- 4
- 输入
- answerKey="TFFT", k=0
- 输出
- 2
最优解:为什么这么做
一句话答案:LeetCode 2024 考试的最大困扰度用滑动窗口:一串只有 T 和 F,最多翻 k 次,求最长同字符段。窗口内少数派个数不超过 k 就能整段刷成一色,右扩左缩扫一遍即可。时间 O(n)、空间 O(1)。
一串 T 和 F 最多翻 k 次,到底在求什么
给一个只由 T 和 F 组成的字符串 answerKey 和次数 k,每次操作能把某个字符改成 T 或 F,最多做 k 次,问改完后最长连续相同字符段能多长。题面 answerKey 为 TTFTTFTT、k 为 1 时答案是 5:把下标 2 的 F 改成 T,前面五个位置连成一片 T。若 k 为 0、串是 TFFT 一个都不能改,最长同字符段只有 2。
枚举每一段子串数翻转,为什么算不动
答案串可能长到 5 万个字符。硬办法是圈定每个连续子串、数出少数派要翻几个,够 k 就拿长度比最大值。这样的子串约 n²/2 个,逐个还要数一遍,逼近 O(n³);一边挪一边带计数省掉重复也只压到 O(n²),平方约 25 亿次,跑不完。
少数派不超过 k,一个窗口凭什么够用
任取一段连续区间要整段变成同一个字符,只需把偏少的那派翻过去,多的留着当底色最省。所以这段要花的操作数正是少数派的个数;只要它不超过 k,就能在预算内刷成纯色,长度便是一个候选答案。
偏少的那派可能是 T、也可能是 F,所以「少数派不超过 k」这一条同时管住「最后全 T」和「最后全 F」两种打算,不必分开试。问题于是变成找最长的、少数派不超过 k 的连续区间。滑动窗口(拿左右两个边界圈住一段、右边界往右扩、左边界视情况跟进)正好对口:区间越长越好,一超预算就从左边收。
右边界扩、左边界追,窗口怎么挪
备两个计数,记当前窗口里 T 和 F 各有多少。右边界每往右吞一格就把新字符计进来。若此刻少数派超过 k,这段就翻不动了,从左边界往里踢字符、把被踢那派的计数减一,踢到少数派落回 k 以内;只踢一格往往不够,得用循环踢到合法。每当窗口合法,就用它的长度刷新记录的最大值。要翻的次数始终是 min(cntT, cntF),不是较多的那派。
拿 TTFTTFTT、k 为 1 走一遍
左边界从 0 起。右边界先吞下标 0、1 的两个 T,少数派都是 0,窗口长到 2。吞下标 2 的 F,T 计 2、F 计 1,少数派 1 不超过 1,长 3。下标 3、4 又是两个 T,少数派仍是那 1 个 F,窗口撑到 5,记录抬到 5。吞下标 5 的 F 时 T 计 4、F 计 2,少数派 2 超过 1:从左边接连踢掉下标 0、1 的 T,少数派仍是 2,再踢下标 2 的 F,F 计回落到 1 才重新合法,左边界停在 3,这段长 3、没破 5。往后吞下标 6、7 的 T,窗口至多回到长 5。扫完答案就是 5。
翻多数派、只缩一格、丢了峰值,都会栽
先算账:右边界把每个字符读进窗口一次、左边界至多踢出一次,每个字符只经手常数次,时间 O(n);只用几个计数变量,空间 O(1)。真会写坏的有三处。翻转次数取成 max(cntT, cntF),翻的就成多数派,操作白费、窗口判死太早。超预算时只缩一格就结算也不行,一次踢掉的未必够、少数派可能仍超 k,得循环缩到合法。还有人怕收缩后弄丢好成绩,其实记录存的是历史最大值,窗口变短不回落。边界顺带想清:k 为 0 时答案就是原串最长的同字符段,k 大到够翻满则整串统一。
▶ 动画逐步走查(共 22 步)——想跟着动画一帧帧对照就展开
- 3记牢这一句:窗口里少数派的个数就是要翻转的次数,这个次数不超过 k,窗口就合法。少数派超了就从左边缩,缩到刚好合法为止。下面每一帧都在套这句话。
- 4先把整串摆出来,下标从 0 到 7 依次是 T T F T T F T T。我们要从左往右滑一个窗口,窗口里数着 T 和 F 各多少个。右边这张小面板就是窗口内的统计,现在窗口还没开张,两个计数都是 0。开始读入字符。
- 5把右端下标 0 的字符 T 读进窗口,它的计数加一。现在窗口里 T 有 1 个,F 有 0 个,少数派是 0 个。少数派 0 ≤ k = 1,窗口还合法,先放着,等下结算。
- 6结算这一步。窗口 [0..0] 现在合法,大小是 1,里面翻掉少数派的 0 个字符就能全部相同。它比之前见过的都长,刷新历史最长 ans = 1。
- 7把右端下标 1 的字符 T 读进窗口,它的计数加一。现在窗口里 T 有 2 个,F 有 0 个,少数派是 0 个。少数派 0 ≤ k = 1,窗口还合法,先放着,等下结算。
- 8结算这一步。窗口 [0..1] 现在合法,大小是 2,里面翻掉少数派的 0 个字符就能全部相同。它比之前见过的都长,刷新历史最长 ans = 2。
- 9把右端下标 2 的字符 F 读进窗口,它的计数加一。现在窗口里 T 有 2 个,F 有 1 个,少数派是 1 个。少数派 1 ≤ k = 1,窗口还合法,先放着,等下结算。
- 10结算这一步。窗口 [0..2] 现在合法,大小是 3,里面翻掉少数派的 1 个字符就能全部相同。它比之前见过的都长,刷新历史最长 ans = 3。
- 11把右端下标 3 的字符 T 读进窗口,它的计数加一。现在窗口里 T 有 3 个,F 有 1 个,少数派是 1 个。少数派 1 ≤ k = 1,窗口还合法,先放着,等下结算。
- 12结算这一步。窗口 [0..3] 现在合法,大小是 4,里面翻掉少数派的 1 个字符就能全部相同。它比之前见过的都长,刷新历史最长 ans = 4。
- 13把右端下标 4 的字符 T 读进窗口,它的计数加一。现在窗口里 T 有 4 个,F 有 1 个,少数派是 1 个。少数派 1 ≤ k = 1,窗口还合法,先放着,等下结算。
- 14结算这一步。窗口 [0..4] 现在合法,大小是 5,里面翻掉少数派的 1 个字符就能全部相同。它比之前见过的都长,刷新历史最长 ans = 5。
- 15把右端下标 5 的字符 F 读进窗口,它的计数加一。现在窗口里 T 有 4 个,F 有 2 个,少数派是 2 个。少数派 2 已经 > k = 1,窗口暂时不合法,得从左边缩一缩。
- 16窗口不合法,从最左边把下标 0 的 T 踢出去,左指针右移到 1。现在窗口里 T 有 3 个,F 有 2 个,少数派 2 个。少数派还是 2 个,仍然 > k = 1,继续往右踢。
- 17窗口不合法,从最左边把下标 1 的 T 踢出去,左指针右移到 2。现在窗口里 T 有 2 个,F 有 2 个,少数派 2 个。少数派还是 2 个,仍然 > k = 1,继续往右踢。
- 18窗口不合法,从最左边把下标 2 的 F 踢出去,左指针右移到 3。现在窗口里 T 有 2 个,F 有 1 个,少数派 1 个。少数派 1 ≤ k = 1,终于合法了,可以停下来结算。
- 19结算这一步。窗口 [3..5] 现在合法,大小是 3,里面翻掉少数派的 1 个字符就能全部相同。它没有超过历史最长 5,ans 保持不动,窗口继续往右滑。
- 20把右端下标 6 的字符 T 读进窗口,它的计数加一。现在窗口里 T 有 3 个,F 有 1 个,少数派是 1 个。少数派 1 ≤ k = 1,窗口还合法,先放着,等下结算。
- 21结算这一步。窗口 [3..6] 现在合法,大小是 4,里面翻掉少数派的 1 个字符就能全部相同。它没有超过历史最长 5,ans 保持不动,窗口继续往右滑。
- 22把右端下标 7 的字符 T 读进窗口,它的计数加一。现在窗口里 T 有 4 个,F 有 1 个,少数派是 1 个。少数派 1 ≤ k = 1,窗口还合法,先放着,等下结算。
- 23结算这一步。窗口 [3..7] 现在合法,大小是 5,里面翻掉少数派的 1 个字符就能全部相同。它没有超过历史最长 5,ans 保持不动,窗口继续往右滑。
- 24整串滑完了。见过的最长合法窗口是 [0..4],也就是 TTFTT 这一段。它里面只有 1 个 F,把这个 F 翻成 T,整段就变成 5 个连续的 T,正好用掉 1 次操作。所以答案就是 5,跟开头看出来的对上了。
⚠️ 容易写错的地方
✗ 错:把要翻转的次数当成 max(cntT, cntF)
✓ 对:翻的是少数派,次数 = min(cntT, cntF)
要让整段相同,把个数少的那一派翻成多数派最省,翻多数派反而更亏
✗ 错:窗口不合法时,一次只缩一格就急着结算
✓ 对:用 while 一直缩到 min(cntT,cntF) ≤ k 为止
一次踢掉的字符可能还不够,少数派仍超 k,必须缩到重新合法才算这一步
✗ 错:窗口缩小后担心把已记录的最长答案弄丢
✓ 对:ans 用 max 记录历史峰值,窗口变小不影响它
ans 存的是曾经见过的最长合法窗口,后面窗口即使变短,之前的最大值依然留在 ans 里
✗ 错:以为必须分别试「最终全 T」和「最终全 F」两种再比较
✓ 对:单窗口只按少数派判合法,已同时覆盖两种情形
窗口合法就意味着翻掉少数派能让整段统一,不论最后统一成 T 还是 F,都被这一个条件包住了
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def maxConsecutiveAnswers(self, answerKey: str, k: int) -> int:
def f(c: str) -> int:
cnt = l = 0
for ch in answerKey:
cnt += ch == c
if cnt > k:
cnt -= answerKey[l] == c
l += 1
return len(answerKey) - l
return max(f("T"), f("F"))C++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
int maxConsecutiveAnswers(string answerKey, int k) {
int n = answerKey.size();
auto f = [&](char c) {
int l = 0, cnt = 0;
for (char& ch : answerKey) {
cnt += ch == c;
if (cnt > k) {
cnt -= answerKey[l++] == c;
}
}
return n - l;
};
return max(f('T'), f('F'));
}
};Java
import java.util.*;
class Solution {
private String s;
private int k;
public int maxConsecutiveAnswers(String answerKey, int k) {
s = answerKey;
this.k = k;
return Math.max(f('T'), f('F'));
}
private int f(char c) {
int l = 0, cnt = 0, n = s.length();
for (int r = 0; r < n; r++) {
cnt += s.charAt(r) == c ? 1 : 0;
if (cnt > k) {
cnt -= s.charAt(l++) == c ? 1 : 0;
}
}
return n - l;
}
}复杂度
时间
O(n)
n 是字符串长度。单窗口法里每个下标最多被右指针读入一次、被左指针踢出一次,合计每个字符常数次操作;参考代码的两趟法各扫一遍,仍是线性
空间
O(1)
只用到 cntT、cntF、l、ans 这几个计数变量,不随字符串变长而增加。参考代码三种语言都直接在原字符串上按下标读字符(Java 用 charAt),不额外开数组,空间同为常数
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 考试的最大困扰度 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
这题和「最大连续 1 的个数 III」是不是一回事?+
本质相同。那道题是 0 和 1 的数组,最多把 k 个 0 翻成 1、求最长连续 1;这里把「翻 0」换成「翻少数派」,把「连续 1」换成「同字符段」,还是同一个变长窗口套路。判定条件只是从「0 的个数不超过 k」变成「少数派个数不超过 k」,其余一模一样。认出「变长窗口加某类字符受限」这个母题,一串题都能顺手套下来。
参考代码为什么写成两趟、还各只数一种字符?+
参考代码把问题拆成两个子问题,用一个函数 f(c) 表示「最后统一成另一种字符时能拿到多长的一段」。传入的 c 就是被翻掉的那一派,一趟只盯它的计数 cnt,超过 k 就把左端 l 往右挪一格。先跑 f("T") 当作最后全 F,再跑 f("F") 当作最后全 T,取两者较大值。每趟只管一种字符,写起来更利落,和正文里同时数两派的单窗口做法结论一致,复杂度都是 O(n)。
为什么单看少数派就够,不用分别验证最后是全 T 还是全 F?+
因为一段区间要变成纯色,谁少翻谁最省,翻完自然统一成较多的那一派。少数派个数不超过 k,就意味着这段不论最后落成全 T 还是全 F,至少有一种在 k 次预算内能达成。所以盯着「少数派不超过 k」这一个条件,两种目标就一并被兜住了,不必开两套流程各验一遍。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 考试的最大困扰度 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。