题目

思路
要理解「哈希表统计字符位置」这个思路的由来,核心是从 "暴力枚举的痛点" 出发,一步步拆解问题、寻找规律、优化计算。我会按「发现问题→拆解问题→找规律→抽象公式→落地实现」的完整思考路径,详细解释这个思路是怎么被 "推导" 出来的,而不是凭空想到的。
第一步:先复盘暴力解法的痛点(为什么必须优化?)
你的原始暴力代码逻辑是:
cpp
for(int i=0;i<len;i++){
for(int j=i+1;j<len;j++){
if(s[i]==s[j]) sum += j-i+1;
}
}
这个思路的核心是「枚举所有满足 s[i]=s[j] 的 (i,j) 对,累加子串长度 j-i+1」,但有两个致命问题:
- 时间复杂度太高:O (n²),n=1e5 时是 1e10 次操作,远超 CPU 处理能力;
- 重复计算:对每个字符对 (i,j) 都要单独判断、单独累加,没有利用 "相同字符的位置规律"。
优化的核心目标:把 "逐个枚举字符对" 的 O (n²) 操作,转化为 "按字符分组批量计算" 的 O (n) 操作。
第二步:拆解问题 ------ 把 "全局枚举" 转化为 "按字符分组"
首先观察一个关键事实:
只有「相同字符」的 (i,j) 对才会产生贡献,不同字符的 (i,j) 对可以直接忽略。
比如字符串 abcaba,只有 a-a、b-b 的字符对需要计算,a-b、a-c 等都不用管。
基于这个事实,我们可以把问题拆分成 26 个子问题(假设只有小写字母):
对每个字符 c(a-z),单独计算所有「c 的位置对 (i,j)(i<j)」的子串长度和,最后把所有字符的结果相加。
举个例子:
- 字符
a在abcaba中的位置是 [0,3,5],需要计算 (0,3)、(0,5)、(3,5) 这 3 对的长度和:(3-0+1)+(5-0+1)+(5-3+1) =4+6+3=13; - 字符
b的位置是 [1,4],计算 (1,4) 的长度和:4-1+1=4; - 字符
c的位置是 [2],没有 (i,j) 对,贡献 0; - 总和 = 13+4=17,和样例一致。
这一步的思考关键:从 "全局找所有字符对" 收缩到 "按字符分组找对",把问题范围缩小,为后续批量计算打基础。
第三步:找规律 ------ 对单个字符的位置列表,批量计算贡献
现在问题简化为:已知字符 c 的出现位置列表 pos = [p0, p1, p2, ..., pk-1](p0<p1<...<pk-1),求所有 i<j 的 (pi,pj) 对的 (pj-pi+1) 之和。
我们先拿具体例子推导规律,比如 a 的位置列表 [0,3,5]:
- 先列所有 (i,j) 对和对应的长度:(0,3) → 3-0+1=4;(0,5) →5-0+1=6;(3,5) →5-3+1=3;总和 = 4+6+3=13。
现在尝试把这些长度的计算公式展开,看看能不能合并:
cpp
4 = (3+1) - 0 → (p1+1) - p0;
6 = (5+1) - 0 → (p2+1) - p0;
3 = (5+1) - 3 → (p2+1) - p1;
总和 = [(p1+1)-p0] + [(p2+1)-p0] + [(p2+1)-p1]
把式子重新整理(把含 (p+1) 的项放一起,含 p 的项放一起):
cpp
总和 = (p1+1)*1 + (p2+1)*2 - (p0*2 + p1*1)
这里能发现关键规律:
- 对于位置列表中的第 m 个元素
pm(从 0 开始计数),它和前面所有p0~p(m-1)组成的对的长度和为:m*(pm + 1) - (p0 + p1 + ... + p(m-1))(m 是前面的元素个数,p0+...+p(m-1)是前面所有位置的和)。
验证这个规律(以 pm=3(第 1 个 a)为例):
- m=1(前面有 1 个元素 p0=0);
- 计算:1*(3+1) - 0 =4 → 和 (0,3) 的长度一致;
再验证 pm=5(第 2 个 a):
- m=2(前面有 2 个元素 p0=0、p1=3,和为 3);
- 计算:2*(5+1) -3 =12-3=9 → 对应 (0,5)+(3,5)=6+3=9,完全一致;
最终字符 a 的总贡献 = 4+9=13,和实际计算的一致。
这一步的思考关键:把 "逐个计算每对长度" 转化为 "用位置和、计数批量计算",从 "单点计算" 升级为 "批量计算",这是优化的核心。
第四步:抽象公式 ------ 把规律转化为可计算的通用公式
对任意字符 c 的位置列表 pos[0], pos[1], ..., pos[k-1]:
- 初始化:
cnt=0(已出现的字符 c 的个数),sum_pos=0(已出现的字符 c 的位置和),contribution=0(该字符的总贡献); - 遍历每个位置
p属于 pos:- 如果
cnt>0(前面有该字符),则当前 p 的贡献为:cnt*(p+1) - sum_pos; - 把这个贡献加到总贡献里;
- 更新
cnt +=1,sum_pos +=p;
- 如果
- 所有字符的 contribution 相加,就是最终答案。
这个公式的本质:
cnt*(p+1):前面有 cnt 个字符,每个都能和当前 p 组成(p+1)的基础项;sum_pos:减去前面所有位置的和,得到实际的长度差;- 两者相减,就是当前 p 和前面所有同字符位置的长度和。
第五步:落地实现 ------ 用哈希表 / 数组统计 cnt 和 sum_pos
现在需要思考:如何高效记录每个字符的 cnt 和 sum_pos?
- 因为字符集是有限的(小写字母只有 26 个),不需要真正的哈希表,用两个数组即可:
cnt[26]:cnt[c]表示字符'a'+c已经出现的次数;sum_pos[26]:sum_pos[c]表示字符'a'+c已经出现的位置总和;
- 遍历字符串的每个位置 i:
- 把字符 s [i] 转化为 0-25 的索引 c(
c = s[i]-'a'); - 如果
cnt[c]>0,则计算当前贡献cnt[c]*(i+1) - sum_pos[c],加到总答案里; - 更新
cnt[c]++,sum_pos[c] +=i;
- 把字符 s [i] 转化为 0-25 的索引 c(
这一步的思考关键:利用 "字符集有限" 的特性,用数组替代哈希表,降低实现成本,同时保证 O (1) 的访问效率。
第六步:完整思考路径总结(从暴力到优化的全流程)
cpp
发现问题:暴力枚举O(n²)超时 → 核心痛点是"逐个计算字符对"
↓
拆解问题:只有相同字符的(i,j)对有贡献 → 按字符分组计算
↓
找规律:对单个字符的位置列表,每新增一个位置p,其贡献可通过"已出现次数×(p+1) - 已出现位置和"批量计算
↓
抽象公式:贡献 = cnt*(p+1) - sum_pos(cnt是已出现次数,sum_pos是已出现位置和)
↓
落地实现:用数组(哈希表)记录每个字符的cnt和sum_pos,遍历字符串一次完成计算
为什么这个思路是 "自然推导" 而非 "凭空想到"?
这个思路的核心是 **"降维"------ 把二维的 (i,j) 枚举,降为一维的 "按字符遍历 + 批量计算"**,而这个降维的灵感来自:
- 对问题的 "特性观察"(只有相同字符有贡献);
- 对暴力解法的 "痛点拆解"(避免逐个枚举);
- 对具体例子的 "规律提炼"(从具体到通用公式);
- 对实现成本的 "工程考量"(用数组替代哈希表)。
换个角度说,如果你一开始想不到这个思路,可以先做两件事:
- 拿小例子手动计算,列出所有贡献项,尝试合并公式;
- 思考 "哪些计算是重复的?能不能批量算?"------ 这是所有优化的核心出发点。
比如你手动算 abcaba 中 a 的贡献时,会发现 (0,3)、(0,5)、(3,5) 的长度和,本质是 "每个新位置和前面所有位置的长度和",而这些长度和可以用 "次数 ×(p+1) - 位置和" 一次性算出来,这就是规律的来源。
最后:验证思路的正确性(用样例再走一遍)
样例输入:abcaba(索引 0:a,1:b,2:c,3:a,4:b,5:a)
- 初始化:total=0,cnt [0..25]=0,sum_pos [0..25]=0;
- i=0(字符 a,c=0):cnt [0]=0 → 无贡献;cnt [0]=1,sum_pos [0]=0;
- i=1(字符 b,c=1):cnt [1]=0 → 无贡献;cnt [1]=1,sum_pos [1]=1;
- i=2(字符 c,c=2):cnt [2]=0 → 无贡献;cnt [2]=1,sum_pos [2]=2;
- i=3(字符 a,c=0):cnt [0]=1 → 贡献 = 1*(3+1)-0=4;total=4;cnt [0]=2,sum_pos [0]=0+3=3;
- i=4(字符 b,c=1):cnt [1]=1 → 贡献 = 1*(4+1)-1=4;total=8;cnt [1]=2,sum_pos [1]=1+4=5;
- i=5(字符 a,c=0):cnt [0]=2 → 贡献 = 2*(5+1)-3=9;total=17;cnt [0]=3,sum_pos [0]=3+5=8;
- 最终 total=17,和样例一致。
整个过程只遍历字符串一次,时间复杂度 O (n),完美解决超时问题。
代码
cpp
#include <stdio.h>
#include <string.h>
#define ll long long
#define MAX_CHAR 26 // 小写字母共26个
int main() {
char s[100005];
scanf("%s", s);
int len = strlen(s);
ll sum = 0;
// 维护每个字符的出现次数和位置总和(初始为0)
int cnt[MAX_CHAR] = {0}; // cnt[c]:字符c出现的次数
ll sum_pos[MAX_CHAR] = {0}; // sum_pos[c]:字符c所有出现位置的总和
for (int i = 0; i < len; i++) {
int c = s[i] - 'a'; // 转为0~25的索引
if (cnt[c] > 0) {
// 计算当前位置i对总长度的贡献
sum += (ll)cnt[c] * (i + 1) - sum_pos[c];
}
// 更新该字符的出现次数和位置总和
cnt[c]++;
sum_pos[c] += i;
}
printf("%lld\n", sum);
}