计数相关知识:
1.加法原理
如果完成一件事有n类方法,第i类方法有mᵢ种方式,则总方法数为:总方法数 = m₁ + m₂ + ... + mₙ
2.乘法原理
如果完成一件事需要n个步骤,第i步有mᵢ种方式,则总方法数为:总方法数 = m₁ × m₂ × ... × mₙ
3.排列
从n个不同元素中取出m个元素按顺序排列:P(n, m) = n! / (n-m)!。示例:5个人选3个排成一排:P(5,3) = 5!/(5-3)! = 60
4.组合
从n个不同元素中取出m个元素不考虑顺序:C(n, m) = n! / (m! × (n-m)!)
【1】594. 最长和谐子序列
日期:10.16
2.题目类型:数组,哈希表,排序,滑动窗口
3.方法一:滑动窗口(一次题解)
先对数组进行排序,这样相同的数字会聚集在一起
使用滑动窗口技术找到满足条件的最长子数组
和谐子序列的条件:窗口内最大值 - 最小值 = 1
begin 指向第一个连续相同元素的子序列的第一个元素,end 指向相邻的第二个连续相同元素的子序列的末尾元素,如果满足二者的元素之差为 1,则当前的和谐子序列的长度即为两个子序列的长度之和,等于 end−begin+1。
关键代码:
cpp
sort(nums.begin(),nums.end());
int sum=0;
int begin=0;// 滑动窗口的起始位置
for(int end=0;end<nums.size();end++){
// 当窗口内最大值与最小值的差超过1时,收缩窗口
while(nums[end]-nums[begin]>1){
begin++;
}
if(nums[end]-nums[begin]==1){
sum=max(sum,end-begin+1);
}
}
4.方法二:哈希表(半解)
首先遍历一遍数组,得到哈希映射。随后遍历哈希映射,设当前遍历到的键值对为 (x,value),那么就查询 x+1 在哈希映射中对应的统计次数,就得到了 x 和 x+1 出现的次数,和谐子序列的长度等于 x 和 x+1 出现的次数之和。
关键代码:
cpp
for(int num:nums){
cnt[num]++;
}
//遍历哈希表,检查相邻数字
for(auto [key, val]:cnt){
if(cnt.count(key+1)){
// 更新结果为当前数字和相邻数字频率和的最大值
res=max(res,val+cnt[key+1]);
}
}
【2】811. 子域名访问计数
日期:10.17
2.题目类型:数组,哈希表,字符串,计数
3.方法一:哈希表(半解)
为了获得每个子域名的计数配对域名,需要使用哈希表记录每个子域名的计数。遍历数组 cpdomains,对于每个计数配对域名,获得计数和完整域名,更新哈希表中的每个子域名的访问次数。
遍历数组 cpdomains 之后,遍历哈希表,对于哈希表中的每个键值对,关键字是子域名,值是计数,将计数和子域名拼接得到计数配对域名,添加到答案中。
substr(0, space) 从字符串 cpdomain 中提取子字符串
stoi() 是 "string to integer" 的缩写,将字符串转换为整数,会自动处理前导空格和符号
关键代码:
cpp
for(auto &&cpdomain:cpdomains){
// 分离数字和域名
int space=cpdomain.find(' ');//找到空格位置,分离数字部分和域名部分
int count=stoi(cpdomain.substr(0, space));//将数字字符串转换为整数
string domain=cpdomain.substr(space + 1);//获取域名字符串
// 统计完整域名
counts[domain]+=count;
// 统计所有子域名
for(int i=0;i<domain.size();i++){
if(domain[i]=='.'){
string subdomain=domain.substr(i+1);
counts[subdomain]+=count;
}
}
}
// 将统计结果转换为输出格式
for(auto &&[subdomain,count]:counts){
ans.emplace_back(to_string(count)+" "+subdomain);
}
【3】819. 最常见的单词
日期:10.18
2.题目类型:哈希表,字符串,计数
3.方法一:哈希表+计数(半解)
需要使用哈希集合存储禁用单词列表中的单词。
遍历段落 paragraph,得到段落中的所有单词,并对每个单词计数,使用哈希表记录每个单词的计数。由于每个单词由连续的字母组成,因此当遇到一个非字母的字符且该字符的前一个字符是字母时,即为一个单词的结束,如果该单词不是禁用单词,则将该单词的计数加 1。如果段落的最后一个字符是字母,则当遍历结束时需要对段落中的最后一个单词判断是否为禁用单词,如果不是禁用单词则将次数加 1。
在遍历段落的过程中,对于每个单词都会更新计数,因此遍历结束之后即可得到最大计数,即出现次数最多的单词的出现次数。遍历段落之后,遍历哈希表,寻找出现次数等于最大计数的单词,该单词即为最常见的单词。
isalpha() 判断字符是否为字母
tolower() 将字符转换为小写,实现大小写不敏感
关键代码:
cpp
// 遍历段落,提取单词并统计频率
for(int i=0;i<=length;i++){
// 如果是字母字符,添加到当前单词(转换为小写)
if(i<length&&isalpha(paragraph[i])){
word.push_back(tolower(paragraph[i]));
}
// 如果遇到非字母字符或到达结尾,且当前单词不为空
else if(word.size()>0){
// 如果单词不在禁用列表中
if(!bannedSet.count(word)){
frequencies[word]++;
maxFrequency=max(maxFrequency,frequencies[word]); // 更新最大频率
}
word=""; // 重置当前单词
}
}
日期:10.19
2.题目类型:哈希表,字符串,计数
3.方法一:哈希表(官方题解)
找出在两个句子中一共只出现一次的单词。
因此我们可以使用一个哈希映射统计两个句子中单词出现的次数。对于哈希映射中的每个键值对,键表示一个单词,值表示该单词出现的次数。在统计完成后,我们再对哈希映射进行一次遍历,把所有值为 1 的键放入答案中即可。
stringstream 自动按空格分割单词
move(word) 使用移动语义提高性能
关键代码:
cpp
// 定义lambda函数用于分割句子并统计单词频率
auto insert=[&](const string& s){
stringstream ss(s); // 创建字符串流
string word;
while(ss>>word) { // 自动按空格分割单词
++freq[move(word)]; // 移动语义,避免不必要的拷贝
}
};
insert(s1);
insert(s2);
// 收集只出现一次的单词
vector<string> ans;
for(const auto& [word, occ]:freq){
if(occ==1){
ans.push_back(word);
}
}
日期:10.20
2.题目类型:哈希表,字符串,计数,组合计数
3.方法一:组合数学(半解)
每首歌曲对结果的影响因素是它的持续时间除以 60 后的余数。可以用一个长度为 60 的数组 cnt,用来表示余数出现的次数。然后分情况统计歌曲对:

