拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

蓝桥杯日志统计题解:滑动窗口与双指针实战解析

蓝桥杯日志统计题解:滑动窗口与双指针实战解析

1. 题目解读与背景分析

1.1 题目到底是什么

第一次看到“日志统计”这四个字,可能不少朋友会觉得这题很简单——无非就是给一堆日志,统计统计热度呗。但真正上手之后才发现,这道2018年蓝桥杯第九届的真题,实际上是考察滑动窗口、双指针、排序哈希综合运用的经典题目,也是当年区分度比较高的一道题。

先看原题大致描述:小明维护着一个程序员论坛,收集了一份“点赞”日志,日志共有N行,每行包含两个整数,分别表示ts(时间戳)和id(帖子编号)。现在小明想知道,在哪些帖子中,任意长度为D的时间段内(注意是长度为D,不是差值为D),收到的点赞数不少于K个。如果满足这个条件,则称该帖为“热帖”。要求输出所有热帖的id,并按从小到大排序。

这个题目在蓝桥杯历年真题里地位很特殊。它不像纯模拟题那样无脑,也不像图论、DP那样需要高深算法,恰恰卡在一个“需要一点优化思维但又不至于太难”的位置。对于备赛蓝桥杯的同学来说,这道题是练习滑动窗口思想的绝佳素材,特别是准备蓝桥杯python组、Java组、C++组的同学,几乎都绕不开它。

1.2 题目考察的核心能力

这道题表面上考察的是模拟+统计,实际上暗含三个核心能力的考察:

第一个是数据组织能力。输入的日志是无序的,时间戳和帖子id交错在一起,怎么把数据组织成方便处理的结构?这决定了后续算法的复杂度。

第二个是时间复杂度的敏感度。如果直接对每个帖子、每个时间区间暴力枚举,数据量一大必然超时。很多同学第一次写出来的暴力版本,在小数据上没问题,一到蓝桥杯的评测数据就直接时间超限。今年不少同学在蓝桥杯赛场上栽跟头,往往不是不会做,而是没意识到需要优化。

第三个是边界条件的处理能力。时间区间是左闭右开还是左闭右闭?点赞数“不少于K”还是“大于K”?排序去重的细节?这些看似小的点,却直接决定代码能否AC。

这道题放在第九届是有一定“劝退”作用的,但现在回头看,它其实就是滑动窗口的入门经典。掌握了这道题,后面遇到同类区间统计问题,就会有很清晰的思路。

2. 核心思路拆解与方案选型

2.1 为什么不能暴力解

很多人的第一反应是:直接用一个二维数组,cnt[postId][time],然后对于每个帖子枚举所有长度为D的时间窗口统计点赞数。理论上看起来没啥问题,但算一下复杂度就明白了。

假设N条日志,帖子编号最大值是M,时间戳最大值是T,暴力做法的时间复杂度是O(N * T)或者O(M * T),在数据范围较大的情况下(比如N达到10^5,时间戳达到10^5甚至更大),这个复杂度是无法承受的。蓝桥杯的评测虽然不像ACM那么变态,但对超时同样是零容忍。

还有一个隐藏问题:日志数据是稀疏的。并不是每个时间点都有点赞记录,用一个稠密的二维数组去存储稀疏的数据,本身就是极大浪费。这也是为什么需要哈希表来组织数据。

2.2 滑动窗口:这道题最优雅的解法

既然暴力不行,那就要换个思路。这里我直接给出核心观察:对于一个固定的帖子id,它的点赞时间戳排好序之后,我们只需要判断是否存在一个区间[i, j],使得times[j] - times[i] < D且区间内点赞数(即j-i+1)不小于K。

这里有个关键细节:题目说的是“长度为D的时间段”,在代码实现时通常判断times[j] - times[i] < D,这样可以保证窗口长度严格小于D。不过要注意,不同版本的题目描述可能有差异,有的版本是<= D,这个细节决定了边界条件怎么写。

