1. 程式人生 > 實用技巧 >P6087 [JSOI2015]送禮物 [單調佇列+01分數規劃]

P6087 [JSOI2015]送禮物 [單調佇列+01分數規劃]

題目背景

\(JYY\)\(CX\) 的結婚紀念日即將到來,\(JYY\) 來到萌萌開的禮品店選購紀念禮物。

萌萌的禮品店很神奇,所有出售的禮物都按照特定的順序都排成一列,而且相鄰 的禮物之間有一種神祕的美感。於是,\(JYY\) 決定從中挑選連續的一些禮物,但究 竟選哪些呢?
題目描述

假設禮品店一共有 \(N\) 件禮物排成一列,每件禮物都有它的美觀度。排在第 \(i\ (1\leqslant i\leqslant N)\) 個位置的禮物美觀度為正整數 \(A_i\)​。\(JYY\) 決定選出其中連續的一段,即編號為 \(i,i+1,\cdots,j-1,j\) 的禮物。選出這些禮物的美觀程度定義為

\[\frac{M(i,j)-m(i,j)}{j-i+K} \]

其中 \(M(i,j)\) 表示 \(\max\{A_i,A_{i+1},\cdots,A_j\}\)\(m(i,j)\) 表示 \(\min\{A_i,A_{i+1},\cdots,A_j\}\)\(K\) 為給定的正整數。 由於不能顯得太小氣,所以 \(JYY\) 所選禮物的件數最少為 \(L\) 件;同時,選得太多也不好拿,因此禮物最多選 \(R\) 件。\(JYY\) 應該如何選擇,才能得到最大的美觀程度?由於禮物實在太多挑花眼,\(JYY\) 打算把這個問題交給會程式設計的你。

輸入格式

本題每個測試點有多組資料。

輸入第一行包含一個正整數 \(T\),表示有 \(T\) 組資料。

每組資料包含兩行。第一行四個非負整數 \(N,K,L,R\)。第二行包含 \(N\) 個正整數,依次表示 \(A_1,A_2,\cdots,A_n\)​。

輸出格式

輸出 \(T\) 行,每行一個非負實數,依次對應每組資料的答案,資料保證答案不 會超過 \(10^3\)。輸出四捨五入保留 \(4\) 位小數。

輸入輸出樣例

輸入

1
5 1 2 4
1 2 3 4 5

輸出

0.7500

說明/提示

對於 \(100\%\) 的資料,\(T\leqslant 10,N,K\leqslant 5\times 10^4\)\(1\leqslant A_i\leqslant 10^8\)

\(2\leqslant L,R\leqslant N\)

分析

單調佇列加\(01\)分數規劃好題!(前幾天剛剛學的\(01\)分數規劃)。

首先看到題目中的一個極其熟悉的柿子和要求的最大值,那麼這個題肯定就與\(01\)分數規劃有關了,毫無疑問。

我們先從題意入手,它的意思就是讓我們選出一段區間,讓區間最大值和最小值的差除以區間長度減一加上\(k\)的最大值。也就是題目中給的式子:

\[\frac{M(i,j) \ -\ m(i,j)}{j-i+k} \]

當然區間長度是有限制的,長度從\(L\)\(R\)

接下來我們根據這個題要求的答案入手,要求的是一個分數的最大值,那麼我們要做的就是讓分子儘量大或者分母儘量小。因為每個區間的最大和最小值都是確定好了的,那麼如果當前區間的最大值或最小值在區間內部,那麼長度可以縮小的,最優狀態就是區間的兩個邊界分別為最大和最小值。我們設\(a[i]\)陣列表示每個位置的值,區間從\(l\)\(r\),當前二分到的答案為\(mid\),那麼我們就得到了這樣的式子:

\[\frac{a[r] - a[l]}{r-l+k}\geqslant mid \]

因為最大值和最小值的位置可以交換,那麼上式中\(r\)\(l\)可以互換。所以我們對式子分母乘過去,然後移項得到了兩個式子:

\[(a[l]+l\times mid)-(a[r]+r\times mid)\geqslant k\times mid \]

\[(a[l]-l\times mid)-(a[r]-r\times mid)\geqslant k\times mid \]

