[暴力题解系列]2023年蓝桥杯-子串简写
大伙都说暴力是最低级的算法,啊那确实。但是好的暴力才是真正牛逼的骗分。
咱就是说,暴力如何骗分呢?就是基于最暴力的算法一步步优化到能得更多分的暴力。
子串简写这题,首先第一步就能想到一件事情:暴力枚举子串开头和末尾的位置,检查是否是符合题目要求的字符,如果是,并且长度大于k,那就给答案加1。这是大伙都能想到的最基础的东西
#include<iostream>
#include<string>
using namespace std;
int main()
{
int k;
int cnt = 0;
scanf("%d", &k);
string a;
cin>>a;
char c1, c2;
cin>>c1>>c2;
for(int i=0; i<a.size()-k+1; i++)
{
if(a[i] != c1)
continue;
for(int j=i+k-1; j<a.size(); j++)
{
if(a[i] == c1 && a[j] == c2)
{
cnt++;
/*for(int k=0; k<a.size(); k++)
{
if(k == i || k == j)
cout<<"|";
cout<<a[k];
}
cout<<endl;*/
}
}
}
cout<<cnt<<endl;
return 0;
}
那么蓝桥网官网的测试结果如何呢?
兄弟,已经有70%的分了,蓝桥杯用暴力打真的不是问题。
那么在这之上还能怎么优化呢?很快我们就能发现时间复杂度主要来自于遍历整个数组,那么有什么方法抛弃掉无用数据,只遍历有用的地方呢?显而易见,我们只需要记录符合题目要求的位置就好了,然后我们再在记录好位置的数组里进行遍历。代码在下面,详细在代码内解释:
#include<iostream>
#include<string>
#include<vector>
using namespace std;
struct vec2{ //这里是记录每个符合条件的所有信息,其实应该改名叫vec3了(笑)
int x, type, total;
/*
x:原数组位置
type:哪一种字符
total:记录当前位置之前有多少个同类型的字符
*/
};
int main()
{
int k;
int cnt = 0;
scanf("%d", &k);
string a;
cin>>a;
char c1, c2;
cin>>c1>>c2;
int c1Cnt=0, c2Cnt=0;
vector<vec2> dat;
for(int i=0; i<a.size(); i++)
{
if(a[i] == c1)
{
c1Cnt ++; //录入,并且增加总数目
dat.push_back({i, 1, c1Cnt});
}
else if(a[i] == c2)
{
c2Cnt ++; //其实这个没啥必要
dat.push_back({i, 2, c2Cnt});
}
}
for(int i=0; i<dat.size(); i++)
{
if(dat[i].type == 2) //找到以c2结尾的数据
{
for(int j=i-1; j>=0; j--) //向前寻找c1的数据
{
if(dat[j].type == 1 && dat[i].x - dat[j].x + 1 >=k)
{
cnt+=dat[j].total;
/*
为什么这里会记录当前位置之前有多少个呢?其实一开始我并没有记录这个东西,但是我突然意识到,如果还要反复往前遍历所有的type 1数据,又会是一个常数级的浪费时间。但是在C++暴力指南里我也写过,任何常数级的时间都不可浪费。而在这里,如果你还需要反复遍历整个数组的前半部分,那就约等于O(n logn)(此处的n是指vector的长度)的复杂度,最坏情况会变成O(n^2)[即前面一大堆type 1数据类型,只有最后一位是type 2]。但是如果提前记录了之前的个数,那么在随机数据的情况下时间复杂度一般来说相当于一个比较小的常数,最坏情况下也只需要O(n)的复杂度就可以完成。因为遍历到末尾之后只需要找到上一个type 1然后加上总和即可。
*/
break;
}
}
}
}
cout<<cnt<<endl;
return 0;
}
那么这个代码的结果如何呢?
兄弟,又多2个点啊兄弟。不过为什么还有一个点没过呢?他不是TLE,而是WA。
回到代码,可以发现一个很大的问题,如果c1和c2是一样的字符呢?那么他就会一直认为数据里的都是c1,就不存在c2了,这时候就需要额外特判一下。把这个问题改完那就直接完美的用暴力拿下这题了,嘛,不过我现在要偷懒啦不想改啦。