字符串哈希 - # CF271D Good Substrings

题目描述

你有一个由小写英文字母组成的字符串 \(s\)。其中一些英文字母是“好”的,剩下的则是“坏”的。
字符串 \(s\) 的一个子串 \(s[l...r]\)\(1 \leq l \leq r \leq |s|\)),即 \(s = s_1s_2...s_{|s|}\)(其中 \(|s|\) 表示字符串 \(s\) 的长度),定义为 \(s_l s_{l+1}...s_r\)
如果子串 \(s[l...r]\) 中的字母 \(s_l, s_{l+1}, ..., s_r\) 至多有 \(k\) 个是“坏”的(具体见样例解释),我们称该子串为“好子串”。

你的任务是计算字符串 \(s\) 中不同的好子串的数量。两个子串 \(s[x...y]\)\(s[p...q]\) 被认为是不同的,当且仅当它们的内容不相同,即 \(s[x...y] \neq s[p...q]\)

输入格式

第一行输入一个非空字符串 \(s\),由小写英文字母组成,长度不超过 \(1500\)

第二行输入一个仅由字符“0”和“1”组成的字符串,长度恰为 \(26\)。如果这个字符串的第 \(i\) 个字符为“1”,则第 \(i\) 个英文字母是“好”的,否则是“坏”的。也就是说,这一行的第一个字符对应字母 \(a\),第二个对应 \(b\),以此类推。

第三行输入一个整数 \(k\)\(0 \le k \le |s|\)),表示一个好子串中最多可以包含的“坏”字符数。

输出格式

输出一个整数,表示字符串 \(s\) 中不同的好子串的数量。

输入输出样例 #1

输入 #1

ababab
01000000000000000000000000
1

输出 #1

5

输入输出样例 #2

输入 #2

acbacbacaa
00000000000000000000000000
2

输出 #2

8

说明/提示

在第一个样例中,所有好子串有:"a"、"ab"、"b"、"ba"、"bab"。

在第二个样例中,所有好子串有:"a"、"aa"、"ac"、"b"、"ba"、"c"、"ca"、"cb"。

二、解题思路

1. 核心观察

字符串长度最大为 1600,所有子串总数最多为 \(\dfrac{1600\times 1601}{2} \approx 1.28\times 10^6\),暴力枚举所有子串完全可行。

2. 枚举子串方式

采用双循环枚举左右端点

  1. 外层循环枚举子串左端点 \(l\)
  2. 内层循环从 \(l\) 开始不断向右扩展右端点 \(r\),逐个字符累加;
  3. 过程中统计当前子串内禁用字符数量 \(cnt\),一旦 \(cnt>k\),后续向右扩展的子串必然也不合法,直接 break 剪枝。

3. 去重方案(单变量合并双哈希)

题目要求统计不同子串,需要对子串判重:

  • 使用双哈希降低哈希碰撞概率:分别计算两组不同模数的多项式哈希;
  • 代码中将两组哈希值做组合:\(hash = num1 \times MOD1 + num2\),把二元哈希对压缩为一个 long long 整数;
  • 借助 set 集合自动去重,最终集合的大小就是答案。

4. 哈希计算方式

采用滚动哈希(无需预处理前缀哈希数组与幂次数组):
固定左端点 \(l\),右端点 \(r\) 右移时,递推更新哈希值:

\[\begin{cases} num1 = (num1 \times BASE + s[r]) \bmod MOD1\\ num2 = (num2 \times BASE + s[r]) \bmod MOD2 \end{cases} \]

BASE 取 31,搭配两个大质数模数 \(10^9+7\)\(10^9+9\),碰撞概率极低。

四、代码详解(附带完整注释)

#include <iostream>
#include <cstring>
#include <set>
using namespace std;

typedef long long LL;

const LL MOD1 = 1000000007;
const LL MOD2 = 1000000009;
const int BASE = 31;
bool F[30];
set<LL> st;

int main() {
    char s[1600];
    scanf("%s", s);
    int len = strlen(s);
    for (int i = 0; i < 26; ++i) {
        char ch;
        scanf(" %c", &ch);
        if (ch == '0')
            F[i] = true;  
    }

    int k;
    cin >> k;

    // 枚举子串左端点 l
    for (int l = 0; l < len; ++l) {
        int cnt = 0;        // 当前子串内禁用字符个数
        LL num1 = 0, num2 = 0; // 两组滚动哈希
        // 枚举子串右端点 r,从 l 向右扩展
        for (int r = l; r < len; ++r) {
            int idx = s[r] - 'a';
            cnt += F[idx];
            if (cnt > k) break;
            // 滚动计算双哈希
            num1 = (num1 * BASE + s[r]) % MOD1;
            num2 = (num2 * BASE + s[r]) % MOD2;
            // 合并两个哈希值,存入集合去重
            st.insert(num1 * MOD1 + num2);
        }
    }
    cout << st.size() << endl;
    return 0;
}

五、复杂度分析

设字符串长度为 \(n\ (n\le 1600)\)

  1. 枚举子串:最坏情况 \(O(n^2)\),总运算量约 \(2.56\times 10^6\),效率极高;
  2. set 插入操作:单次复杂度 \(O(\log m)\)\(m\) 为不同子串数量;
  3. 整体复杂度:\(\boldsymbol{O(n^2 \log n)}\),完全满足本题数据限制。

六、关键点补充

  1. 输入细节
    scanf(" %c", &ch) 前面加空格,作用是跳过缓冲区中的换行、空格,避免读到空字符。
  2. 哈希合并技巧
    \(num1 \times MOD1 + num2\) 将两个 long long 哈希值合并为一个值存入 set,替代 set<pair<LL,LL>>,写法更简洁。
  3. 剪枝优化
    当子串内禁用字符数 \(cnt>k\) 时直接 break,避免无效枚举,在限制 \(k\) 较小时能大幅提速。
  4. 哈希选择
    选用两组大质数模数 + 不同进制基数,是竞赛中规避哈希碰撞的标准做法。
posted @ 2026-06-11 11:12  alice_ss  阅读(8)  评论(0)    收藏  举报
//雪花飘落效果