一、题目描述
1.问题描述
小明维护着一个程序员论坛。现在他收集了一份"点赞"日志,日志共有N行。其中每一行的格式是:ts id
,表示在ts
时刻编号id
的帖子收到一个"赞"。现在小明想统计有哪些帖子曾经是"热帖”。
如果一个帖子曾在任意一个长度为D
的时间段内收到不少于K
个赞,小明就认为这个帖子曾是"热帖"。
具体来说,如果存在某个时刻T
满足该帖在$[T,T十D)$这段时间内(注意是左闭右开区间)收到不少于K
个赞,该帖就曾是"热帖"。给定日志,请你帮助小明统计出所有曾是"热帖"的帖子编号。
2.输入格式
输入格式:
第一行包含三个整数 N,D,K。
以下 N 行,每行一条日志,包含两个整数 ts 和 id。其中$1≤K≤N≤10^5$ , $0≤ts≤10^5,0≤id≤10^5$。
3.输出格式
按从小到大的顺序输出热帖 id。每个 id 一行。
4.一个例子
下面是一个样例 |
输入:
7 10 2
0 1
0 10
10 10
10 1
9 1
100 3
100 3
输出:
1
3
二、题目分析
先简单的说一下这道题的意思,就是在输入的数据中找出每个 [ 当前时间点,当前时间点+D) 这个时间的范围内,id出现的次数是不是大于等于K,如果在这些范围内大于等于K,最后就输出该id。
看一下给出的样例,7代表7行日志,10代表当前时间点+10这个范围,2表示出现id次数要大于2.
在[0,0+10) 这个范围内,输入的数据中出现了2次1,等于2,所以要输出它。
在[1,1+10)这个范围内,输入的数据中只出现了1次10,不符合。
在[91,91+10) 这个范围内,输入的数据中出现了2次3,等于2,所以2要输出它。
1、暴力法
看到这道题的第一个感觉就是,直接枚举出所有时间点的情况,然后再去判断是不是符合大于等于K的条件,如果符合条件,就标记它,最后一起输出。那这样就需要两层循环,第一层循环的范围是从0开始一直到输入数据中的最大时间,第二层循环就是判断每次的时间段中每个id出现的次数是不是大于等于K。(总代码放在最后)
for (int time = 0; time <= Max_time; time++) //枚举时间
{
memset(cnt, 0, sizeof(cnt)); //每次的时间段都要重新计数
for (int i = 1; i <= n; i++) //枚举日志
{
int t = blog[i].ts;
int id = blog[i].id;
if (t >= time && t < time + d) //判断是不是符合时间范围
cnt[id]++;
if (cnt[id] >= k) //判断是不是符合大于等于K
hot[id] = true; //标记答案
}
}
但是这样干虽然简单,但是时间复杂度有点高,不能完成所有的样例。
2、双指针
对双指针完全不了解的朋友可以看下这篇文章:双指针(尺取法):爱,是双向奔赴,还是你追我赶?
双指针中快慢指针题目的特点就是:每次循环都会看窗口(两个指针围起来的区间)是不是满足某个条件,如果满足,输出的是子区间的变形。
这道题中的范围$[T,T十D)$就是一个可以滑动的窗口,我们可以使用双指针中的快慢指针,再将输入的数据按照时间的排序后,不断地移动窗口,这样当处理到T时候,$[T,T十D)$这个窗口前面的数据就会被忽略。
以本题的输入的样例为例。我们先对该组数据按照时间进行排序:
然后定义两个指针,分别为指针i和指针j
指针i作为快指针,遍历随时间而消失的帖子。指针j作为慢指针,根据题目的要求(指针i对于的时间-指针j对应的时间大于等于D),舍去T之前的帖子(指针j移动)。
int i = 0; int j = 0; //定义两个指针
for (i = 0; i < n; i++) //快指针遍历
{
num[blog[i].id]++; //遍历过的每个帖子点赞数+1
while (blog[i].ts - blog[j].ts >= d) //超出题目时间范围
{
num[blog[j].id]--; //j指针指向的帖子点赞数-1
j++; //指针j向前移动
}
}
当指针j每移动一步时候,就把其对应的帖子点赞数量减去1,这样就能保证两个指针包围的窗口不会超过题目范围以外的数据。
写完之后去测一下 (总代码放在最后) :
三、代码汇总
1、暴力代码汇总
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 1e5 + 10;
bool hot[N]; //判断是不是热帖
int cnt[N]; //记录点赞数
int n, d, k;
struct Blog
{
int ts;
int id;
}blog[N];
int main() {
scanf("%d%d%d", &n, &d, &k);
int Max_time = 0;
for (int i = 1; i <= n; i++)
{
scanf("%d%d", &blog[i].ts, &blog[i].id);
Max_time = max(blog[i].ts, Max_time); //获取输入的数据中的最大时间
}
for (int time = 0; time <= Max_time; time++) //枚举时间
{
memset(cnt, 0, sizeof(cnt)); //每次的时间段都要重新计数
for (int i = 1; i <= n; i++) //枚举日志
{
int t = blog[i].ts;
int id = blog[i].id;
if (t >= time && t < time + d) //判断是不是在范围之内
cnt[id]++;
if (cnt[id] >= k) //判断是不是符合热帖要求
hot[id] = true; //标记为热帖
}
}
for (int i = 0; i <= 1e5; i++)
if (hot[i])
printf("%d\n", i);
}
2、双指针代码汇总
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 1e5 + 10;
int num[N]; //记录帖子点赞数
bool hot[N]; //判断是不是热帖
int n, d, k;
struct Blog
{
int ts;
int id;
}blog[N];
int cmp(Blog a, Blog b)//排序规则是按时间从小到大
{
return a.ts < b.ts;
}
int main() {
scanf("%d%d%d", &n, &d, &k);
for (int i = 0; i < n; i++)
{
scanf("%d%d", &blog[i].ts, &blog[i].id);
}
sort(blog, blog + n,cmp);//排序
int i = 0; int j = 0;
for (i = 0; i < n; i++) //指针i开始遍历
{
num[blog[i].id]++; //遍历过的每个帖子点赞数+1
while (blog[i].ts - blog[j].ts >= d)//超出题目时间范围
{
num[blog[j].id]--; //指针j指向的帖子点赞数-1
j++; //指针j向前移动
}
if (num[blog[i].id] >= k) //如何符合标准
hot[blog[i].id] = true; //标记为热帖
}
for (int i = 0; i < N; i++)
if (hot[i] )
printf("%d\n", i);
return 0;
}