具体步骤是这样的:

  1. 用哈希表(字典)把每个帖子id对应的所有点赞时间戳存起来,每个id的时间戳是一个列表。
  2. 对每个id的时间戳列表从小到大排序。
  3. 在每个排序后的列表上用双指针维护一个窗口:右指针不断向右扩展,把新的时间戳纳入窗口;当times[right] - times[left] >= D时,左指针向右收缩,直到窗口合法。
  4. 如果窗口内的元素个数(点赞数)达到K,则该id就是热帖,记录答案。

这个思路的时间复杂度是O(N log N),主要开销在排序上,双指针部分每个元素最多进出窗口一次,是线性的。排序加双指针的组合,直接就把原来的超高复杂度降下来了。

2.3 哈希表选择:C++ map还是unordered_map

如果使用C++实现,这里有个很实际的选择题:用map<int, vector<int>>还是unordered_map<int, vector<int>>?

map底层是红黑树,key有序,但插入和查询是O(log N)。unordered_map底层是哈希表,插入和查询均摊O(1),但key无序。

因为最后要按照id从小到大输出热帖,用map的话,遍历时天然有序,省去最后排序的一步;用unordered_map则需要在结尾单独排序。从代码简洁角度看,map更省事,而且N在10^5量级时,两者的性能差距并不明显。不过如果追求极致性能且不介意多写一行排序,unordered_map在数据量大的时候会略快一些。

我个人在实际写题时通常用map,因为蓝桥杯的评测环境对代码长度和出错概率更敏感,map少一个排序步骤,更稳。用Python的话就不存在这个问题了,直接字典搞定。

3. 完整实现与代码深度解析

3.1 C++完整实现

先上我用C++写的完整AC代码,代码里加了详细注释,方便对照分析:

#include <bits/stdc++.h> using namespace std; int main() { int n, d, k; scanf("%d %d %d", &n, &d, &k); map<int, vector<int>> mp; // id -> 时间戳列表 for (int i = 0; i < n; i++) { int ts, id; scanf("%d %d", &ts, &id); mp[id].push_back(ts); } vector<int> ans; for (auto &it : mp) { int id = it.first; vector<int> &times = it.second; sort(times.begin(), times.end()); int left = 0, right = 0; int cnt = 0; // 滑动窗口:窗口内是 [left, right) while (right < (int)times.size()) { // 窗口右边界扩展,纳入一个新时间戳 while (right < (int)times.size() && times[right] - times[left] < d) { cnt++; right++; } // 如果当前窗口内点赞数满足条件,记录答案 if (cnt >= k) { ans.push_back(id); break; // 找到一个即可,不需要继续找 } // 左边界收缩,移出一个时间戳 cnt--; left++; // 注意:这里如果right == left,需要重置窗口 if (left == right) { cnt = 0; if (right < (int)times.size()) { cnt++; right++; } } } } for (int id : ans) { printf("%d\n", id); } return 0; }

这段代码用的是map<int, vector<int>>,天然按id排序,最后直接遍历输出即可。但这段代码有个细节要仔细想:当left移动之后,cnt维护的是当前窗口[left, right)内的元素个数,而不是简单的right - left,因为right可能已经移动到了末尾,而cnt的变化需要手动维护。

其实这里有一个更简洁的写法,伪代码如下:

for (auto &it : mp) { sort(it.second.begin(), it.second.end()); int l = 0, r = 0; while (r < it.second.size()) { if (it.second[r] - it.second[l] < d) { r++; } else { l++; } if (r - l >= k) { ans.push_back(it.first); break; } } }

这个写法更直观:先用l和r两个指针维护窗口,当窗口不满足时间差小于d时左指针右移;当窗口满足时间差小于d时右指针右移;每次移动后检查窗口长度(r - l)是否达到k。窗口长度就是点赞数,因为窗口内每个时间戳代表一次点赞。

