题意
Input
Output
输入样例
ABC??FGHIJK???OPQR?TUVWXY?
AABCDEFGHIJKLMNOPQRSTUVW??M
输出样例
ABCDEFGHIJKLMNOPQRSTUVWXYZ
-1
提示
分析
第二题最开始看着有点懵,但是做起来就发现不难。这道题可以运用尺取法来解决。
- 分析题目
提炼信息,根据题意可知:
我们需要在一个字母序列中找到一段连续的序列,其满足:
- 区间长度为26
- 区间中恰好包含所有英文字母
- 区间中可以出现" ?",并且可以代替表示任何字母
显然,本题的关键是在一个序列中寻找其中的一段符合要求的连续区间。毫不犹豫,这完全符合经典的尺取法👉[week5]平衡字符串——尺取法。
- 尺取法的基本设计
既然是尺取法,那么我们就要关注尺取法的几个关键问题:
- 什么条件下右指针前移
- 什么条件下左指针前移
- 什么条件下当前左右指针所含区间为答案
- 什么条件下结束遍历
1. 右指针前移的条件
右指针一般是在当前新加入的元素仍然满足要求时继续前移。因此若没有出现重复字母,则代表可以继续前移。
- 当未出现重复字母时
我们所要求的区间中不能包含重复出现的确定的字母。
也就是说出现" ? "时,我们默认是出现了不同于区间中已有的所有字母,但是若重复出现确认的字母,则当前区间不符合要求。
因此,当若右指针前移后指向的新字母在区间中未出现过,或是指向的是" ? "时,则右指针可以前移。
- 左右指针重叠
若当前左右指针重叠,则右指针前移。
- ❗️未到达边界时
若当前右指针没有到达序列末尾,则仍然可以移动。若右指针超出了序列末尾,则需要将其规范到序列末尾。
2. 左指针前移的条件
一般若当前区间不再符合要求时,左指针需要前移,直到区间符合要求。
- 若出现重复字母
若当前区间内存在重复字母,则应该前移左指针。
3. 可以作为答案的区间
在左右指针遍历过程中提及的符合要求的区间,实际上是指的没有违法的区间,也就是可能在之后被最终答案包含的小区间【潜力股233】。
而如果存在答案,则遍历的过程中,一定会出现符合答案要求的区间。但是在遍历过程中,可能会出现多个答案。
不过,根据题目可知,答案取最左序列。也就是说,从左至右遍历遇到的第一个符合要求的序列即为备选答案。
但是题目还说要字典序最小,这是否会影响到尺取法?答案是不会。字典序最小是仅针对区间中最左且包含" ? “的序列,因为一旦包含” ? ",则代表同一个序列有多种取法,但是这已经和尺取法无关了。
那么在本道题的尺取法中该如何判定?何时判定呢?
- 当左右指针包含区间为26时
由于答案要求区间长度为26,因此若一段连续序列合法,则左右指针之间的区间长度一定能达到26。否则一旦不合法,左指针就会前移,区间长度就会缩短。
因此只要出现该情况,则代表此时的区间即为符合要求的答案。
- 第一次遇到符合要求的区间
根据题目我们需要选最左出现的答案,本质上就是我们在遍历中遇到的第一个备选答案。所以当我们第一次遇到上述情况时,就可以直接将其选作答案,并结束尺取法了。
4. 何时结束尺取法
根据第3点可知,当我们获得答案时,即可结束尺取法。
除此之外,假设右指针一直走到序列末尾时,若左指针前移直到两者之间的区间小于26,此时区间一定不合法,不需要遍历。因此,规定左指针到序列末尾的距离大于等于26即可。
- 尺取法的具体实现
在本道题的尺取法中,我们需要解决如何标记字母是否出现以及出现" ? "情况的问题。
在我的代码中,设置了一个bool型数组,数组中每个元素对应ascii码减去65的差等于其下标的字母。true为在区间中已出现,false为没有出现。
【小tip:A-Z在ascii码中是连续的,从A开始依次增加1。A为65。】
- 指针的移动
在右指针前移时,若前移后的右指针指向的字母未出现过,则将其指向的字母标记为true,否则左指针前移;若出现的是" ? “,则标记其数量+1。
在左指针前移时,将左指针指向的字母重新标记为false后再前移;若指向的是” ? ",则问号数量-1。
- 合法区间的标记
若当前区间为合法答案,则记录下当前区间的左指针即可,因为长度固定已知,只需要从记录的起点向后遍历固定个。
- 取最小字典序
这也不难。首先我们可以设置一个char类型的数组,按升序依次存入所有大写字母。
从标记答案的起始处开始遍历序列,依次输出字母。若第一次遇到" ? “,则从头开始遍历字母数组,若当前字母已出现则跳过,将第一个遇到的未出现的字母输出,并标记为已出现。继续遍历序列,再遇到” ? "时,从上一次所取字母的下一个开始遍历。重复此操作,直到遍历完序列上长度为26的连续区间。
当从最小的字母开始遍历扫描未出现的字母时,最先遇到的一定是当前没有出现过的最小的字母,则这样的组合一定满足最小字典序答案。而在此之前所有遍历过的字母都一定出现过,所以之后再寻找未出现的字母时,只需要从上一次找到的字母之后开始寻找。
总结
- 这道题比较简单和经典,思路很快就来了。但是调试和修改的时间还是比较长,希望以后可以越来越熟练🤗不过调试的时间还是没白费,再一次证明了耐心和细心和重要
代码
//
// main.cpp
// lab2
//
//
#include <iostream>
#include <string>
#include <algorithm>
#include <vector>
#include <deque>
using namespace std;
string s;
char letter[26]=
{'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'};
bool appear[26] = {false}; //标记当前所取区间中的字符出现情况
//vector<int> ans; //标记所有符合要求的答案
int main()
{
ios::sync_with_stdio(false);
for( int i = 0 ; i < 26 ; i++ )
appear[i] = false;
cin>>s;
int n = s.size(),l = 0,r = 0,number = 0,ans = -1;
/* for( int i = 0 ; i < s.size() ; i++ )
cout<<s[i]<<" ";
cout<<n<<" !! "<<endl;*/
appear[(int)s[0] - 65] = true; //标记第一个字符已出现
while ( ( n - 1 ) - l >= 25 ) //若左指针到数组最右的长度已经小于26时,可以退出
{
if( r == l )
r++;
// cout<<s[r]<<" | "<<l<<" "<<r<<" ** "<<endl;
if( s[r] != '?' ) //如果当前为字符
{
if( appear[(int)s[r] - 65] ) //如果当前扫描的字符已经出现过
{
if( s[l] != '?' ) //若左指针为字符
{
appear[(int)s[l] - 65] = false; //则将其所指字符移出所选
// sum--; //所选字符数-1
}
else //否则将所选问号数量-1
number--;
l++; //左指标右移
}
else //如果当前扫描的字符未出现过
{
appear[(int)s[r] - 65] = true; //标记为已出现
r++; //右指标继续右移
}
}
else //如果当前为问号
{
number++; //则所选问号数量+1
r++; //右指标继续右移
}
//先移动指针,所以当前右指针所指符号其实还未被选
//即若已出现满足要求的字符串时,左指针到当前右指针的前一个为该字符串
if( r - l == 26 ) //若已选区间大小为26
{
//由于指针是从左到右依次遍历,所以第一次遇到的答案一定最小,遇到即可退出
ans = l;
break;
}
if( r > n - 1 ) //约束右指针的边界
r == n - 1;
}
if( ans == -1 ) //说明没有答案
cout<<-1<<endl;
else //有答案时
{
for( int i = ans ; i <= ans + 25 ; i++ ) //从字符串起始点开始输出
{
if( s[i] != '?' ) //若不为问号则输出
cout<<s[i];
else //若为问号,则遍历字母数组
{
for( int j = 0 ; j < 26 ; j++ )
{
if( appear[j] ) //如果当前字母已出现则跳过
continue;
else //永远优先输出第一个遇到的未出现字母
{
cout<<letter[j];
appear[j] = true; //并将其标记为已出现
break;
}
}
}
}
}
return 0;
}