年轻的宝可梦教练谢尔盖·B.发现了一栋大房子,由n 个公寓按从左到右的顺序排列组成。每个公寓都可以从街上进入,也可以从每个公寓出去。此外,每个公寓都与左边的公寓和右边的公寓相连。第1号公寓只与第2号公寓相连,第n 号公寓只与第n - 1号公寓相连。
每个公寓里都有一种类型的宝可榜。谢尔盖·B.请求房子的居民让他按顺序进入他们的公寓来捕捉宝可榜。在与房子的居民商量后,他们决定让谢尔盖·B.从街上进入一个公寓,访问几个公寓,然后从某个公寓出去。但他们不会让他访问同一个公寓超过一次。
谢尔盖·B.非常高兴,现在他想尽可能少地访问公寓,以收集出现在这栋房子里的所有宝可榜。你的任务是帮助他确定他必须访问的最少公寓数量。
输入
第一行包含整数n (1 ≤ n ≤ 100 000) --- 房子里的公寓数量。
第二行包含长度为n 的行s ,由英文字母的大写和小写组成,第i 个字母表示在第i号公寓里的宝可榜的类型。
输出
输出谢尔盖·B.应该访问的最少公寓数量,以便捕捉到房子里出现的所有类型的宝可榜。
示例 1
| Inputcopy | Outputcopy |
|---|---|
3 AaA |
2 |
示例 2
| Inputcopy | Outputcopy |
|---|---|
7 bcAAcbc |
3 |
示例 3
| Inputcopy | Outputcopy |
|---|---|
6 aaBCCe |
5 |
注意
在第一个测试中,谢尔盖·B.可以从第1号公寓开始,例如,然后在第2号公寓结束。
在第二个测试中,谢尔盖·B.可以从第4号公寓开始,然后在第6号公寓结束。
在第三个测试中,谢尔盖·B.必须从第2号公寓开始,然后在第6号公寓结束。
一、 题目分析
【题目大意】
有n个公寓排成一排,每个公寓里有一只特定类型的宝可梦(用大小写英文字母表示)。谢尔盖希望连续访问一段公寓,确保能抓到这栋楼里出现的所有种类 的宝可梦,并且要求访问的公寓数量最少。
【核心等价转化】
把题目剥开,这其实是一道纯粹的字符串问题:
给定一个字符串s,求它的一个最短的连续子串,使得这个子串包含了s中出现过的所有不同的字符。
【物理模型匹配】
看到"连续子串"和"最短/最长",我们应该立刻想到------滑动窗口.
-
扩张(右指针向右): 当我们还没凑齐所有种类的宝可梦时,我们只能硬着头皮继续往右走,把新的公寓纳入窗口。
-
收缩(左指针向右): 当我们已经凑齐了所有种类时,我们要考虑:最左边的那间公寓是不是多余的?能不能把它踢掉以缩短总长度?
二、 思考过程与算法设计
解决这个问题,我们需要分两步走:
第一步:全图开视野(明确目标)
我们首先需要遍历一次整个字符串,用一个哈希表(或 ASCII 数组)统计出一共有多少种不同的宝可梦。记为cnt。这就是我们滑动窗口需要达成的"KPI"。
第二步:滑动窗口动态结算
有了目标cnt,我们就可以派出左右两个指针l和r去拉尺子了:
-
右指针
r不断向右探索,将路过的宝可梦种类记录下来。 -
一旦当前窗口内的宝可梦种类数达到了
cnt,说明当前窗口是合法的。 -
此时,我们记录下当前窗口的长度
r-l+1,更新最小长度mi。 -
为了寻找更短 的可能,我们强制让左指针
l向右收缩,吐出最左边的宝可梦,看看吐掉之后是不是依然合法。重复这个过程。
三、 时空复杂度分析
-
时间复杂度: O(N)。在这个过程中,无论是左指针还是右指针,都只会一直向右走,绝对不会回头。每个字符最多进入窗口一次,离开窗口一次。完美线性复杂度!
-
空间复杂度: O(
)。由于宝可梦的类型仅由大小写字母组成,ASCII 码的范围在128以内。我们只需要开两个大小为 200 的整型数组来充当哈希表,空间消耗微乎其微,约等于 O(1)。
四、 易错点总结
-
大小写敏感:题目明确说明是由大写和小写字母组成,所以 'A' 和 'a' 是两种不同的宝可梦。使用数组充当哈希表时,数组大小至少要开到 128(推荐开 200 确保安全)。
-
无穷大初始化 :既然是求"最小值",记录答案的变量
mi一定要初始化为一个极大的数(如0x3f3f3f3f或n),千万不能初始化为0。 -
内层循环的越界 :右指针向右滑动的过程中,务必时刻检查
r<s.size(),防止数组越界引发错误。
五、 标程注释与框架对比
这里为大家提供两种实现思路。第一种直觉写法,第二种是工程上的黄金模板。
版本一:定左探右法(物理模拟的直觉呈现)
这套代码以左端点i为外层主循环,内部用while驱动右端点探索,极其生动地模拟了"拉尺子"的过程:
cpp
//CodeForces - 701C
#include <iostream>
#include <cstring>
using namespace std;
int n;
string s;//s[i]表示在第i号公寓里的宝可榜的类型
int a[200];//记录每种类型宝可棒出现多少次
int cnt;//记录一共有多少种宝可榜
int cnt2;//记录进入的公寓内总共有多少种宝可棒
int b[200];//记录进入的公寓内每种宝可榜收集多少次
int mi=0x3f3f3f3f;//记录必须访问的最小公寓数量
int main(){
cin>>n;
cin>>s;
//记录每一种宝可榜出现多少次及总共多少种
for(int i=0;i<s.size();i++){
//如果该类型未出现过 就把类型数加一
if(a[s[i]]==0)
cnt++;
a[s[i]]++;//然后出现次数加一
}
int r=0;//从第r间公寓出去(右端点,实际为r+1)
//从第i间公寓进入(实际为i+1)
for(int i=0;i<s.size();i++){
//向右缩小左边界
if(i>0){
//当从第i间公寓进入时第i-1间收集的宝可榜就要减掉
//所以至少要从最左边+1的公寓开始减(即i>0)
//即如果左端点右移,需要把前一间公寓的宝可梦吐出来
b[s[i-1]]--;
//当该类型收集次数为0时
//进入的公寓内收集种类减1
if(b[s[i-1]]==0) cnt2--;
}
//内部不断拉长右端点,直到凑齐所有种类
while(r<s.size()){
//如果第r间里的宝可梦尚未收集过
//就把进入公寓内宝可榜收集种类+1
if(b[s[r]]==0) cnt2++;
//然后该种宝可棒收集次数+1
b[s[r]]++;
//如果进入的公寓内宝可棒出现总类少于所有种类,就仅扩大右边界
if(cnt2<cnt) r++;
else{//收集种类数等于总种类数
r++;//边界向右移动一个备用(不然下一轮会重复访问公寓)
break;//退出
}
}
//现在可以更新一下最小进入公寓数
if(cnt2==cnt) mi=min(mi,r-i);
}
cout<<mi;
return 0;
}
版本二:定右缩左(滑动窗口的"黄金模板")
版本一的逻辑非常严密,但内外循环的跳转稍显复杂。
在算法竞赛中,我们更推崇"外层无脑进,内层看情况缩"的流水线结构,这样极不容易出现越界和死循环:
cpp
//CodeForces - 701C
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
int n,cnt=0,current_cnt=0;
int total_map[200],window_map[200];
string s;
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin>>n>>s;
for(char c:s) {
if(total_map[c]==0) cnt++;
total_map[c]++;
}
int mi=n; //答案最大就是 n
int l=0; //左指针
//外层永远是右指针r负责吃进新元素
for(int r=0;r<n;r++){
//无脑吃进右边的新宝可榜
if (window_map[s[r]]==0) current_cnt++;
window_map[s[r]]++;
//内层:只要凑齐了,就不停地尝试吐掉左边的冗余元素
while(current_cnt==cnt) {
//满足条件,更新最小值
mi=min(mi,r-l+1);
//左指针吐出元素,开始收缩
window_map[s[l]]--;
if(window_map[s[l]]==0) {
current_cnt--; // 某种宝可榜被彻底吐光了,循环将被打破
}
l++;
}
}
cout<<mi;
return 0;
}