注意这个写法里,r - l恰好等于窗口内元素数量,不需要额外维护cnt,逻辑更清晰。这也是我推荐大家掌握的版本。

3.2 Python完整实现

Python版本对准备蓝桥杯python组的同学更重要,因为近年来Python参赛人数暴涨,这道题也频繁出现在各路真题解析里。Python实现如下:

n, d, k = map(int, input().split()) from collections import defaultdict mp = defaultdict(list) for _ in range(n): ts, id_ = map(int, input().split()) mp[id_].append(ts) ans = [] for id_ in sorted(mp.keys()): times = sorted(mp[id_]) l = 0 for r in range(len(times)): # 保证窗口内时间差小于 d while times[r] - times[l] >= d: l += 1 # 如果窗口长度达到 k,说明是热帖 if r - l + 1 >= k: ans.append(id_) break for id_ in ans: print(id_)

Python版本的逻辑更紧凑:外层遍历mp的key(即帖子id),按id排序后依次处理。内层用for r in range(len(times))作为右指针,每次循环中当窗口不满足时间差小于d时,左指针l右移,直到窗口重新合法。然后判断r-l+1是否达到k。

这里有一个关键点:为什么只判断一次就break?因为只要存在一个满足条件的窗口,该帖子就是热帖,不需要继续找。这个优化可以节省大量时间。

3.3 参数计算与核心细节说明

这道题的“参数”主要体现在时间差判断上。

题目描述说“任意长度为D的时间段”,但代码里是times[r] - times[l] < d。为什么是小于而不是小于等于?这里涉及到时间段的定义:如果时间段是从t到t+D,那么长度是D;但点赞时刻如果在t+D这一瞬间,算不算在这个时间段内?不同题目描述有细微差别。蓝桥杯2018年第九届这题的官方说法是“长度为D的时间段”,通常理解为开区间,即times[r] - times[l] < d。

但有一版这道题的描述写的是“在任意长度为D的闭区间内”,那判断条件就应该是times[r] - times[l] <= d。这就要求我们读题时格外注意,比赛时碰到这种边界描述一定要仔细。我在备考时喜欢把两种判断都写一遍,本地验证边界数据后再提交,这样最稳。

另外,窗口内的时间戳数量就是点赞次数,因为同一个时间戳可能出现多次?这里要特别说明:在真实的日志统计数据中,一个时间戳不可能对应同一条帖子两次点赞,但蓝桥杯的测试数据里,同一时间戳同一条帖子可能出现多次(理论上不合理,但数据就是这样给的)。vector里每个元素代表一次点赞,两个相同的时间戳也代表两次点赞,所以r-l+1就是点赞数,不做去重。这一点很多新手会踩坑,把时间戳去重后再统计,反而错了。

3.4 两种思路对比总结

方案时间复杂度空间复杂度实现难度推荐指数
暴力枚举(帖子id × 时间窗口)O(N * T)O(N)低不推荐
排序 + 双指针滑动窗口O(N log N)O(N)中强烈推荐
排序 + 前缀和O(N log N + M * D)O(N)中看情况

前缀和方案也是一种可行思路:对每个帖子id的时间戳做前缀和,然后枚举每个长度为D的窗口,用前缀和O(1)查询窗口内点赞数。但这种方式需要把时间戳离散化或者映射到连续数组,代码复杂度比双指针高,而且枚举所有窗口的效率反而不如双指针一步到位。所以主流解法还是滑动窗口。

4. 常见问题与排查技巧实录

4.1 超时问题:十有八九是暴力了

这是最常见的错误。你在本地测试小数据时一切正常,交到OJ上就TLE。原因就是复杂度太高。解决办法只有一个:换滑动窗口。如果已经用了滑动窗口还超时,可以检查以下几点:

  • 是不是用了vector的push_back频繁扩容?可以预先reserve或换成deque。
  • 是不是在循环里反复调用size()函数?在C++里times.size()返回的是size_t,循环里反复调用效率略低,可以提前存起来。
  • 是不是用了endl进行输出?大数据的输出应该用\n而不是endl。

