试题 G: 子串简写

试题 G: 子串简写

时间限制:  1.0s      内存限制:  256.0MB      本题总分: 20 分

 

【问题描述】

程序猿圈子里正在流行一种很新的简写方法: 对于一个字符串, 只保留首 尾字符, 将首尾字符之间的所有字符用这部分的长度代替。例如 internation- alization 简写成 i18n Kubernetes  (注意连字符不是字符串的一部分) 简 写成 K8sLanqiao 简写成 L5o 等。

在本题中, 我们规定长度大于等于 K  的字符串都可以采用这种简写方法 (长度小于 K 的字符串不配使用这种简写)。

给定一个字符串 S  和两个字符 c1   和 c2 ,请你计算 S  有多少个以 c1   开头 c2  结尾的子串可以采用这种简写?

 

【输入格式】

第一行包含一个整数 K

第二行包含一个字符串 S  和两个字符 c1  和 c2。

 

【输出格式】

一个整数代表答案。

 

【样例输入】

4

abababdb a b

 

【样例输出】

6

 

【样例说明】

符合条件的子串如下所示,中括号内是该子串:

 

[abab]abdb

[ababab]db

[abababdb]

ab[abab]db

ab[ababdb]

abab[abdb]

 

【评测用例规模与约定】

对于 20% 的数据, 2 ≤ K ≤ |S| ≤ 10000。

对于 100%  的数据, 2 ≤ K ≤ |S| ≤ 5 × 10^5 。S  只包含小写字母。c1  和 c2  都 是小写字母。

|S| 代表字符串 S  的长度。

 

我的答案:

一、信息

1.题目和题干的前面部分告诉我这是关于简写字符串的题目

2.简写的规则:保留首尾两个字母中间部分用长度来代替比如ABC可以简写为A1C

3.使用简写的前提条件:字符串长度大于等于K

4.给定的三个元素:S,c1,c2

5.输入格式在已有的三个元素上加入了K即最小简写长度

6.输出格式:一个整数

7.样例说明

8.评测数据规模与约定对于 20% 的数据, 2 ≤ K ≤ |S| ≤ 10000。 对于 100%  的数据, 2 ≤ K ≤ |S| ≤ 5 × 10^5 。S  只包含小写字母。c1  和 c2  都 是小写字母。

二、分析

条件1在告诉我们这个问题的背景即我们要解决的问题是字符串简写的问题;

条件2的简写规则和条件7样例说明其实就是在告诉我们这个字符串简写是怎么进行的;

条件3再告诉我们最小简写字符串长度后续,很明显就是一个判断前提条件用if语句就可以表示;

条件4和条件5其实就是再告诉我们怎么输入和输入的是什么?;

条件8这里就很有意思了就是给我们在选择算法的时候排除掉一些不满足时间复杂度的算法因为蓝桥杯的C++运行时间限制在1s的所以在选择过程中我们就可以把一些算法排除掉

三、算法设计

三、算法设计

