洛谷 P3512 [POI2010]PIL-Pilots

Description

给定 \(n\)\(k\) 和一个长度为 \(n\) 的序列,求最长的最大值最小值相差不超过 \(k\) 的子段。

Constraints

\(0 \le k \le 2*10^9\)\(1 \le n \le 3*10^6\)

Solution 1

考虑暴力。

暴力枚举最后答案的长度,然后再暴力寻找。

时间复杂度\(O(n^3)\),超时。

Solution 2

发现最终答案满足二分性质。

二分最终答案长度,固定区间后,就类似于滑动窗口一题,利用单调队列来维护区间里的最大值、最小值。

时间复杂度为 \(O(nlogn)\),由于 \(n\) 较大,常数过大可能会被卡掉一个点。

Code

// by youyou2007 in 2022
#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <queue>
#include <stack>
#include <map>
#define REP(i, x, y) for(int i = x; i < y; i++)
#define rep(i, x, y) for(int i = x; i <= y; i++)
#define PER(i, x, y) for(int i = x; i > y; i--)
#define per(i, x, y) for(int i = x; i >= y; i--)
#define lc (k << 1)
#define rc (k << 1 | 1)
using namespace std;
const int N = 3E6 + 5;
int n, k;
int a[N]; 
int low, mid, high, ans = 0;
int qmin[N], qmax[N];
bool check(int kk)//判断长度为mid是否可行
{
	int sum = 0x3f3f3f3f;
	int minhead = 1, mintail = 0, maxhead = 1, maxtail = 0;
	qmin[mintail] = 1, qmax[maxtail] = 1;
	rep(i, 1, n)
	{
		while(minhead <= mintail && qmin[minhead] <= i - kk)//如果队列头元素超过了区间范围,则需要从队列前面弹出
		{
			minhead++;
		}
		while(maxhead <= maxtail && qmax[maxhead] <= i - kk)//最大值同理
		{
			maxhead++;
		}
		while(minhead <= mintail && a[qmin[mintail]] >= a[i])//如果维护最小值的队列里,队尾元素大于等于 $a[i]$,说明 $a[i]$ 更小,且位置更靠后,肯定更优
		{
			mintail--;
		}
		while(maxhead <= maxtail && a[qmax[maxtail]] <= a[i])
		{
			maxtail--;
		} 
		qmin[++mintail] = i;
		qmax[++maxtail] = i;
		if(i >= kk)
		{
			if(a[qmax[maxhead]] - a[qmin[minhead]] <= k) return 1;只要能找到任意一段满足最大最小值差不超过k,则说明有解
		}
	}
//	cout << kk<<" " << sum<<endl;
	return 0;
}
int main()
{
	scanf("%d%d", &k, &n);
	rep(i, 1, n)
	{
		scanf("%d", &a[i]);
	}
	low = 0, high = n;
	while(low <= high)
	{
	//	cout << low << " " << high << endl;
		mid = (low + high) / 2;
		if(check(mid) == 1)//如果mid可行则向右半边寻找
		{
			ans = mid;
			low = mid + 1;
		}
		else//否则向左半边寻找
		{
			high = mid - 1;
		}
	}
	printf("%d", ans);
	return 0;
}

Solution 3

上一个算法太靠人品了,还要再优化

发现我们可以不用去先确定答案长度,而是在维护单调队列时求出

考虑维护两个单调队列,分别维护的是当前以 \(a[i]\) 结尾的不上升(最大值)、不下降(最小值)的子序列,子序列中每个数的下标

在每一个位置判断一下这两个队列头位置下标所对应值的差是否大于k,如果大于,则将这两个队列中队头位置靠前(为了保持合法区间尽量大)的不断弹出,直到差值 \(≤k\)

然后更新一下合法序列的左端点,即为弹出那个位置的后一个位置。每次更新一下序列长度的最大值就行了。

至于为什么要用不上升的维护最大值,不下降的维护最小值,其实也很好理解:

如果当前区间差值过大,说明最大值过大或最小值过小,为了减小差值,可以缩小最大值或扩大最小值。所以队列维护的都是可以缩小差值的元素。

时间复杂度 \(O(n)\)

Code:

// by youyou2007 in 2022
#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <queue>
#include <stack>
#include <map>
#define REP(i, x, y) for(int i = x; i < y; i++)
#define rep(i, x, y) for(int i = x; i <= y; i++)
#define PER(i, x, y) for(int i = x; i > y; i--)
#define per(i, x, y) for(int i = x; i >= y; i--)
#define lc (k << 1)
#define rc (k << 1 | 1)
using namespace std;
const int N = 3e6 + 5;
int n, a[N], k; 
int qmax[N], qmin[N];
int l = 1, ans = 1;//l表示的是当前区间的左端点,这里开始的答案ans就要为1,否则在n=1的情况下会出错!
int headmax = 1, tailmax = 1, headmin = 1, tailmin = 1;//一开始队列里就会有一个数
int main()
{
	scanf("%d%d", &k, &n);
	rep(i, 1, n)
	{
		scanf("%d", &a[i]);
	}
	qmax[1] = 1;
	qmin[1] = 1;
	rep(i, 2, n)
	{
		while(headmax <= tailmax && a[qmax[tailmax]] < a[i]) tailmax--;//最大值,却维护的是不上升的子段
		while(headmin <= tailmin && a[qmin[tailmin]] > a[i]) tailmin--;//最小值,维护的是不下降的子段
		qmin[++tailmin] = i;
		qmax[++tailmax] = i;
		while(a[qmax[headmax]] - a[qmin[headmin]] > k)//如果差值大于k,则要不断缩小区间
		{
			if(qmax[headmax] < qmin[headmin])//如果是维护最大值的队头位置靠前,说明优先缩小最大值
			{
				l = qmax[headmax] + 1; //更新左端点,既然最大值的位置不合法,那它后面就合法了,所以是最大值的位置+1
				headmax++;//弹出队头
			}
			else//反之也同理
			{
				l = qmin[headmin] + 1;
				headmin++;
			}	
		}		
		ans = max(ans, i - l + 1);//在每一个位置都要更新一次区间长度最大值
	}	
	printf("%d", ans);
	return 0;
}

posted @ 2022-01-18 23:32  panjx  阅读(48)  评论(0编辑  收藏  举报