They Are Everywhere(Codeforces- P701C)

年轻的宝可梦教练谢尔盖·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,我们就可以派出左右两个指针lr去拉尺子了:

  1. 右指针 r 不断向右探索,将路过的宝可梦种类记录下来。

  2. 一旦当前窗口内的宝可梦种类数达到了cnt,说明当前窗口是合法的。

  3. 此时,我们记录下当前窗口的长度 r-l+1,更新最小长度mi

  4. 为了寻找更短 的可能,我们强制让左指针l向右收缩,吐出最左边的宝可梦,看看吐掉之后是不是依然合法。重复这个过程。


三、 时空复杂度分析

  • 时间复杂度: O(N)。在这个过程中,无论是左指针还是右指针,都只会一直向右走,绝对不会回头。每个字符最多进入窗口一次,离开窗口一次。完美线性复杂度!

  • 空间复杂度: O()。由于宝可梦的类型仅由大小写字母组成,ASCII 码的范围在128以内。我们只需要开两个大小为 200 的整型数组来充当哈希表,空间消耗微乎其微,约等于 O(1)。


四、 易错点总结

  1. 大小写敏感:题目明确说明是由大写和小写字母组成,所以 'A' 和 'a' 是两种不同的宝可梦。使用数组充当哈希表时,数组大小至少要开到 128(推荐开 200 确保安全)。

  2. 无穷大初始化 :既然是求"最小值",记录答案的变量mi一定要初始化为一个极大的数(如 0x3f3f3f3f或n),千万不能初始化为0。

  3. 内层循环的越界 :右指针向右滑动的过程中,务必时刻检查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;
}
相关推荐
wen__xvn2 小时前
力扣模拟题刷题
算法·leetcode
bbbb3652 小时前
算法复杂度与能耗关系的多变量分析研究的技术7
算法
不要秃头的小孩2 小时前
力扣刷题——111.二叉树的最小深度
数据结构·python·算法·leetcode
wutang0ka2 小时前
LeeCode HOT 100 104.二叉树的最大深度
算法
散峰而望2 小时前
【基础算法】从入门到实战:递归型枚举与回溯剪枝,暴力搜索的初级优化指南
数据结构·c++·后端·算法·机器学习·github·剪枝
setmoon2142 小时前
C++代码规范化工具
开发语言·c++·算法
进击的小头2 小时前
第15篇:MPC的发展方向及展望
python·算法
We་ct2 小时前
LeetCode 35. 搜索插入位置:二分查找的经典应用
前端·算法·leetcode·typescript·个人开发
IT猿手3 小时前
基于 ZOH 离散化与增量 PID 的四旋翼无人机轨迹跟踪控制研究,MATLAB代码
开发语言·算法·matlab·无人机·动态路径规划·openclaw