而這不就是得到了\(01\)分數規劃中的那個式子了嗎。我們只需要利用一個\(c[i]\)陣列記錄\(a[i]+i\)\(a[i]-i\)兩種,然後用單調佇列維護最大值,最後判斷一下是否滿足上邊的那兩個式子,繼續進行二分即可。

還有一種情況,就是從某個區間中的最小和最大值之間距離比\(L\)小,那麼為了取最優解,就可以直接讓長度等於\(L\),這樣肯定是最優的,我們只需要用兩個單調佇列維護區間最大和最小值就可以算出答案了,最後只需要用這個算出來的答案跟二分出來的答案比較大小選大的就行了。

具體細節程式碼中註釋見:

程式碼

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn = 5e5+10;
const double eps = 1e-6;
double a[maxn];
double c[maxn];
int head1,head2,tail1,tail2;
int q1[maxn],q2[maxn];
int n,k,L,R;
int q3[maxn];
inline int read(){//快讀
	int s = 0,f = 1;
	char ch = getchar();
	while(ch < '0' || ch > '9'){
		if(ch == '-')f = -1;
		ch = getchar();
	}
	while(ch >= '0' && ch <= '9'){
		s = s * 10 + ch - '0';
		ch = getchar();
	}
	return s * f;
}
bool jud(double mid){//二分判斷函式
	memset(c,0,sizeof(c));
	for(int i=1;i<=n;++i){//記錄第一種相減的情況
		c[i] = a[i] - i * mid;
	}
	double ans = -100000.0;
	int head3=0,tail3=0;
	for(int i=L+1;i<=n;++i){//單調佇列維護最大值
		while(head3 < tail3 && i - q3[head3] >= R)head3++;
		while(head3 < tail3 && c[q3[tail3-1]] >= c[i-L])tail3--;
		q3[tail3++] = i-L;//記錄最小值的位置
		ans = max(ans,c[i] - c[q3[head3]]);
	}
	head3=0,tail3=0;
	for(int i=1;i<=n;++i){//找另一種相加的情況
		c[i] = a[i] + i * mid;
	}
	for(int i=n-L;i;--i){//單調佇列維護最大值
		while(head3 < tail3 && i - q3[head3] >= R)head3++;
		while(head3 < tail3 && c[q3[tail3-1]] >= c[i+L])tail3--;
		q3[tail3++] = i+L;
		ans = max(ans,c[i] - c[q3[head3]]);
	}
	return ans - mid * k >= 0;//判斷是否成立
}
double calc1(){//計算長度為L的情況下的答案
	double ans = -100000.0;
	head1 = head2 = tail1 = tail2 = 0;//一定要初始化,不然爆掉
	for(int i=1;i<=n;++i){
		if(i < L){//位置不超過L的時候只維護單調性
			while(head1 < tail1 && a[q1[tail1-1]] >= a[i])tail1--;
			while(head2 < tail2 && a[q2[tail2-1]] <= a[i])tail2--;
		}
		else {//位置超過L的時候維護單調性且長度超過L的時候彈出隊首
			while(head1 < tail1 && i - q1[head1] >= L)head1++;
			while(head2 < tail2 && i - q2[head2] >= L)head2++;
			while(head1 < tail1 && a[q1[tail1-1]] >= a[i])tail1--;
			while(head2 < tail2 && a[q2[tail2-1]] <= a[i])tail2--;
		}
		q1[tail1++] = i;
		q2[tail2++] = i;
		if(i >= L)//算答案
			ans = max(ans,(double)(a[q2[head2]] - a[q1[head1]]) / (L-1+k));
	}
	return ans;
}
int main(){
	int T = read();
	while(T--){
		n = read();
		k = read();
		L = read();
		R = read();
		for(int i=1;i<=n;++i){
			scanf("%lf",&a[i]);
		}
		double ans = calc1();//長度剛好為L的答案
		double l = 0,r = 1000000.0;
		while(r - l >= eps){//二分找到最優解
			double mid = (l + r) / 2;
			if(jud(mid))l = mid;
			else r = mid;
		}
		printf("%.4lf\n",max(ans,l));//取兩種情況的最大值
	}
	return 0;
}