题目:[语言月赛202301] 新年快乐
题号:B3703
难度:普及一
题目分析
题目背景
要过春节了,扶苏收到了她的新年礼物:一个崭新的字符串。
题目描述
扶苏收到的字符串是一个只含小写字母的字符串 s。
对于一个长度为 x 的只含小写字母的字符串 t,我们定义 t 的『上一个字符串』是:将所有的长度为 x 且仅含小写字母的字符串按字典序排列后,恰好排在 t 之前一个的字符串。
例如,字符串 bcd 的『上一个字符串』是 bcc,而 aaa 的『上一个字符串』不存在。
现在,扶苏有 q 次询问,每次询问给出一个区间 [l,r],查询:s 的第 l 个到第 r 个字符组成的字符串的『上一个字符串』是否在 s 中出现?
如果查询区间对应的字符串不存在『上一个字符串』,也算作『上一个字符串』没有在 s 中出现。
输入格式
第一行是一个字符串,表示扶苏收到的字符串 s。
第二行有一个整数,表示询问的次数 q。
接下来 q 行,每行两个整数,表示一次询问的 l,r。
输出格式
对于每次询问,输出两行,每行一个字符串。
对于每次询问的第一行输出,请输出对应字符串的『上一个字符串』。如果『上一个字符串』不存在,则输出 NULL
。
对于每次询问的第二行输出,如果对应字符串的『上一个字符串』在 s 中出现了,请输出 Happy New Year!
,否则请输出 Happy Chinese New Year!
。
分析
本题主要意思,通过输入获取一个父字符串,和若干个范围
然后分别找出这些范围内的子字符串的“上一个字符串”(字典序减一)
找到之后访问父字符串中,比较是否存在该字符串。
然后不同的条件对应不同的输出,
bcc Happy New Year! 存在指定字典序减一字符串,并且也在父字符串中找到了该字符串
NULL Happy Chinese New Year!存在指定字典序减一字符串,但是未在父字符串中找到了该字符串
zy Happy Chinese New Year!不存在指定字典序减一字符串(例如aaa的减一为zzz)
分为上述三种情况。
思路
本题代码量是肯定不会少的,
因为我们需要,在获取的父字符串中,获取指定范围的子字符串,然后计算出该子字符串的字典序减一的上一个字符串,然后再在父字符串中找寻该字符串,再对应不同的输出。
我们能做的是,尽可能的将其分解为一个个简单的子任务,将程序尽可能的模块化,这样不仅方便调试,而且思路清晰。
我们需要的写的函数
1,在父字符串中根据传入的范围输出该范围的子字符串
2,将获得的子字符串字典序减一,得到“前一个字符串”
3,传入父字符串和计算得到的指定字符串,判断是否在父字符串中存在。
此外我们还要在main函数中做好枚举和条件判断,输入和输出。
此题的数据量并不大,所以本题不用采用动态的内存分配,只需要提前设定最大范围的数组作为储存中间子字符串的传递。
代码
首先是函数一:获取指定范围的子字符串(其中p是全局变量中的缓存数组)
char *ptr(char a[],int left, int right) {//取出指定范围字符串
//left和right都是下标
pn++;
int u = 0;
for(int i = left; i <= right; i++) {
p[pn][u++] = a[i-1];
}
p[pn][u] = '\0';
return p[pn];
}
函数二:获取指定字符串的前一个字符串(此函数的形参是上一个函数的范围值)
char *ptr1(char b[])
{
int n = strlen(b);
//此时b[n]指向空字符
while(n!=0)
{
n--; //第一次指向的是最后一个字符
if(b[n] != 'a')
{b[n] = b[n] - 1;
break;
}
else
b[n] = 'z';
}
return b;//如果全是a,那么就返回全是z的字符串,则指定未找到
}
函数三:判断特定字符串是否在父字符串中存在
int find(char a[], char b[]) {
//a是主串,b是子串
int n = strlen(a);
int m = strlen(b);
for(int i = 0; i <= n - m; i++) {
int j;
for(j = 0; j < m; j++) {
if(a[i+j] != b[j]) break;
}
if(j == m) return 1; //找到子串,返回子串的起始位置
}
return 0; //未找到子串,返回-1
}
主代码
#include <stdio.h>
struct stu{ //存储指定范围的结构体一维数组
int left;
int right;
}ctr[202];
char s[402];//存储输入的字符串
char p[402][402];//存储所有的子串
char q[402][402];//存储所有的子串的字典序-1的子串
int pn = 0; //记录p数组的行数
char *ptr(char a[],int left, int right) {//取出指定范围字符串
//left和right都是下标
pn++;
int u = 0;
for(int i = left; i <= right; i++) {
p[pn][u++] = a[i-1];
}
p[pn][u] = '\0';
return p[pn];
}
char *ptr1(char b[]) //获取字典序减一的上一个字符串
{
int n = strlen(b);
//此时b[n]指向空字符
while(n!=0)
{
n--; //第一次指向的是最后一个字符
if(b[n] != 'a')
{b[n] = b[n] - 1;
break;
}
else
b[n] = 'z';
}
return b;//如果全是a,那么就返回全是z的字符串,则指定未找到
}
int find(char a[], char b[]) {
//a是主串,b是子串
int n = strlen(a);
int m = strlen(b);
for(int i = 0; i <= n - m; i++) {
int j;
for(j = 0; j < m; j++) {
if(a[i+j] != b[j]) break;
}
if(j == m) return 1; //找到子串,返回子串的起始位置
}
return 0; //未找到子串,返回-1
}
int main() {
int y;
fgets(s, 402, stdin);
int n = strlen(s);
if(s[n-1] == '\n') s[n-1] = '\0';
int r = strlen(s) - 1; //最后一个字符的下标(‘\0’前面的那个)
scanf("%d", &y);
for(int i = 1; i <= y; i++)
scanf("%d %d", &ctr[i].left, &ctr[i].right);
//访问的时候记得下标是从1开始的 1 3 代表第一个字符到第三个字符
for(int i = 1; i <= y; i++) {
int left = ctr[i].left; //第一个字符的下标
int right = ctr[i].right; //最后一个字符的下标
char *str = ptr(s, left, right); //取出指定范围字符串
char *str1 = ptr1(str); //取出字典序-1的字符串
int ret = 1;
for(int j=0;j<strlen(str1);j++)
if(str1[j]!='z')
ret = 0;
if(ret)
{
printf("NULL\nHappy Chinese New Year!\n");
continue; //如果全是z,那么就输出NULL和Happy Chinese New Year!,并且跳过后面的操作,进入下一轮循环
//注意这里是continue,不是break,因为我们要跳过后面的操作,进入下一轮循环,而不是跳出循环,所以是continue而不是break
}
int flag = find(s, str1); //查找字典序-1的字符串是否在主串中
printf("%s\n", str1);
if(flag) printf("Happy New Year!\n"); //如果找到,输出
else printf("Happy Chinese New Year!\n"); //如果未找到,输出
}
}
总结
本题思路逻辑上不难,但还是很具有挑战性的,因为要写的代码块不少,将复杂的问题简化成多个简单的子问题是解决本题的关键,当然本题也有很多可以优化的地方,比如,如果对传入传出的指针参数运用熟练,且不怕出错后的复杂调试的话,完全可以不用声明这么多的缓存数组,可以直接将每一步函数得到的指针参数(字符串首地址)直接互相传送。不过这些都不重要,重要的是模块化解决每个规模逐渐增大的题,而且要注意好代码的时间空间复杂度。