数据结构 - 题解
标签与难度
标签: 哈希表, 前缀和, 字符串, 计数, 思维, 状态压缩 难度: 1600
题目大意喵~
你好呀,未来的算法大师!这道题是这样的喵:
我们拿到一个只包含字符 '0', '1', '2' 的字符串,长度是 。 我们的任务是,找出这个字符串里有多少个子串,满足一个特殊的条件:在这个子串里,'0', '1', '2' 这三个字符的数量是完全相等的!
比如说,对于字符串 "01210":
- 子串
"012"是一个解,因为它有1个'0', 1个'1', 1个'2'。 - 子串
"1210"不是解,因为它有1个'0', 2个'1', 1个'2'。
你需要计算出所有满足条件的子串的总数,然后告诉我就好啦,喵~
解题思路分析
这道题如果直接去检查每一个子串,那可就太慢了呀!一个长度为 的字符串,子串的数量可是有 这么多的,再对每个子串计数,时间复杂度就更高了,对于 这么大的情况,肯定会超时的说。所以,我们需要一个更聪明的办法,喵!
这种关于子串计数的问题,一个非常强大的武器就是前缀和!让我来带你一步步推导吧~
1. 从条件出发
我们的目标是找到一个子串 s[j...i],使得它包含的 '0', '1', '2' 数量相等。 我们用 count0(k), count1(k), count2(k) 分别表示在字符串的前 个字符(也就是 s[0...k-1])中 '0', '1', '2' 的数量。
那么,对于子串 s[j...i],其中各个字符的数量就可以表示为:
- '0' 的数量:
count0(i+1) - count0(j) - '1' 的数量:
count1(i+1) - count1(j) - '2' 的数量:
count2(i+1) - count2(j)
我们的条件是这三个数量相等,也就是: count0(i+1) - count0(j) == count1(i+1) - count1(j)count1(i+1) - count1(j) == count2(i+1) - count2(j)
2. 神奇的变形
这两个等式看起来还是有点复杂,我们来施展一下数学魔法,把它们变个形!我们把和下标 i 相关的项移到一边,和下标 j 相关的项移到另一边:
第一个等式变形后得到: count0(i+1) - count1(i+1) == count0(j) - count1(j)
第二个等式变形后得到: count1(i+1) - count2(i+1) == count1(j) - count2(j)
哇!发现了吗?这个变形揭示了一个惊人的秘密!
一个子串 s[j...i] 满足条件,当且仅当 "前缀 i 的状态" 和 "前缀 j-1 的状态" 是相同的!
这里的 "状态" 是什么呢?就是由两个差值构成的一个组合: 状态 (k) = (count0(k) - count1(k), count1(k) - count2(k))
3. 转化为计数问题
现在,问题就转化成了一个我们更熟悉的样子: 我们从头到尾遍历字符串,计算到每个位置 i 为止的前缀状态 (count0(i+1) - count1(i+1), count1(i+1) - count2(i+1))。 对于每个位置 i,我们想知道,在它之前有多少个位置 j-1,其前缀状态和当前位置 i 的前缀状态是完全一样的。
这不就是一个经典的计数问题嘛!我们可以用一个哈希表(在C++里是 std::map 或者 std::unordered_map)来解决。
4. 算法流程
- 创建一个哈希表
state_counts,用来存储每个状态(差值1, 差值2)已经出现过的次数。 - 在开始之前,一个空的前缀(可以看作是第-1个位置)是存在的,它的 '0', '1', '2' 数量都是0,所以差值是
(0, 0)。我们初始化state_counts[{0, 0}] = 1。这代表了我们有一个“起点”。 - 初始化总答案
ans = 0,以及三个字符的计数器c0 = 0, c1 = 0, c2 = 0。 - 遍历字符串的每个字符
s[i]: a. 根据s[i]的值,更新c0,c1,c2。 b. 计算当前的前缀状态currentState = (c0 - c1, c1 - c2)。 c. 在哈希表中查找currentState已经出现过多少次了。假设是k次。这k次就对应了k个合法的子串起始点j(s[j...i])。所以,我们将答案ans加上k。 d. 然后,将当前这个状态currentState的出现次数在哈希表中加一,因为它也成为了一个新的历史状态,可以作为未来子串的起点了。 - 遍历结束后,
ans就是我们要求的总数啦!
举个栗子,s = "012":
- 开始:
ans=0,c0=0, c1=0, c2=0。map里有{(0,0): 1}。 - i=0, s[0]='0':
c0=1, c1=0, c2=0。- 状态是
(1-0, 0-0) = (1,0)。 map里(1,0)出现0次,ans += 0。map更新为{(0,0):1, (1,0):1}。
- i=1, s[1]='1':
c0=1, c1=1, c2=0。- 状态是
(1-1, 1-0) = (0,1)。 map里(0,1)出现0次,ans += 0。map更新为{(0,0):1, (1,0):1, (0,1):1}。
- i=2, s[2]='2':
c0=1, c1=1, c2=1。- 状态是
(1-1, 1-1) = (0,0)。 map里(0,0)出现1次(就是最开始的空前缀),ans += 1。map更新为{(0,0):2, (1,0):1, (0,1):1}。
最后 ans 是 1,正确!这个方法是不是又快又优雅呢,喵~
代码实现
这是我根据上面的思路,精心为你准备的一份代码哦!注释很详细,希望能帮到你,喵~
#include <iostream>
#include <string>
#include <vector>
#include <map>
#include <utility> // for std::pair
// 使用一个函数来处理单次测试,这样main函数会很整洁呐
void solve() {
int n;
std::cin >> n;
std::string s;
std::cin >> s;
// 使用 long long 来存储答案,因为结果可能会很大哦!
long long ans = 0;
// c0, c1, c2 分别记录从字符串开头到当前位置'0', '1', '2'的数量
int c0 = 0, c1 = 0, c2 = 0;
// map<pair<int, int>, int> 用来存储状态出现的次数
// Key: pair<int, int> 表示状态 (count0 - count1, count1 - count2)
// Value: int 表示这个状态已经出现过的次数
std::map<std::pair<int, int>, int> state_counts;
// 初始化:空前缀的状态是 (0, 0),它出现了一次
// 这是所有子串的“共同起点”
state_counts[{0, 0}] = 1;
// 遍历整个字符串
for (char ch : s) {
// 1. 更新当前前缀的字符计数
if (ch == '0') {
c0++;
} else if (ch == '1') {
c1++;
} else { // ch == '2'
c2++;
}
// 2. 计算当前前缀的状态
std::pair<int, int> current_state = {c0 - c1, c1 - c2};
// 3. 查找这个状态之前出现过多少次
// 如果map中存在这个状态,就意味着我们找到了若干个合法的子串起始点
if (state_counts.count(current_state)) {
ans += state_counts[current_state];
}
// 4. 将当前状态的出现次数加一,为后面的计算做准备
state_counts[current_state]++;
}
std::cout << ans << std::endl;
}
int main() {
// 加速输入输出,让程序跑得更快一点~
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
int t;
std::cin >> t;
while (t--) {
solve();
}
return 0;
}复杂度分析
时间复杂度: 我们对长度为 的字符串只遍历了一次。在循环的每一步中,我们都对
std::map进行了操作(查找和插入)。std::map 的操作时间复杂度是对数级别的,即 ,其中 是 map 中不同状态的数量。在最坏的情况下, 可能接近 ,所以总的时间复杂度是 。如果使用 std::unordered_map,平均时间复杂度可以优化到 哟!空间复杂度:
state_counts这个哈希表最多会存储 个不同的状态(包括初始的空前缀)。每个状态由两个整数和一个计数器构成。因此,占用的空间与字符串长度 成正比,空间复杂度是 的说。
知识点总结
这道题是前缀和思想的一个非常漂亮的应用,喵~ 通过它我们可以学到:
- 前缀和/差分思想: 遇到子数组/子串相关的问题,特别是和区间内的元素总和、数量、属性有关时,优先考虑前缀和。它可以将对区间的查询,转化为对两个前缀端点的查询。
- 状态抽象与哈希: 将一个看似复杂的条件(三个数相等)通过数学变换,抽象成一个简单的、可哈希的“状态”(比如一个
pair)。这是解决很多计数问题的关键一步。 - 哈希表 (
std::map): 当你需要快速地记录和查询某个“状态”或“元素”的出现次数时,哈希表是你的不二之选。它把查找操作从线性时间优化到了对数或常数时间。
希望这篇题解能让你有所收获,继续加油哦,你超棒的!喵~ >w<