📌  相关文章
📜  权重之和至多为 K 的所有子串的计数(1)

📅  最后修改于: 2023-12-03 14:55:27.867000             🧑  作者: Mango

权重和至多为 K 的所有子串的计数

有一个长度为 $n$ 的字符串 $s$,和一个整数 $k$。定义一个子串的权重为其每个字符的出现次数的和。请你找到满足权重和至多为 $k$ 的子串的个数。

例如,对于字符串 $s = "aabbcc"$ 和 $k=4$,存在 $5$ 个子串权重和不超过 $4$。它们分别为:"a", "b", "c", "aa", "bb"。

算法思路

我们可以使用一个滑动窗口的方法来解决该问题。具体的,设当前滑动窗口的左端点为 $l$,右端点为 $r$,那么我们按如下的方式移动右端点:

  • 对于新加入的字符 $s_r$,我们将其出现次数加 $1$。
  • 如果当前子串的权重和超过了 $k$,那么我们将 $s_l$ 减 $1$,并将 $l$ 右移,直到子串的权重和不超过 $k$。

在停止移动右端点之后,我们就将以 $r$ 结尾的满足权重和至多为 $k$ 的子串计入答案。特别地,对于长度为 $l$ 至 $r$ 的子串权重和不超过 $k$,根据基本不等式我们知道其权重和必然不超过 $l \times \max_{i\in [l,r]} cnt_i$,其中 $cnt_i$ 表示 $s_i$ 在该子串中出现的次数。因此我们只需要记录 $cnt_i$ 即可在线性时间内计算当前子串的权重和。

代码实现

下面给出基于滑动窗口的代码实现。

def count_substrings(s: str, k: int) -> int:
    cnt = [0] * 26  # 记录当前子串中每个字符出现次数
    l, r = 0, -1  # 初始化滑动窗口的左右端点
    ans = 0  # 记录答案
    while r < len(s) - 1:
        r += 1  # 右端点右移
        cnt[ord(s[r]) - ord('a')] += 1  # 累加新字符的出现次数
        while l <= r and sum(cnt) > k:
            cnt[ord(s[l]) - ord('a')] -= 1  # 将左端点对应的字符出现次数减 1
            l += 1  # 左端点右移
        ans += (r - l + 1)  # 计入以 r 结尾的满足条件的子串个数
    return ans
复杂度分析

该算法的时间复杂度为 $O(n)$,其中 $n$ 是字符串 $s$ 的长度。证明如下:

  • 右端点 $r$ 最多向右移动 $n$ 次。
  • 左端点 $l$ 在向右移动的过程中最多向右移动 $n$ 次。
  • 记 $c$ 表示每个字符在 $s$ 中出现的次数的上界(例如对于任意字符 $t$,由于 $|s| \leq 10^6$,故其出现的次数必然不超过 $10^6$),那么每次修改 $cnt_i$ 的时间复杂度为 $O(c)$。
  • 因此该算法的时间复杂度为 $O(n \cdot c)$,由于 $c$ 是常数级别的,故实际运行的时间复杂度为 $O(n)$。

该算法的空间复杂度为 $O(1)$。