【C/C++】467. 环绕字符串中唯一的子字符串

220 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第18天,点击查看活动详情


题目链接:467. 环绕字符串中唯一的子字符串

题目描述

把字符串 s 看作 "abcdefghijklmnopqrstuvwxyz" 的无限环绕字符串,所以 s 看起来是这样的:

  • "...zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcd...." 。 现在给定另一个字符串 p 。返回 s不同p非空子串 的数量 。 

提示:

  • 1p.length1051 \leqslant p.length \leqslant 10^5
  • p 由小写英文字母组成

示例 1:

输入: p = "a"
输出: 1
解释: 字符串 s 中只有 p 的一个 "a" 子字符。

示例 2:

输入: p = "cac"
输出: 2
解释: 字符串 s 中只有 p 的两个子串 ("a", "c") 。

示例 3:

输入:p = "zab"
输出:6
解释:在字符串 s 中有 p 的六个子串 ("z", "a", "b", "za", "ab", "zab") 。

整理题意

题目规定了一个无限长的循环字符串,是从由小写字母 az 的循环字符串组成。然后给定我们一个字符串 s,让我们在 s 串中寻找有多少个 不同子串 与无限长的循环字符串的子串相同。

需要注意的是这里的子串是要求是连续的,而子序列不要求连续。

解题思路分析

首先观察题目数据范围,给定的字符串长度最长 10510^{5}。数据范围算是比较大的,正常寻找一个字符串的所有子串需要 O(n2)O(n^2) 的时间,这是题目不能接受的时间复杂度。

问题一:计算子串个数

但由于无限循环的字符串的子串具有一个很明显的特征,就是相邻字符是字典序连续的,也就是 ASCII 码相差 1,那么我们可以在给定我们的字符串中找到字典序连续的子串,并且可以直接计算当前长度为 len 的子串包含多少个不同的子串(考虑以第一个字符开始的话有 len 个,以第二个字符开始的有 len - 1 个……以此类推,以最后一个字符开始的有 1 个,那么总共就有 1 + 2 + 3 +……+ len 这么多个,用等差数列前 n 项和可以直接计算)。

问题二:去重

但是存在重复计算的问题,需要考虑 去重 的问题。比如 "abcd""bc" 两个字符串都包含 "b""c""bc" 子串。

我们在统计子串时使得子串包含一个固定的字母且固定该字母的位置使得子串具有唯一性,比如我们固定以某个字母结尾作为标记,那么对于两个以同一个字符结尾的子串,长的那个子串必然包含短的那个。例如 "abcd""bcd" 均以 "d" 结尾,"bcd""abcd" 的子串,在统计的时候我们仅需记录最长的那个串即可,且在统计子串个数时我们只统计以该字符结尾的子串个数,例如 "abcd",我们只统计 "abcd""bcd""cd""d" 这些子串,这样就可以避免重复统计的问题了。

具体实现

  1. 我们定义 dp[i] 表示无限循环串中以字符 i 结尾且在给定字符串中的子串的最长长度,知道了最长长度,也就知道了不同的子串的个数(长度即为个数);
  2. 遍历给定字符串,统计连续字典序的字符串(这里需要注意的是因为时循环字符串,所以 za 也算是字典序连续的);
  3. 在遍历给定字符串时,维护连续递增的子串长度 len。具体来说,遍历到 p[i] 时,如果 p[i]p[i-1] 在字母表中的下一个字母,则将 k 加一,否则将 k 置为 1,表示重新开始计算连续递增的子串长度。然后,用 k 更新 dp[p[i]] 的最大值。
  4. 遍历结束后仅需将 dp[i]iaz)累加求和即可。

复杂度分析

  • 时间复杂度:O(n)O(n),其中 n 是给定字符串 p 的长度。
  • 空间复杂度:O(Σ)O(∣Σ∣),其中 Σ|\Sigma| 为字符集合的大小,本题中字符均为小写字母,故 Σ=26|\Sigma|=26

代码实现

class Solution {
public:
    int findSubstringInWraproundString(string p) {
        //dp[i]记录以 'a' + i 字母结尾的最长连续子串长度
        vector<int> dp(26, 0);
        int n = p.length();
        //k记录当前最长长度,初始化为1,即当前字母
        int k = 1;
        //初始化字符串第一个字母为1
        dp[p[0] - 'a'] = 1;
        //跳过第一个字母,避免越界
        for(int i = 1; i < n; i++){
            //判断当前字母与上一个字母是否连续,同时注意z后是a
            if(p[i] - p[i - 1] == 1 || (p[i] == 'a' && p[i - 1] == 'z')) k++;
            //如果是增加长度,否则将长度置为1
            else k = 1;
            //不断更新以字母 p[i]结尾的最长连续子串长度(去重)
            dp[p[i] - 'a'] = max(dp[p[i] - 'a'], k);
        }
        //以字母 p[i] 结尾的连续子串个数即为最长长度
        int ans = 0;
        //避免拷贝(&)的同时只读(const)
        for(const int & x : dp) ans += x;
        return ans;
    }
};

总结

  • 该题难点在于思考如何 去重 统计子串,很巧妙的固定以某个字母结尾作为标记来使得子串具有唯一性,同时考虑到 对于两个以同一个字符结尾的子串,长的那个子串必然包含短的那个,所以在统计的时候我们仅需记录最长的那个串即可。
  • 在知道长度的情况下计算以某个字母结尾的子串即为字符串长度,这是因为以字符串中每一个字符作为开头,固定字母作为结尾,这样就有该字符串长度这么多个子串,且都具有 唯一性(都包含最后一个字母,且长度不同)。
  • 测试结果: 微信截图_20220611125813.png

结束语

时间时公平的,你是懒惰还是努力,时间都会在日积月累中,慢慢给出属于你的答案。珍惜时间的人,必将为时间所珍惜。坚持提升自己,努力修炼自己,终有一天你会感谢此刻努力生长的自己。