Description
给定数组arr和整数num,求arr的连续子数组中满足:其最大值减去最小值的结果大于num的个数。请实现一个时间复杂度为O(length(arr))的算法。
Input
输入第一行为测试用例个数。每一个用例有若干行,第一行为数组,每一个数用空格隔开,第二行为num。
Output
输出一个值。
Sample Input 1
1
3 6 4 3 2
2
Sample Output 1
6
思路
1.暴力
代码:
#include <iostream>
#include <string>
#include <sstream>
#include <vector>
#include <limits.h>
#include <algorithm>
using namespace std;
int main()
{
int t;
cin >> t;
for (int turn = 0; turn < t; turn++) {
string str;
stringstream ss;
int sin;
vector<int> a;
cin.ignore();
getline(cin, str);
ss << str;
while (ss >> sin) {
a.push_back(sin);
}
int aLen = a.size();
int num;
cin >> num;
int res = 0;
//暴力法
for (int i = 0; i < aLen; i++) {
int maxNum = INT_MIN, minNum = INT_MAX;
for (int j = i; j < aLen; j++) {
minNum = min(minNum, a[j]);
maxNum = max(maxNum, a[j]);
res += maxNum - minNum > num ? 1 : 0;
}
}
if (turn + 1 == t) printf("%d", res);
else printf("%d\n", res);
}
return 0;
}
2.滑动窗口法
首先,对于满足最大值减去最小值的结果大于num的个数的连续子数组,有这样一个性质,即包含该子数组的所有其他子数组一定也符合条件,因为不管包含该子数组的数组再如何扩张,其min值一定只会减小,max值一定只会增大,二者差值一定是只会增大。但这条性质的前提是,扩张的数组想要符合条件至少要包含最初符合条件的子数组的全部元素。
反之,另一个性质是,若一个子数组不符合条件,那么在该子数组范围内部的子数组也不会符合条件,类似第一个性质,这些内部子数组min和max差值一定只会减小,更不可能符合条件。
有了这两条性质就可以采用滑动窗口来解决本题,用两个双端队列分别存放min值和max值。
设置i, j两个遍历指针,初始都指向第一个元素,所以两个队列初始值都是第一个元素。
Step1. 首先令j向右移动,把qmin队尾所有大于a[j]值的元素全部弹出并在其尾部压入a[j],或把qmax队尾所有小于a[j]的元素全部弹出并在其尾部压入a[j],则用当前j指向元素替换之,并判断此时qmin和qmax队列的队首元素差值是否大于num。若当j便利至某元素符合上述条件时,那么从j所指向元素后一个元素开始一直到最后一个元素与当前i到j之间的子数组组成的所有子数组都符合条件,共有a.size()-j+1个,此时就跳出j的遍历循环,j固定不动。
Step2. 接下来,先对qmin和qmax两个队列进行判断,若两个队列都不为空,且两队首值的差大于num,说明上一轮循环得出的符合条件的子数组的最大值或最小值都还在队列中,比如测试样例数组为4, 6, 4, 3, 2,当i==0时,找到第一个符合条件的子数组是4, 6, 4, 3,最小值3和最大值6由于不是a[0]指向的元素因此没有弹出,当i==2时,3和6仍在原队列中且符合条件,那么[6, 4, 3]及[6, 4, 3, 2]也是符合结果的子数组,这里做一次res+=a.size()-j+1,防止遍历中这两个符合条件的子数组被跳过。
Step3. 然后对qmin和qmax队列的元素做判断,i指向的元素是否是qmin或qmax队首元素,是则要弹出,之后再重复Step1.
#include <iostream>
#include <string>
#include <sstream>
#include <vector>
#include <limits.h>
#include <algorithm>
#include <deque>
using namespace std;
int main()
{
int t;
cin >> t;
for (int turn = 0; turn < t; turn++) {
string str;
stringstream ss;
int sin;
vector<int> a;
cin.ignore();
getline(cin, str);
ss << str;
while (ss >> sin) {
a.push_back(sin);
}
int aLen = a.size();
int num;
cin >> num;
int res = 0;
//滑动窗口法
deque<int> qmin;
deque<int> qmax;
int i = 0, j = 0;
while(i < aLen) {
if (!qmin.empty() && !qmax.empty() && (i < j) && a[qmax.front()] - a[qmin.front()] > num) {
res += aLen - j + 1;
}
else {
while (j < aLen) {
while (!qmax.empty() && a[j] >= a[qmax.back()]) {
qmax.pop_back();
}
qmax.push_back(j);
while (!qmin.empty() && a[j] <= a[qmin.back()]) {
qmin.pop_back();
}
qmin.push_back(j);
j++;
//若当前遍历到的子数组中最大最小值差值已经大于num,那么j再向右遍历的所有包含这个子数组的所有其他子数组也符合条件,跳出当前循环
//min只会更小,max只会更大,二者差值只会更大
if (a[qmax.front()] - a[qmin.front()] > num) {
//j从当前位置一直遍历到数组最后一个元素所形成的连续子数组都符合条件
//此时j已增长1,但实际遍历过的是j-1
//[i,...,j-1], [i,...,j], ... , [i,...,aLen-1]
res += aLen - j + 1;
break;
}
}
}
//若当前队列队首元素为i,则将其弹出,因为其不会再出现在之后的遍历中
if (qmin.front() == i) qmin.pop_front();
if (qmax.front() == i) qmax.pop_front();
i++;
}
if (turn + 1 == t) printf("%d", res);
else printf("%d\n", res);
}
return 0;
}