4.2 边界条件错误

最常见的边界错误是:

  1. 用times[r] - times[l] <= d但题目要求开区间,导致边界情况多算了一个。
  2. 窗口内点赞数判断是>= k还是> k?题面要求“不少于K个”,必然是>= k。
  3. 热帖id要从大到小还是从小到大?题目要求从小到大。

判断边界最有效的方法是构造一组极端小数据手算一遍。比如:

输入: 5 2 2 1 1 2 1 3 1 4 2 5 2

手动模拟:帖子1的时间戳是1、2、3,d=2,区间[1,3)包含1和2,点赞数2,达到k=2,所以1是热帖。帖子2的时间戳是4、5,区间[4,6)包含4和5,点赞数2,也是热帖。输出应该是1和2。用这组数据验证代码逻辑,边界基本能暴露出来。

4.3 数据去重的误区

前面提到,一定不要对同一帖子的时间戳去重。逻辑上听起来好像同一条帖子同一秒只能被点赞一次?但题意没有做这个限制,而且蓝桥杯的评测数据并不会遵守这种“物理直觉”。你一旦去重,那些“同一秒被点赞两次”的数据点就会漏判。

用一个极端例子说明:假设d=1,k=2,某帖子的时间戳列表是[1, 1],这代表在时间1有两次点赞。按照题意,在长度为1的时间段[1,2)内点赞数为2,是热帖。如果去重后只剩[1],点赞数为1,就错误地判定为非热帖。这种隐蔽的坑,只有亲手踩过才会记住。

4.4 输出格式与排序

蓝桥杯的判题对输出格式要求严格,每个id占一行,末尾不能有多余空格,最后一行也无所谓换不换行。如果用map遍历,天然有序;用unordered_map则要最后sort一下。有些同学直接在遍历unordered_map时输出,结果顺序错误,白丢分。

还有一个细节:空输出。如果没有任何热帖,是输出空还是输出什么?题目没说有特殊输出,那就什么都不输出。有些同学会在最后加一个换行,虽然一般不会判错,但稳妥起见,没有答案就直接结束,不要画蛇添足。

4.5 读入速度优化

对于C++选手,如果担心cin太慢,最直接的办法是用scanf。但如果你想用cin,可以加上这两行:

ios::sync_with_stdio(false); cin.tie(0);

这两行能显著提升cin的读取速度。注意加了ios::sync_with_stdio(false)之后,就不能再混用scanf和cin了,否则容易出错。Python选手则用sys.stdin.buffer.read()做快速读入,可以大幅提高大数据下的读入效率:

import sys data = sys.stdin.buffer.read().split() n, d, k = map(int, data[:3]) idx = 3 for _ in range(n): ts = int(data[idx]); id_ = int(data[idx+1]); idx += 2 mp[id_].append(ts)

4.6 常见问题速查表

现象原因解决方式
本地运行正常,OJ上超时暴力枚举或低效输出改用滑动窗口,用\n输出
答案错误,差1个结果边界条件判断出错检查< d还是<= d,>= k还是> k
输出id顺序不对用unordered_map未排序改用map或最后sort
漏掉一些热帖把同一帖子的时间戳去重保留重复时间戳,不去重
数组越界双指针移动时左指针超过右指针在循环内加l <= r保护判断
代码复杂难调窗口维护逻辑太啰嗦用简洁写法:右指针for循环+左指针while收缩

5. 同类题目扩展与进阶思路

5.1 滑动窗口应用的场景延伸

“日志统计”本质上是一个固定长度区间内的计数问题。这类问题在算法竞赛里出现频率极高,常见变体包括:

  • 长度为D的窗口内最大点赞数是多少(不要求到K,而是求最大值)
  • 多个帖子竞争热度,求最热帖子id(就是边维护边更新最大值)
  • 时间区间是环形的(时间戳是一个环,需要考虑首尾相接的情况)
  • 点赞数带上权重(每个人可以点赞多次,权重可能是非线性的)