1.算法选择 这里我一般喜欢先从简单到复杂,最简单的算法其实就是遍历,我们设计一下遍历并看一下时间复杂度是多少,首先先把输入和输出搞定这里我用的语言是C++,首先我们先定义一个整型变量命名为K int k然后定义两个字符char c1和c2然后定义一个字符串S vectorS 第二步我们实现输入第一行cin>>K; 第二行cin>>S>>c1>>c2 ; 然后我们定义输出cout<<这里我们发现我们先前没有定义这个输出的整型变量 我们回到前面重新定义 int answer;到此为止我们输入输出框架就准备好了由于从简单到复杂我们先把所有的东西都写在主函数里,我们接下来开始填充中间的细节,我们来看看字符串S该如何遍历出满足条件的子串呢?首先我的第一个思路是双指针遍历因为如果不是这样很麻烦但是这与我的先易后难的算法选择矛盾因此这里我们还是选择平时的单指针遍历,我们可以把字符串S复制一份到另外一个字符串这里就发现前面的定义变量也是不足的所以我们新定义一个字符串S1然后通过字符串复制函数把S赋值给S1然后我们定义两个指针char *p1,char *p2分别指向S和S1的首位字符然后我们用两层循环分别控制S和S1的遍历,for(int i=0;i

新问题的出现

什么字符串算法的时间复杂度满足条件?

解答:

基于我的分析,我们需要一个算法,其时间复杂度优于 O(n^2) 以适应大数据量的情况。考虑到数据规模和题目要求,我们可以考虑以下几种算法:

1. 哈希表 + 双指针

  • 哈希表可以用来快速统计和查找字符 c1 和 c2 在字符串 S 中的所有位置。
  • 双指针策略可以用来遍历这些位置的组合,快速计算满足条件的子串数量。
  • 这种方法的时间复杂度取决于 c1 和 c2 出现的频率,以及它们之间位置组合的数量,但是通常会比 O(n^2) 要好。
  • 如果有读者在这部分对于这两个概念不熟悉可以看看我的两篇文章:夏驰和徐策带你从零开始学数据结构—— 7.4 哈希表2.10 蓝桥杯基础算法之双指针

2. 前缀和

  • 如果问题可以转换为某种形式的区间查询,前缀和可以提供一个快速计算的方法。
  • 对于这个问题,前缀和可能不直接适用,因为我们关注的是特定字符的位置和数量,而不仅仅是数值的累加。

3. 后缀数组和LCP(最长公共前缀)

  • 后缀数组加上LCP可以快速处理字符串中的重复模式和子串问题。
  • 对于这个特定的问题,后缀数组可能有点过于复杂,因为我们不需要处理复杂的字符串匹配问题。

4. 二分搜索

  • 对于一些特定类型的查询,二分搜索可以将时间复杂度降低到 O(logn),但是它通常需要在已排序的数据上操作。
  • 在这个问题中,二分搜索可能不直接适用,因为我们关注的是满足特定模式的子串数量,而不是排序后的数值查询。

综上所述

对于这个特定的问题,哈希表 + 双指针似乎是最合适的选择,因为它允许我们有效地定位特定字符并快速遍历可能的子串组合。虽然直接的双指针方法可能因为高复杂度而不适用,但通过哈希表优化字符位置的查找,我们可以显著减少不必要的遍历,从而降低时间复杂度。

好我们现在摆在面前的有四条路我们先走第一条

法一:哈希表+双指针 不完善

这里又有两种思路第一种就是直接把所以字符都用哈希存储,另外一种就是只存满足条件的c1和c2知道c1和才c2的位置即可,但是我两种都试过没有什么太大差别都是错的只能过80%

我们这里选的是第一种

一、信息

  • 题目背景:字符串简写问题。
  • 简写规则:保留首尾字符,中间用长度代替。
  • 简写条件:字符串长度大于等于 K。
  • 输入:字符串 S 和两个字符 c1、c2,以及整数 K。
  • 输出:符合简写条件的子串数量。

二、分析

  • 简写规则的理解:核心在于找出所有以 c1 开头,以 c2 结尾,且长度大于等于 K 的子串。
  • 算法选择的依据:考虑到数据规模很大(|S| 最大可达 5 × 10^5),直接的暴力遍历方法(O(n^2)复杂度)将无法在限定时间内完成计算。因此,需要选择一个更加高效的算法。
  • 哈希表+双指针方法:利用哈希表快速定位字符位置,双指针用于高效地遍历和计算符合条件的子串数量。

三、算法设计

  1. 初始化:使用一个哈希表来存储字符出现的所有位置。
  2. 填充哈希表:遍历字符串 S,记录每个字符出现的位置到哈希表中。
  3. 使用双指针寻找符合条件的子串
    • 第一个指针(left)初始化为 c1 在字符串中的第一个出现位置。
    • 第二个指针(right)初始化为 left 后 c2 出现的位置。
    • 移动 right 指针,直到找到 c2,检查子串是否满足长度要求(>= K)。
    • 遍历哈希表中 c1 和 c2 的所有位置,计算符合条件的子串数量。

四、代码实现(C++)

#include 
#include 
#include 
using namespace std;

int main() {
    int K;
    string S;
    char c1, c2;
    cin >> K >> S >> c1 >> c2;

    unordered_map> positions;
    for (int i = 0; i < S.length(); i++) {
        positions[S[i]].push_back(i);
    }

    int count = 0;
    if (positions.find(c1) != positions.end() && positions.find(c2) != positions.end()) {
        for (int i : positions[c1]) {
            for (int j : positions[c2]) {
                if (j > i && j - i + 1 >= K) {
                    count++;
                }
            }
        }
    }

    cout << count << endl;
    return 0;
}

试题 G: 子串简写_第1张图片 

五、实现代码过程中可能遇到的问题

  • 性能问题:尽管使用了哈希表来优化字符位置的查找,但是在极端情况下,如果 c1 和 c2 非常频繁地出现,双层循环仍然可能导致性能瓶颈。
  • 内存消耗:哈希表存储了字符串中每个字符的所有位置,对于非常长的字符串,这可能会导致较大的内存消耗。
  • 优化:进一步的优化可能需要考虑特定情况下的优化策略,比如预先过滤掉一些明显不满足条件的情况,或者寻找更高效的数据结构和算法。

因此我们选择法2即前缀和试一下

法二:前缀和也是部分正确

一、信息

  • 简写规则:保留首尾字符,中间部分用长度代替。
  • 简写条件:字符串长度大于等于 K。
  • 输入元素:整数 K,字符串 S,字符 c1 和 c2。

二、分析

  • 简写规则与样例说明:这提供了问题的具体背景和操作定义,即如何将字符串进行简写。
  • 长度条件:确定了简写的最小长度标准,对解决方案的设计产生直接影响。
  • 输入与输出:明确了如何接收问题的参数以及如何输出结果。
  • 评测数据规模与约定:指出了算法性能要求,尤其是时间复杂度的限制,提示我们需要一个高效的算法来处理大规模数据。

三、算法设计

使用前缀和方法,我们首先考虑如何利用前缀和技术优化查找过程:

  1. 前缀和的定义:在这个问题中,我们可以定义前缀和数组来记录到当前位置为止,满足以 c1 开始和以 c2 结尾的子串数量。
  2. 算法步骤
    • 遍历字符串 S 一次,记录每个位置之前出现 c1 和 c2 的总次数。
    • 对于每个可能的起始点(c1 出现的位置),计算在距离 K-1 之后,c2 出现的总次数。这个差值即为从当前位置开始,可能形成的满足条件的子串数量。

四、代码实现(用C++)

#include 
#include 
using namespace std;

int main() {
    int K;
    string S;
    char c1, c2;
    cin >> K >> S >> c1 >> c2;

    int n = S.length();
    vector prefixC1(n+1, 0), prefixC2(n+1, 0);
    // 构建前缀和
    for (int i = 1; i <= n; ++i) {
        prefixC1[i] = prefixC1[i-1] + (S[i-1] == c1);
        prefixC2[i] = prefixC2[i-1] + (S[i-1] == c2);
    }

    int answer = 0;
    // 遍历每个可能的起始点
    for (int i = 0; i < n; ++i) {
        if (S[i] == c1 && i + K <= n) {
            // 计算结束点的范围
            int end = i + K - 1;
            answer += prefixC2[n] - prefixC2[end];
        }
    }

    cout << answer << endl;
    return 0;
}

试题 G: 子串简写_第2张图片 

五、实现代码过程中可能遇到的问题

  • 边界条件处理:确保在使用前缀和数组时,对数组索引和边界条件进行正确处理,避免越界。
  • 性能问题:虽然前缀和大大减少了重复计算,但在极端大规模数据情况下,仍需要注意循环和条件判断的效率。
  • 理解前缀和的应用:前缀和通常用于处理区间求和问题,但在这种特定场景下,其应用需要适当的调整和理解。

改进后的答案

一、信息

  • 简写规则:字符串可以简写为首尾字符加上中间字符数的形式。
  • 简写条件:字符串长度至少为 K 才能进行简写。
  • 输入元素:字符串 S,字符 c1 和 c2,整数 K。

二、分析

  • 优化的关键:利用前缀和快速计算区间内特定字符出现的次数。
  • 有效子串的确定:长度至少为 K 的以 c1 开始和以 c2 结尾的子串。
  • 前缀和数组的作用:快速查询任意区间内 c2 出现的次数。

三、算法设计

  • 使用一个数组 a 来记录前缀和,即到当前位置为止,字符 c2 出现的次数。
  • 对于字符串 S 中的每一个字符,当它等于 c1 时,计算区间 [i+k-1, n] 内 c2 出现的次数。
  • 结果 res 为在该区间内 c2 的出现次数,通过 a[n] - a[i+k-2] 快速获取。

四、代码实现(用C++)

#include
using namespace std;
typedef long long LL;
const int N = 500010;

int k;
string s;
char c1, c2;
int a[N];

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(0); cout.tie(0);
    cin >> k >> s >> c1 >> c2;
    int n = s.size();
    s = '?' + s;
    for (int i = 1; i <= n; ++i) {
        a[i] = (s[i] == c2);
        a[i] += a[i - 1];
    }
    LL ans = 0;
    for (int i = 1; i + k - 2 < n; ++i) {
        if (s[i] == c1) ans += a[n] - a[i + k - 2];
    }
    cout << ans << '\n';
    return 0;
}

