蓝桥杯-日志统计(滑动窗口)

小明维护着一个程序员论坛。现在他收集了一份”点赞”日志,日志共有 N 行。

其中每一行的格式是:

ts id 表示在 ts 时刻编号 id 的帖子收到一个”赞”。

现在小明想统计有哪些帖子曾经是”热帖”。

如果一个帖子曾在任意一个长度为 D 的时间段内收到不少于 K 个赞,小明就认为这个帖子曾是”热帖”。

具体来说,如果存在某个时刻 T 满足该帖在 [T,T+D) 这段时间内(注意是左闭右开区间)收到不少于 K 个赞,该帖就曾是”热帖”。

给定日志,请你帮助小明统计出所有曾是”热帖”的帖子编号。

输入格式

第一行包含三个整数 N,D,K。

以下 N 行每行一条日志,包含两个整数 ts 和 id。

输出格式

按从小到大的顺序输出热帖 id。

每个 id 占一行。

数据范围

1≤K≤N≤1e5,
0≤ts,id≤1e5,
1≤D≤10000

输入样例:

7 10 2
0 1
0 10
10 10
10 1
9 1
100 3
100 3

输出样例:

1
3

题解:

这题正常暴力会超时

暴力代码👇(只能过不到一半数据)

#include <bits/stdc++.h>
using namespace std;
#define x first
#define y second
const int N = 1e5 + 10;
int cnt[N];  // 统计帖子在某个时间中出现的次数
bool st[N];  // 帖子是否是热帖
vector<pair<int,int>> v;
int main()
{
	int n, d, k; cin >> n >> d >> k;
	v.resize(n);
	
	for (int i = 0; i < n; i ++)
		cin >> v[i].x >> v[i].y;
	
	sort(v.begin(), v.end());
	
	for (int i = 0; i < n; i ++)
	{
		memset(cnt, 0, sizeof cnt);
		for (int j = i; v[j].x < v[i].x + d; j ++)
		{
			cnt[v[j].y] ++;
			if (cnt[v[j].y] >= k) 
				st[v[j].y] = true;
		}	
	}
	
	for (int i = 0; i < N; i ++)
		if (st[i]) cout << i << endl;
	return 0;
}

最优解:

本题属于滑动窗口的变形题

  1. 先对输入的 st 和 id 进行排序
  2. 维护一个滑动窗口, 窗口的时间长度是 d, 每次要维护窗口满足的条件是 使窗口中最早出现的帖子的时刻刚进来的帖子时刻相差不超过 d

也就是说每次当窗口往下移动的时候, 减去前面出现时间过早的帖子id, 加上后面进来的一个帖子id。 ( 每次只进来一个帖子, 但可能出去很多个帖子, 因为需要让整个窗口中的帖子的时间差在 区间[刚进来的帖子的st, 刚进来的帖子的st - (d - 1)] )

每次都要判断帖子的滑动窗口的帖子数量是否不小于k, 是的话这个帖子是热帖

题中要求的时间区间是[t, t + d), 我们代码中维护的区间是[t, t + d - 1]
ac代码👇

#include <bits/stdc++.h>
using namespace std;
#define x first
#define y second
const int N = 1e5 + 10;
int cnt[N];  // 帖子出现的次数
bool st[N];  // 是否是热帖
vector<pair<int,int>> v;
int main()
{
	int n, d, k; cin >> n >> d >> k;
	v.resize(n);
	
	for (int i = 0; i < n; i ++) cin >> v[i].x >> v[i].y;
	
	sort(v.begin(), v.end());		// 排序
	
	for (int i = 0, j = 0; i < n; i ++)
	{
	    int id = v[i].y;
		cnt[id] ++;		// 刚进来的帖子, 该id的帖子数量加1
		
        // 不理解为什么用的while 而不是 if的话, 看代码下面的图片
		while (v[i].x - v[j].x >= d) // 使时间区间满足 [t, t + d - 1]
		{
			cnt[v[j].y] --;
			j ++;
		}
		
		if (cnt[id] >= k) st[id] = true;	// 是否是热帖
	}

	for (int i = 0; i < N; i ++)
		if (st[i]) cout << i << endl;
	return 0;
}

当 d = 10, t = 25的时候, 此时下标 j 应该在18的下标的前面, 因为 25 - 18 = 7 (小于d), 当 i ++ 后, t = 30, 此时 j 应该是21对应的下标
下标i加一, j 加的可能不只是1


总结

滑动窗口算法是一种用于解决子数组或子串问题的有效技巧.

常见类型:

  1. 固定长度的子数组/子串

    • 问题:找到固定长度的子数组或子串,并求其某些特征(如最大或最小值、平均值等)。
    • 例子:给定一个数组和一个整数k,找到长度为k的子数组的最大平均值。
  2. 可变长度的子数组/子串

    • 问题:找到可变长度的子数组或子串,使其满足某些条件(如和等于某个值、包含某些字符等)。
    • 例子:给定一个数组,找到和等于某个值的最长子数组。
  3. 最长或最短的子数组/子串

    • 问题:找到满足某些条件的最长或最短子数组或子串。
    • 例子:给定一个字符串和一个整数k,找到包含最多k个不同字符的最长子串。

本题可以看成是第三种类型, 使每个id的维护的窗口都满足“最早出现的帖子的时刻刚进来的帖子时刻 相差不超过 d”,并且使窗口中的帖子尽可能多

滑动窗口算法通常用于需要在线性时间内解决子数组或子串问题的场景。这些问题的特征通常包括寻找具有特定属性的最长或最短子数组/子串,以及满足某些条件的子数组/子串。这种技术通过动态调整窗口的大小,使得算法能在高效的时间复杂度内找到问题的解。

觉得写的不错的话, 点个赞吧~

  • 12
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值