关键代码:
cpp
vector<int> cnt(60);
for(int t:time){
cnt[t%60]++; // 统计每个时间对60取模的余数
}
long long res=0; // 使用long long防止整数溢出
// 处理余数1到29的情况(与59到31配对)
for(int i=1;i<30;i++){
res+=cnt[i]*cnt[60-i]; // 配对数量 = 余数i的数量 × 余数(60-i)的数量
}
// 处理特殊情况:余数0和余数30
res+=(long long)cnt[0]*(cnt[0]-1)/2+ // 余数0内部配对:组合数C(n,2)
(long long)cnt[30]*(cnt[30]-1)/2; // 余数30内部配对:组合数C(n,2)
【6】1079. 活字印刷
日期:10.21
2.题目类型:哈希表,字符串,回溯,深度优先搜索
3.方法一:回溯(官方题解)
使用深度优先搜索(DFS)生成所有可能的序列
通过字符计数确保不会超过可用字符的数量
使用回溯法探索所有可能的排列组合
关键代码:
cpp
// DFS函数:递归生成所有可能的序列
int dfs(unordered_map<char,int>& count,set<char>& tile,int i){
// 基线条件:没有剩余字符可用
if(i==0){
return 1;
}
int res=1; // 从1开始,表示当前序列
for(char t:tile){
if(count[t]>0){
count[t]--; // 使用一个该字符
res+=dfs(count,tile,i-1); // 递归处理剩余字符
count[t]++; // 回溯,恢复字符计数
}
}
日期:10.22
2.题目类型:哈希表,字符串,计数,排序
3.方法一:排序 + 哈希表(一次题解)
首先将元素按照 values 的值进行降序排序。待排序完成后,依次遍历每个元素并判断是否选择。使用一个变量 choose 记录已经选择的元素个数,以及一个哈希表记录每一种标签已经选择的元素个数(键表示标签,值表示该标签已经选择的元素个数):
如果 choose=numWanted,直接退出遍历;
如果当前元素的标签在哈希表中对应的值等于 useLimit,忽略这个元素,否则选择这个元素,并更新 choose、哈希表以及答案。
关键代码:
cpp
for(int i=0;i<n&&choose<numWanted;++i){
int label=labels[id[i]];
// 如果该标签已达到使用限制,跳过
if(cnt[label]==useLimit){
continue;
}
// 选择当前物品
++choose;
ans+=values[id[i]];
++cnt[label];
}
【8】1160. 拼写单词
日期:10.23
2.题目类型:哈希表,字符串,计数
3.方法一:哈希表(一次题解)
只需要用一个哈希表存储 chars 中每个字母的数量,再用一个哈希表存储 word 中每个字母的数量,最后将这两个哈希表的键值对逐一进行比较即可。
关键代码:
cpp
for(string word : words){
// 统计当前单词中每个字符的出现频率
unordered_map<char,int> word_cnt;
for(char c:word){
++word_cnt[c];
}
// 检查当前单词是否可以用 chars 拼写
bool is_ans=true;
for(char c:word){
// 如果 chars 中某个字符的数量不足
if(chars_cnt[c]<word_cnt[c]){
is_ans=false;
break;
}
}
// 如果可以拼写,累加单词长度
if(is_ans){
ans+=word.size();
}
日期:10.24
2.题目类型:字符串,计数
3.方法一:统计(一次题解)
构成单词 "balloon" 需要 1 个字母 'b' 、1 个字母 'a' 、2 个字母 'l' 、2 个字母 'o' 、1 个字母 'n',因此只需要统计字符串中字母 'a','b','l','o','n' 的数量即可。其中每个字母 "balloon" 需要两个 'l','o',可以将字母 'l','o' 的数量除以 2,返回字母 'a','b','l','o','n' 中数量最小值即为可以构成的单词数量。
关键代码:
cpp
for(auto & ch: text){
if(ch=='b'){
cnt[0]++;
}else if(ch=='a'){
cnt[1]++;
}else if(ch=='l'){
cnt[2]++;
}else if(ch=='o'){
cnt[3]++;
}else if(ch=='n'){
cnt[4]++;
}
}
cnt[2]/=2;
cnt[3]/=2;
【10】1221. 分割平衡字符串
日期:10.25
2.题目类型:贪心,字符串,计数
3.方法一:贪心(半解)
根据题意,对于一个平衡字符串 s,若 s 能从中间某处分割成左右两个子串,若其中一个是平衡字符串,则另一个的 L 和 R 字符的数量必然是相同的,所以也一定是平衡字符串。
为了最大化分割数量,可以不断循环,每次从 s 中分割出一个最短的平衡前缀,由于剩余部分也是平衡字符串,将其当作 s 继续分割,直至 s 为空时,结束循环。
代码实现中,可以在遍历 s 时用一个变量 d 维护 L 和 R 字符的数量之差,当 d=0 时就说明找到了一个平衡字符串,将答案加一。
关键代码:
cpp
int ans=0,d=0;
for(char ch : s){
ch=='L'?++d:--d;
if(d==0){
++ans;
}
}