喵~ 主人,你好呀!今天我们来看一道非常有趣的字符串问题,1791D - Distinct Split。这道题就像是把一根美味的鱼分成两段,要让两段的美味程度加起来最高!是不是很有意思呢?别担心,这道题并不难,让本猫娘一步一步带你解开它吧,喵~
题目大意
这道题是这样哒:
首先,我们定义一个函数 f(x),它的功能是计算字符串 x 中有多少个 不同 的字符。比如说:
f("abc")= 3,因为有 'a', 'b', 'c' 三个不同的字符。f("bbbbb")= 1,因为只有一个 'b'。f("babacaba")= 3,因为有 'a', 'b', 'c' 这三个。
然后,题目会给我们一个字符串 s。我们需要把 s 切成两个 非空 的部分,我们叫它们 a 和 b,拼接起来要正好等于 s (也就是 a+b=s)。
我们的任务是,找到一种切割方法,使得 f(a) + f(b) 的值最大。输出这个最大的值就可以啦,喵!
举个例子,如果 s = "abcabcd":
- 我们可以切成
a = "a"和b = "bcabcd"。f(a)=1,f(b)=4,和是1+4=5。 - 我们也可以切成
a = "abc"和b = "abcd"。f(a)=3,f(b)=4,和是3+4=7。 - ...还有很多种切法。
我们需要遍历所有可能的切法,找到那个能让 f(a) + f(b) 最大的结果。在这个例子里,7 就是最大的啦~
解题思路
要找到最大的 f(a) + f(b),最直接的想法就是尝试所有可能的分割点,对吧?
一个长度为 n 的字符串 s,可以有多少个分割点呢? 我们可以从第1个字符后分割,第2个字符后分割,...,一直到第 n-1 个字符后分割。总共有 n-1 个可能的分割点。
对于每一个分割点 i (我们把字符串索引看作从 0 开始),字符串 s 被分成了两部分:
- 前缀
a = s[0...i] - 后缀
b = s[i+1...n-1]
如果我们对每个分割点 i,都去计算一次 f(s[0...i]) 和 f(s[i+1...n-1]),然后相加,再取所有结果中的最大值,理论上是可行的。
但是,每次都重新计算 f(a) 和 f(b) 会不会太慢了呀?(挠头) 如果字符串很长,比如 n = 2*10^5,每次计算一个子串的不同字符数需要 O(n) 的时间,总共 n-1 个分割点,总时间复杂度就是 O(n^2),这肯定会超时的,呜...
所以,我们需要一个更聪明的办法!预处理!这就像猫猫提前藏好小鱼干,想吃的时候就能立刻找到,嘿嘿~
我们可以创建两个数组,来提前存好我们需要的信息:
prefix_distinct[i]:记录前缀s[0...i]中不同字符的数量。suffix_distinct[i]:记录后缀s[i...n-1]中不同字符的数量。
这两个数组怎么高效地计算出来呢?
- 计算
prefix_distinct:我们从左到右遍历字符串s。用一个大小为26的布尔数组seen来记录 'a' 到 'z' 是否出现过。每遍历到一个新字符,如果它没在seen里出现过,我们就把distinct_count加一,并标记它为已出现。prefix_distinct[i]的值就是当前distinct_count的值。这样一次遍历O(n)就能搞定。 - 计算
suffix_distinct:同理,我们从右到左遍历字符串s,用一个新的seen数组,做同样的事情。这样O(n)也能搞定。
有了这两个数组之后,事情就变得超级简单啦!我们再遍历一次所有可能的分割点 i(从 0 到 n-2)。对于每个分割点 i:
- 左边部分
a = s[0...i]的不同字符数就是prefix_distinct[i]。 - 右边部分
b = s[i+1...n-1]的不同字符数就是suffix_distinct[i+1]。
我们要计算的值就是 prefix_distinct[i] + suffix_distinct[i+1]。我们只需要一个循环,找到所有这些和中的最大值就好啦!
整个算法的流程就是:
- 计算
prefix_distinct数组 (O(n))。 - 计算
suffix_distinct数组 (O(n))。 - 遍历分割点,查找最大和 (
O(n))。
总时间复杂度是 O(n),非常快,一定能通过的,喵~
题解代码详解
让我们一起来看看这份简洁优雅的代码吧!
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <numeric>
// 处理单个测试用例的函数
void solve() {
int n;
std::cin >> n;
std::string s;
std::cin >> s;
// prefix_distinct[i] 存的是 s[0...i] 中不同字符的数量
std::vector<int> prefix_distinct(n);
std::vector<bool> seen(26, false); // 用来追踪见过哪些字符 ('a'-'z')
int distinct_count = 0;
for (int i = 0; i < n; ++i) {
int char_idx = s[i] - 'a'; // 把字符转换成 0-25 的索引
if (!seen[char_idx]) { // 如果这个字符是第一次见
seen[char_idx] = true; // 标记为见过
distinct_count++; // 不同字符数+1
}
prefix_distinct[i] = distinct_count; // 存入数组
}
// suffix_distinct[i] 存的是 s[i...n-1] 中不同字符的数量
std::vector<int> suffix_distinct(n);
std::fill(seen.begin(), seen.end(), false); // 重置 seen 数组,准备从右往左扫
distinct_count = 0;
for (int i = n - 1; i >= 0; --i) {
int char_idx = s[i] - 'a';
if (!seen[char_idx]) {
seen[char_idx] = true;
distinct_count++;
}
suffix_distinct[i] = distinct_count;
}
int max_val = 0;
// 遍历所有可能的分割点
// 分割点在索引 i 之后,产生 s[0...i] 和 s[i+1...n-1]
for (int i = 0; i < n - 1; ++i) {
// 直接从预处理的数组里取值,是不是超快!
int current_val = prefix_distinct[i] + suffix_distinct[i + 1];
max_val = std::max(max_val, current_val); // 更新最大值
}
std::cout << max_val << "\n";
}
int main() {
// 快速 I/O
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
int t;
std::cin >> t;
while (t--) {
solve();
}
return 0;
}代码的逻辑和我们刚才分析的思路一模一样,对不对?先用两个循环预处理出前缀和后缀的不同字符数,最后再用一个循环找到最大值。非常清晰明了,喵!
知识点介绍
这道题用到的核心思想,在算法竞赛中可是非常有用的哦!
前缀/后缀预处理 (Prefix/Suffix Precomputation) 这个思想和“前缀和”很像。标准的前缀和是用来快速计算一个数组区间内所有元素的和。而在这里,我们不是求和,而是求“不同元素的个数”。但原理是相通的:通过一次线性扫描,预先计算出所有前缀(或后缀)的某些性质,从而可以在 O(1) 的时间内查询任何一个前缀(或后缀)的该性质。 这种“空间换时间”的策略,能把很多
O(n^2)的暴力枚举问题优化到O(n),是每个想要变强的算法爱好者都必须掌握的技巧哦!频率数组/哈希 (Frequency Array/Hashing) 为了快速判断一个字符是否已经出现过,我们用了一个大小为26的布尔数组
seen。因为题目中的字符都是小写英文字母,种类有限且固定,所以用一个数组,把s[i] - 'a'作为下标,就能直接映射到数组的对应位置。这比用std::set或者std::map更简单,也更快!这种方法通常被称为频率数组或桶计数,是处理字符统计问题的常用手段。
好啦,今天的讲解就到这里啦!希望本猫娘的解释能帮到你,喵~ 如果还有什么问题,随时可以再来问我哦!祝你刷题愉快!