xtuoj 字符串计数

题目

思路

要理解「哈希表统计字符位置」这个思路的由来,核心是从 "暴力枚举的痛点" 出发,一步步拆解问题、寻找规律、优化计算。我会按「发现问题→拆解问题→找规律→抽象公式→落地实现」的完整思考路径,详细解释这个思路是怎么被 "推导" 出来的,而不是凭空想到的。

第一步:先复盘暴力解法的痛点(为什么必须优化?)

你的原始暴力代码逻辑是:

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」,但有两个致命问题:

  1. 时间复杂度太高:O (n²),n=1e5 时是 1e10 次操作,远超 CPU 处理能力;
  2. 重复计算:对每个字符对 (i,j) 都要单独判断、单独累加,没有利用 "相同字符的位置规律"。

优化的核心目标:把 "逐个枚举字符对" 的 O (n²) 操作,转化为 "按字符分组批量计算" 的 O (n) 操作

第二步:拆解问题 ------ 把 "全局枚举" 转化为 "按字符分组"

首先观察一个关键事实:

只有「相同字符」的 (i,j) 对才会产生贡献,不同字符的 (i,j) 对可以直接忽略。

比如字符串 abcaba,只有 a-ab-b 的字符对需要计算,a-ba-c 等都不用管。

基于这个事实,我们可以把问题拆分成 26 个子问题(假设只有小写字母):

对每个字符 c(a-z),单独计算所有「c 的位置对 (i,j)(i<j)」的子串长度和,最后把所有字符的结果相加。

举个例子:

  • 字符 aabcaba 中的位置是 [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 +=1sum_pos +=p
  • 所有字符的 contribution 相加,就是最终答案。

这个公式的本质:

  • cnt*(p+1):前面有 cnt 个字符,每个都能和当前 p 组成 (p+1) 的基础项;
  • sum_pos:减去前面所有位置的和,得到实际的长度差;
  • 两者相减,就是当前 p 和前面所有同字符位置的长度和。

第五步:落地实现 ------ 用哈希表 / 数组统计 cnt 和 sum_pos

现在需要思考:如何高效记录每个字符的 cntsum_pos

  • 因为字符集是有限的(小写字母只有 26 个),不需要真正的哈希表,用两个数组即可:
    1. cnt[26]cnt[c] 表示字符 'a'+c 已经出现的次数;
    2. 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

这一步的思考关键:利用 "字符集有限" 的特性,用数组替代哈希表,降低实现成本,同时保证 O (1) 的访问效率

第六步:完整思考路径总结(从暴力到优化的全流程)

cpp 复制代码
发现问题:暴力枚举O(n²)超时 → 核心痛点是"逐个计算字符对"
↓
拆解问题:只有相同字符的(i,j)对有贡献 → 按字符分组计算
↓
找规律:对单个字符的位置列表,每新增一个位置p,其贡献可通过"已出现次数×(p+1) - 已出现位置和"批量计算
↓
抽象公式:贡献 = cnt*(p+1) - sum_pos(cnt是已出现次数,sum_pos是已出现位置和)
↓
落地实现:用数组(哈希表)记录每个字符的cnt和sum_pos,遍历字符串一次完成计算

为什么这个思路是 "自然推导" 而非 "凭空想到"?

这个思路的核心是 **"降维"------ 把二维的 (i,j) 枚举,降为一维的 "按字符遍历 + 批量计算"**,而这个降维的灵感来自:

  1. 对问题的 "特性观察"(只有相同字符有贡献);
  2. 对暴力解法的 "痛点拆解"(避免逐个枚举);
  3. 对具体例子的 "规律提炼"(从具体到通用公式);
  4. 对实现成本的 "工程考量"(用数组替代哈希表)。

换个角度说,如果你一开始想不到这个思路,可以先做两件事:

  1. 拿小例子手动计算,列出所有贡献项,尝试合并公式;
  2. 思考 "哪些计算是重复的?能不能批量算?"------ 这是所有优化的核心出发点。

比如你手动算 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);
}
相关推荐
天`南2 小时前
【群智能算法改进】一种改进的金豺优化算法IGJO[1](动态折射反向学习、黄金正弦策略、自适应能量因子)【Matlab代码#94】
学习·算法·matlab
Han.miracle2 小时前
数据结构与算法--006 和为s的两个数字(easy)
java·数据结构·算法·和为s的两个数字
AuroraWanderll3 小时前
C++类和对象--访问限定符与封装-类的实例化与对象模型-this指针(二)
c语言·开发语言·数据结构·c++·算法
月明长歌3 小时前
【码道初阶】LeetCode 622:设计循环队列:警惕 Rear() 方法中的“幽灵数据”陷阱
java·算法·leetcode·职场和发展
mit6.8243 小时前
博弈-翻转|hash<string>|smid
算法
代码游侠3 小时前
复习——Linux 系统编程
linux·运维·c语言·学习·算法
Han.miracle3 小时前
优选算法-005 有效三角形的个数(medium)
数据结构·算法·有效的三角形个数
yuuki2332333 小时前
【C++】类和对象下
数据结构·c++·算法
huohuopro3 小时前
结构体与链表
数据结构·算法·链表