这些变体在蓝桥杯后续的年份里都有影子。比如第十四届的某些模拟题、计数题,底层思路都和这道题一脉相承。掌握了滑动窗口,相当于拿到了区间统计问题的通用钥匙。

5.2 从滑动窗口到双指针的其他考法

双指针不只是滑动窗口的别名,它还衍生出很多变种:对撞指针(有序数组两数之和)、快慢指针(链表判环)、同向双指针(区间最值、去重)。在蓝桥杯中,双指针的结合场景很常见,比如“日志统计”这种“先排序再双指针”的模式,在很多真题里反复出现。

我建议备赛的同学把这道题刷透之后,主动做几道类似的题加深印象,比如POJ上的某些区间统计题、LeetCode的“无重复字符的最长子串”“长度最小的子数组”等。这些题的底层框架高度相似,都是“右指针扩展 + 左指针收缩”,区别只在于收缩条件和答案更新时机。

5.3 如果数据规模再扩大怎么办

如果这道题的N扩大到10^6甚至更大,O(N log N)的排序可能成为瓶颈。这时候可以考虑把时间戳的统计换成“桶排序”思想:由于时间戳范围有限(比如最大是10^5),可以用计数数组直接记录每个时间点是否有点赞,再做前缀和。但这样做的前提是提前知道时间戳的最大范围,否则空间可能不够。

还有一种思路是用“对偶尺取”或者其他更高级的算法,但蓝桥杯一般不会考到这么深。目前这道题的数据范围,排序+双指针是绝对够用的。

5.4 考场实战建议

在2026年蓝桥杯备考中,这道题很适合作为“模拟+数据结构”模块的重点练习。我给大家几个实战层面的建议:

  1. 拿到题先看数据范围,不要急着写代码。如果N在10^5量级,任何O(N^2)的做法都不可取,直接往滑动窗口上思考。
  2. 代码写完先构造边界测试,比如K=1、D=1、所有点赞集中在同一秒这类极端情况,确认无误再提交。
  3. 蓝桥杯的评测是赛后统一判分,没有实时反馈,所以必须靠平时的扎实积累来保证一遍AC。

6. 实操总结与个人经验记录

这道“日志统计”我刷了不止一遍,不同时期的收获完全不同。第一次做的时候也是一通暴力,结果在时间测试点上翻车。第二次静下来分析数据特征,才想到排序+双指针。到第三遍做的时候,已经能几分钟内写出完全正确的代码,连注释都不用加。

其中有一个细节印象特别深刻:我当时用了unordered_map但忘了最后排序,结果输出的热帖顺序是乱的。界面上显示答案错误,我看了很久都没发现问题,最后对比官方输出才恍然大悟。从那时起,我养成了一个习惯——凡是最后要求按顺序输出的题目,优先用map,如果用了unordered_map,一定在末尾补上sort。

还有一个体会是,这种区间计数问题最怕的不是算法不会,而是边界条件不清。建议大家在做这类题的时候,把“区间开闭”“不少于”这些描述专门圈出来,转化为代码中的具体符号。我的习惯是先在草稿纸上写“开区间:< d;点赞数:>= k”,再动键盘,这样能大幅减少低级错误。

最后再分享一个小技巧:如果题目数据很弱(N很小),暴力写起来快,但千万不要养成依赖暴力的习惯。蓝桥杯近几年的题目数据范围逐年增大,以前暴力能过的题,现在可能就过不了。把滑动窗口这种基础算法练成肌肉记忆,才是考场上的真正保障。

这道题虽然只是众多蓝桥杯真题中的一道,但它的价值在于:用最朴素的方式展示了“如何从暴力思维过渡到优化思维”。这也是我在备赛过程中最看重的收获。

返回列表