前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【面试高频题】难度 1.5/5,常见字符串线性 DP 运用题

【面试高频题】难度 1.5/5,常见字符串线性 DP 运用题

作者头像
宫水三叶的刷题日记
发布2022-11-01 10:32:28
3110
发布2022-11-01 10:32:28
举报
文章被收录于专栏:宫水三叶的刷题日记

题目描述

这是 LeetCode 上的「467. 环绕字符串中唯一的子字符串」,难度为「中等」

Tag : 「线性 DP」、「树状数组」

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

"...zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcd...." .

现在给定另一个字符串 p 。返回 s 中 唯一 的 p 的 非空子串 的数量 。

示例 1:

代码语言:javascript
复制
输入: p = "a"

输出: 1

解释: 字符串 s 中只有一个"a"子字符。

示例 2:

代码语言:javascript
复制
输入: p = "cac"

输出: 2

解释: 字符串 s 中的字符串“cac”只有两个子串“a”、“c”。.

示例 3:

代码语言:javascript
复制
输入: p = "zab"

输出: 6

解释: 在字符串 s 中有六个子串“z”、“a”、“b”、“za”、“ab”、“zab”。

提示:

1 <= p.length <= 10^5
  • p 由小写英文字母构成

线性 DP + 树状数组 + 同字符最大长度计数

早上起来没睡醒 老了,脑袋不行了,第一反应是用「线性 DP + 树状数组」来做,估了一下时间复杂度没问题就写了。 该做法有一点点思维难度,因此可能不是这道中等题的标准解法。 ❞

定义

f[i]

为以

s[i]

为结尾的最大有效子串的长度。

从该状态定义容易得到如下的状态转移方程:

  • 存在
s[i - 1]

并且

s[i]

能够接在

s[i - 1]

后面(除了

s[i]

s[i - 1]

的下一字母以外,还特别包括

s[i - 1] = z

同时

s[i] = a

的情况),则我们有

f[i] = f[i - 1] + 1

  • 不存在
s[i - 1]

或者

s[i]

不能接在

s[i - 1]

后面,则有

f[i] = 1

,含义为

s[i]

只能自身组成子串。

与此同时,我们知道当结尾元素固定,子串长度固定,对应子串唯一确定。

当不考虑子串重复问题时,若

f[i] = k

,则以

s[i]

为结尾的有效子串数量为

k

个(对应以

s[i]

为结尾,长度范围为

[1, k]

的子数组)。

但实际上,我们不能统计相同的子串,因此我们需要考虑该如何去重。

不失一般性地,假设我们当前处理到字符串 p 中的第

i

位,以

s[i]

为结尾的最大子串长度为

f[i]

  • 此前如果 出现过
s[i]

为结尾,长度「大于等于」

f[i]

的子串的话,那么以

s[i]

为结尾长度为

f[i]

的子串必然已被统计,需要跳过。因此我们可以使用一个长度为

26

的数组 max,记录以每个字符

s[i]

结尾的,出现过的最大子串长度为多少(当 max[s[i]] >= f[i] 时,跳过计数);

  • 此前如果 出现过
s[i]

为结尾,长度「小于」

f[i]

的子串的话,我们也不能直接统计累加

f[i]

到答案上,这会导致那些以

s[i]

为结尾,长度小于

f[i]

的子串被重复计数,此时我们 需要知道在以

s[i]

为结尾,长度为

[1, f[i]]

范围内还有多少个子串尚未被统计,这可以使用「树状数组」解决:在

[1, f[i]]

中总个数为

a = f[i]

,使用树状数组维护在

[1, f[i]]

中已被统计的数的个数

b

,那么

cnt = a - b

即是本次可增加的计数,计数完成后我们还需要在树状数组中的

f[i]

位置增加

cnt

,确保下次查询相同字符结尾长度不小于

f[i]

的已覆盖子串数量时不会出错。

至此,我们通过「树状数组」+「记录同字符最大长度」的方式来分别解决「长度比

f[i]

小」和「长度比

f[i]

大」的重复子串统计问题。

代码:

代码语言:javascript
复制
class Solution {
    int N = 100010;
    int[][] trs = new int[26][N];
    int[] f = new int[N], max = new int[26];
    int n, ans;
    int lowbit(int x) {
        return x & -x;
    }
    void add(int[] tr, int x, int v) {
        for (int i = x; i <= n + 1; i += lowbit(i)) tr[i] += v;
    }
    int query(int[] tr, int x) {
        int ans = 0;
        for (int i = x; i > 0; i -= lowbit(i)) ans += tr[i];
        return ans;
    }
    public int findSubstringInWraproundString(String _p) {
        char[] cs = _p.toCharArray();
        n = cs.length; 
        for (int i = 0; i < n; i++) {
            int c = cs[i] - 'a';
            if (i == 0) {
                f[i] = 1;
            } else {
                int p = cs[i - 1] - 'a';
                if ((c == 0 && p == 25) || p + 1 == c) f[i] = f[i - 1] + 1;
                else f[i] = 1;
            }
            if (max[c] >= f[i]) continue;
            int cnt = f[i] - query(trs[c], f[i]);
            if (cnt == 0) continue;
            ans += cnt;
            add(trs[c], f[i], cnt);
            max[c] = f[i];
        }
        return ans;
    }
}
  • 时间复杂度:
O(n\log{n})
  • 空间复杂度:
O(C \times n)

,其中

C = 26

为字符串 p 的字符集大小

线性 DP

对于相同的结尾字符

c

而言,如果在整个动规过程中的最大长度为

len

,那么以

c

为结尾字符对答案的贡献为

len

基于此,我们只需保留解法一中的 max 数组即可,同时利用

f[i]

只依赖于

f[i - 1]

进行更新,因此动规数组也可以使用一个变量来代替。

代码:

代码语言:javascript
复制
class Solution {
    public int findSubstringInWraproundString(String _p) {
        char[] cs = _p.toCharArray();
        int n = cs.length, ans = 0;
        int[] max = new int[26];
        max[cs[0] - 'a']++;
        for (int i = 1, j = 1; i < n; i++) {
            int c = cs[i] - 'a', p = cs[i - 1] - 'a';
            if ((p == 25 && c == 0) || p + 1 == c) j++;
            else j = 1;
            max[c] = Math.max(max[c], j);
        }
        for (int i = 0; i < 26; i++) ans += max[i];
        return ans;
    }
}
  • 时间复杂度:
O(n)
  • 空间复杂度:
O(C)

,其中

C = 26

为字符串 p 的字符集大小

最后

这是我们「刷穿 LeetCode」系列文章的第 No.467 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-09-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 宫水三叶的刷题日记 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 题目描述
  • 线性 DP + 树状数组 + 同字符最大长度计数
  • 线性 DP
  • 最后
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档