1. 核心思想
通过某种线性关系将原先O(n2)的朴素算法优化为O(n)的快速算法
2. 一般应用思路
(1)先写一个暴力的朴素O(n2)的算法
(2)找到两个指针之间的单调关系
(3)如果有单调关系,可以将枚举从O(n2)优化到O(n)
3. 模板
for(int i = 0, j = 0; i < n; i++)
{
while(j < i && check(i, j)) j++;//具体问题的逻辑
}
常见问题的分类:
(1)对于一个序列,用两个指针维护一段区间,如:快速排序中用两个指针调整区间
(2) 对于两个序列,维护某种次序,比如归并排序中合并两个有序序列的操作
例题1:单词分割
把一个句子中用空格隔开的单词每个一行输出
#include<iostream>
#include<string.h>
using namespace std;
int main()
{
char str[10010];
gets(str);
int n = strlen(str); //字符串总长度
for(int i = 0; i < n; i++)
{
int j = i; //每次j指针从i所在下标开始
while (j<n && str[j] != ' ')
{
cout << str[j];
j++;
}
cout << endl;
i = j;
}
return 0;
}
例2:最长连续不重复子序列
给定一个长度为n的整数序列,请找出最长的不包含重复数字的连续区间,输出它的长度。
输入格式
第一行包含整数n。
第二行包含n个整数(均在0~100000范围内),表示整数序列。
输出格式
共一行,包含一个整数,表示最长的不包含重复数字的连续子序列的长度。
数据范围
1≤n≤100000
输入样例:
5
1 2 2 3 5
输出样例:
3
一般朴素做法:
用一个两层循环
for(int i = 0; i < n; i++)
for(int j = i; j < n; j++)
i在前,j在后维护的一段区间判重
这样j每次都要退回,时间复杂度为O(n^2)
双指针做法:
让j在前, i在后维护一段区间
相当于一个队列,外层循环控制队尾,内层循环控制队头
#include<iostream>
using namespace std;
const int N = 100010;
int n;
int a[N], s[N];
int main()
{
cin >> n;
for(int i = 0; i < n; i++) cin >> a[i];
int res = 0; //序列长度
for(int i = 0, j = 0; i < n; i++)
{
s[a[i]] ++; //两个指针维护的区间内每个元素出现的次数
//如果区间内有重复数字,把前面一个指针j向后移动
while(s[a[i]] > 1)
{
s[a[j]] --;
j++;
}
res = max(res, i - j + 1); //计算最大序列长度
}
cout << res << endl;
return 0;
}
例3:日志统计
小明维护着一个程序员论坛。现在他收集了一份”点赞”日志,日志共有 N 行。
其中每一行的格式是:
ts id
表示在 ts 时刻编号 id 的帖子收到一个”赞”。
现在小明想统计有哪些帖子曾经是”热帖”。
如果一个帖子曾在任意一个长度为 D 的时间段内收到不少于 K 个赞,小明就认为这个帖子曾是”热帖”。
具体来说,如果存在某个时刻 T 满足该帖在 [T,T+D) 这段时间内(注意是左闭右开区间)收到不少于 K 个赞,该帖就曾是”热帖”。
给定日志,请你帮助小明统计出所有曾是”热帖”的帖子编号。
输入格式
第一行包含三个整数 N,D,K。
以下 N 行每行一条日志,包含两个整数 ts 和 id。
输出格式
按从小到大的顺序输出热帖 id。
每个 id 占一行。
数据范围
1≤K≤N≤105,
0≤ts,id≤105,
1≤D≤10000
输入样例:
7 10 2
0 1
0 10
10 10
10 1
9 1
100 3
100 3
输出样例:
1
3
解题思路:
利用pair存储每条记录,排序后用双指针模拟队列,每次入队一个记录,并判断当前队列长度,如果队列长度大于限制区间长度,就从队头出队一个元素。入队出队的同时记录当前队列中元素出现的次数,如果大于k就输出
#include<iostream>
#include<algorithm>
#include<vector>
#include<cstring>
using namespace std;
const int N = 100010;
typedef pair<int, int> PII;
int n, d, k;
int cnt[N];
PII logs[N];
bool st[N];
int main()
{
cin >> n >> d >> k;
for(int i = 0; i < n; i++)
cin >> logs[i].first >> logs[i].second;
sort(logs, logs+n);
memset(st, false, sizeof st);
for(int i = 0, j = 0; i < n; i++)
{
int id = logs[i].second;
cnt[id]++;
while(logs[i].first - logs[j].first >= d)
{
cnt[logs[j].second]--;
j++;
}
if(cnt[id] >= k) st[id] = true;
}
for(int i = 0; i < N; i++)
if(st[i]) cout << i << endl;
return 0;
}
例4:完全二叉树的权值
给定一棵包含 N 个节点的完全二叉树,树上每个节点都有一个权值,按从上到下、从左到右的顺序依次是 A1,A2,⋅⋅⋅AN,如下图所示:
现在小明要把相同深度的节点的权值加在一起,他想知道哪个深度的节点权值之和最大?
如果有多个深度的权值和同为最大,请你输出其中最小的深度。
注:根的深度是 1。
输入格式
第一行包含一个整数 N。
第二行包含 N 个整数 A1,A2,⋅⋅⋅AN。
输出格式
输出一个整数代表答案。
数据范围
1≤N≤105,
−105≤Ai≤105
输入样例:
7
1 6 5 4 3 2 1
输出样例:
2
解题思路:
题目要求求出和值最大的层数。按照完全二叉树的标号顺序,可以找到每一层起始元素与元素个数,所以只要利用双指针算法,一个指针遍历起始元素,另一个指针遍历每一层各个元素累加,就可以求出每层的和值,维护最大值即可。
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 100010;
typedef long long LL;
int a[N];
int n;
int main()
{
cin >> n;
for(int i = 1; i <= n; i++) scanf("%d", &a[i]);
LL maxv = -1e18;
int depth = 0;
for(int i = 1, d = 1; i <= n; d++, i *= 2)
{
LL sum = 0;
for(int j = i; j - i + 1 <= 1 << d - 1 && j <= n; j++)
sum += a[j];
if(sum > maxv)
{
maxv = sum;
depth = d;
}
}
cout << depth << endl;
return 0;
}