首先在看例题前,给大家介绍一种常用的算法——尺取法。
尺取法也被称为双指针、two pointers,是算法竞赛中一个常用的优化技巧,是用来解决序列的区间问题。操作简单、容易编程。简单来说,可以把两重循环转化为一重循环,从而把时间复杂度从 O(n2)提高到 O(n)。
首先看一个用 i 和 j执行的两重循环:
for(int i = 0; i < n; i++) //i从头扫到尾
for(int j = n-1; j >= 0; j--){ //j从尾扫到头
......
}
其中 i从 0 循环到 n−1,j 反过来从 n−1 循环到 0 。这两重循环的时间复杂度是 O(n2) 的。
尺取法可以在一个循环中一起处理 i,j,代码如下,这样复杂度就从 O(n2)变成了 O(n)。
for (int i = 0, j = n - 1; i < j; i++, j--) {
......
}
还有另一种while()写法:
//用while实现:
int i = 0, j = n - 1;
while (i < j) { //i和j在中间相遇。这样做还能防止i、j越界
...... //满足题意的操作
i++; //i从头扫到尾
j--; //j从尾扫到头
}
以上只是一种反向扫描,患有另一种同向扫描。
蓝桥杯大佬做出了总结,看看下面就会明白了:
把循环指针 i 、j 称为 扫描指针,在尺取法中,这两个指针 i、j,有两种扫描方向:
反向扫描 i、j 方向相反,i 从头到尾,j 从尾到头,在中间相会。也可以把反向扫描的 i、j 指针称为左右指针。
同向扫描 i、j 方向相同,都从头到尾,但是速度不一样,比如可以让 j 跑在 i 前面。也可以把同向扫描的 i、j 指针称为快慢指针,此时由于 i 和 j 速度不同,i 和 j 之间在序列上产生了一个大小可变的滑动窗口,这是尺取法的优势,有灵活的应用。
注意,用尺取法的最关键之处在于,两个指针 i、j 在总体上只能有一个循环,例如:i 循环一遍,对应的 j 只能跟随 i 循环一遍。这样才能实现计算复杂度从 O(n2) 到 O(n) 的优化。
接下来是一个省赛的真题,用python暴力法,和c++的尺取法复杂度差不多:
小明维护着一个程序员论坛。现在他收集了一份"点赞"日志,日志共有 N 行。其中每一行的格式是:
ts id
表示在 ts 时刻编号 id 的帖子收到一个"赞"。
现在小明想统计有哪些帖子曾经是"热帖"。如果一个帖子曾在任意一个长度为 D 的时间段内收到不少于 K个赞,小明就认为这个帖子曾是"热帖"。
具体来说,如果存在某个时刻 T 满足该帖在[T,T+D) 这段时间内(注意是左闭右开区间)收到不少于 K 个赞,该帖就曾是"热帖"。
给定日志,请你帮助小明统计出所有曾是"热帖"的帖子编号。
第一行包含三个整数 N,D,K。
以下 N 行每行一条日志,包含两个整数 ts 和 id。
其中,1<=K<=N<=105,0<=ts<=105,0<=id<=105。
按从小到大的顺序输出热帖 id。每个 id 一行。
7 10 2
0 1
0 10
10 10
10 1
9 1
100 3
100 3
1
3
虽然我们上面介绍了尺取法,这个可以作为了解,本题使用python的暴力法和c++的尺取法复杂度差不多,所以在这里我首先采用python暴力法(因为代码好写,嘿嘿):
from bisect import bisect_left
maxn=int(1e5+50)
n,d,k=map(int,input().split())
m=[[] for _ in range(maxn)]
post=set()
for _ in range(n):
ts,idd=map(int,input().split())
post.add(idd)
m[idd].append(ts) #读每个帖子的赞的时间
post = sorted(post) #对帖子id排序
for idd in post: #检查每个帖子
m[idd]=sorted(m[idd]) #把某个帖子的ts排序
for i in range(len(m[idd])): #暴力统计这个帖子是不是热帖
td = m[idd][i]+d
if(bisect_left(m[idd],td)-i >= k):
print(idd)
break