五、实现代码过程中可能遇到的问题

  • 边界条件处理:必须确保在处理字符串时考虑到所有的边界条件,例如在这个代码中字符串被重新定义为从索引 1 开始。
  • 数据类型选择:由于可能出现的子串数量非常大,我们使用了 long long 类型来存储结果,以避免整数溢出。
  • 性能问题:尽管已经使用前缀和优化了时间复杂度,但在处理字符串和执行 I/O 操作时仍需注意性能。

六、错误原因

我之前的代码实现中存在的问题:

  1. 错误的边界处理:在枚举以 c1 开头的位置时,我没有正确处理子串长度的边界条件。
  2. 前缀和数组的使用:可能没有正确初始化或使用前缀和数组,导致查询的结果不准确。
  3. 逻辑错误:在计算以 c1 开头且以 c2 结尾的子串数量时,可能出现了逻辑上的错误,例如考虑了不应该计算的情况,或者遗漏了应该计算的情况。

改进的代码,使用了一个额外的 '?' 来初始化字符串 s 的前缀,这样做是为了使字符串的索引与前缀和数组的索引对齐,便于计算。同时,对于每个以 c1 开头的位置,代码直接计算了 [i + k - 1, n] 这个区间内 c2 出现的次数。这个优化显著地减少了不必要的计算,确保了算法的时间复杂度为 O(n),符合题目对时间效率的要求。

试题 G: 子串简写_第3张图片

 

 

 

你可能感兴趣的:(蓝桥杯题库,算法,前缀和,双指针,哈希表)